Lesson 5 — Depots as points
The map currently shows service area boundaries — large polygons covering the country. Real dispatch software has more than just boundaries on its maps; it has the operational hubs that coordinate the work. Depots are the regional dispatch centres — the buildings where dispatchers run their shifts, coordinate with field officers across their region, and route work to the right people. There’s typically one per major regional area, and a dispatcher’s job is to manage the work flowing through their own depot.
In this lesson we put depots on the map. By the end, the Lab page will show the same SA3 polygons we already had, with nine depot pins overlaid — one for each major Australian city. Click a depot and a popup tells you which one it is.
The mechanics are familiar. We’ve already done one source/layer pair for service areas; this lesson adds a second source/layer pair for depots. The pattern of “multiple sources per map” is fundamental to building richer maps, and this lesson establishes it.
Seeding the depots
Module 3 Lesson 2 had us add Sydney Central and Brisbane North manually as we learned the basics of inserting PostGIS values. Two depots was enough for that lesson’s purposes; for a dispatch app it’s nowhere near enough. We need depots in every major capital.
Rather than insert seven more by hand, we’ll create a rake task that seeds them. This introduces a pattern that’s distinct from Module 3’s import task: where the SA3 import reads from a vendored data file, this seed reads from inline literal data in the rake task itself.
The distinction matters. SA3 boundaries come from an external authoritative source (the ABS) — they belong in a vendored file with proper provenance documentation. Depot locations are chassis-curated tutorial data — a plausible scattering of dispatch hubs we’ve made up for teaching purposes. Inlining them in the seed task is honest about what they are, and avoids adding a download to the appendix for what’s effectively just nine rows.
Create lib/tasks/seed.rake:
|
|
Worth understanding a few details:
upsert with unique_by: :code — re-running the task is
safe. Each upsert either inserts the row or updates the
existing one matched by code. If your manually-inserted
“Brisbane North” from Module 3 was identical to the seed value,
nothing changes; if you typed a slightly different name, the
seed brings it back to canonical.
The location: string — "POINT(#{d[:lng]} #{d[:lat]})"
is WKT (Well-Known Text), the format we met in Module 3. The
ActiveRecord postgis adapter parses WKT strings on assignment,
converts them to PostGIS geometry, and stores them in the
column. Note the order: longitude first, latitude second —
WKT’s coordinate convention.
No SRID prefix needed. The column constraint already forces SRID 4326, so the WKT doesn’t need to declare it. The adapter handles the SRID stamping on the way in.
Run the task:
|
|
Output:
|
|
Serving depots as GeoJSON
Same pattern as Lesson 1. We need a service object that turns depots into a GeoJSON FeatureCollection, plus a controller that serves it.
Create app/services/serialize_depots.rb:
|
|
Same shape as SerializeServiceAreas from Lesson 1. The only
differences are which model and which columns. The pattern
generalises nicely — for any new spatial model the dispatch
deck adds, the serializer follows this shape.
Note the top-level id: on each feature, just like
SerializeServiceAreas. Even though we don’t yet have hover or
selection on depots, the convention is consistent: every
GeoJSON feature in the dispatch deck has a top-level id, ready
to be addressed by feature-state if any layer needs it later.
Generate the controller:
|
|
Add the route:
|
|
Edit app/controllers/depots_controller.rb:
|
|
Verify it works by visiting http://localhost:3000/depots.json.
You should see a FeatureCollection with 9 features. The
geometry of each is a Point with [lng, lat] coordinates. The
properties have id, code, and name.
Adding the source and layer to the component
Now the visual side. Open app/components/service_area_map.rb
and add a depots source plus a circle layer to the map block.
The component grows; the Lab page stays untouched.
|
|
Three new pieces.
Second source. m.source :depots, url: "/depots.json". The
component now has two sources, fetched independently. MapLibre
parallelises the fetches — the depots GeoJSON and service areas
GeoJSON download in parallel.
Circle layer. type: :circle with circle-specific paint
properties:
circle_color: "#dc2626" — Tailwind red-600. The depots stand
out clearly against the muted blue polygon fills.
circle_radius: 6 — 6-pixel-radius circles. Visible from any
zoom level without dominating the map.
circle_stroke_color: "#ffffff" and circle_stroke_width: 2 —
white border around each circle. The stroke separates the
depot pins from the polygon fills underneath, ensuring they’re
visible even when the polygon is highlighted on hover.
A small on_hover: clause bumps circle_radius from 6 to 8
when the cursor hovers over a depot. This serves two purposes:
the depot subtly grows under the cursor, communicating
interactivity directly, and the gem’s hover wiring also
changes the cursor to a pointer — the standard web-UI signal
for “you can click this.” Both feed the same expectation:
when you hover something interactive, it should respond.
Popup template. popup: { template: "..." } opens a popup
when the user clicks a depot. The {{name}} and {{code}}
placeholders interpolate with each feature’s properties at
click time — no server round trip, no pre-rendered HTML in the
GeoJSON payload.
The template is HTML; you can use Tailwind classes inside if
you want — though the popup styling lives in MapLibre’s CSS
contexts and Tailwind utilities don’t always behave exactly as
they would in the rest of the app. For now, simple font-
and text- utilities work fine.
Activity 1 — See the depots on the map
Refresh the Lab page. You should now see:
- The same translucent blue SA3 polygons as before
- Plus nine red circles, one in each major Australian capital
- Hover over a polygon — it still highlights as before, the depot pins on top stay visible
Pan to each capital. Sydney’s in the south-east, Cairns up in the tropical north, Perth on the west coast, Hobart far south in Tasmania. The geographic spread shows Australia’s actual population distribution.
If something’s missing:
- Check the Network tab for the
/depots.jsonrequest — it should return 200 OK with 9 features - If the request 404s, check the
resources :depotsroute - If the request returns but no circles appear, check the
layer’s
source:references the right name (:depots, matching the source’s first argument)
Activity 2 — Click a depot
Click any of the red circles. A popup should appear showing the depot’s name and code.
Click another. The first popup closes; the second opens.
Click outside the depot (on a polygon, or empty area). The popup closes.
This is the simplest popup pattern Vera offers — a per-feature template interpolated client-side. Other patterns exist: server-rendered HTML carried in feature properties, lazy turbo-frame popups that fetch content on demand. For a small amount of static info per feature (like name and code), the client-side template is the simplest approach.
Where this leaves us
The map now shows two layers of meaningful spatial data — service areas as polygons, depots as points — with proper visual treatment for each. The component has grown to hold both; the Lab page is unchanged.
The architectural shape that’s now in place:
- Each spatial model has a serializer in
app/services/ - Each spatial model has a controller serving it as JSON
- Map components compose multiple sources/layers in their
view_template
This pattern repeats from here. When Module 5 introduces
technicians, they get SerializeTechnicians, a controller, and
a layer added to whichever map needs them. When Module 6
introduces jobs, same pattern. The dispatch deck’s map
components grow by adding layers; the data plumbing stays
uniform.
Module 4 ends here. Module 5 takes the Lab page out of the
spotlight: authentication and roles arrive, product pages
appear (/dispatch, /jobs, /technicians), and the
dispatcher’s view starts coming together. Module 6 lights up
the first real product page with state-scoped service areas,
click-to-drilldown, and the spatial joins that make it work.