Lesson 2 — Sources and layers
In Lesson 1 we built a controller that returns service area boundaries as GeoJSON. In this lesson we connect that endpoint to the Lab page’s map and watch real Australian polygons appear on the screen.
This is the moment Module 4 has been building toward — the database, the controller, and the map all become one working pipeline. The lesson is short on prose because most of the work is two new DSL calls inside the Lab page’s existing map.
By the end you’ll see all 340 SA3 boundaries rendered as filled polygons over the basemap. Style is deliberately minimal — flat-fill polygons, no outlines, no hover behaviour. Lesson 3 turns that into something that looks like the dispatch deck’s visual identity.
Sources and layers — what they are
MapLibre separates spatial data from visual representation through two concepts:
A source is a named bundle of data. It tells MapLibre where the data lives and how to fetch it. A source might be a GeoJSON URL, an inline GeoJSON object, a vector tile service, or a few other things. The source is just data — no styling, no visual presence.
A layer is a visual representation of a source’s features. It tells MapLibre what to draw (polygons, lines, circles, labels) and how to style it. A layer references a source by ID and adds the visual treatment.
The split matters because one source can drive multiple layers. We’ll initially add one fill layer for the service areas, but later modules will add an outline layer (showing borders), a hover layer (highlighting on mouseover), and possibly a label layer (showing SA3 names). All of those will reference the same source — MapLibre fetches the data once and the layers all consume it.
Adding the source and layer
Open the Lab page at app/views/lab/index.html.erb. Module 2
left it looking like this:
|
|
The Vera::Map.new(...) call accepts a block. Inside the block,
the DSL methods (source, layer, marker, etc.) are
available on the yielded m object. We’re going to convert
that single-line render into a block form and add a source plus
a layer inside it.
Change the map render from:
|
|
to:
|
|
Two new DSL calls inside the block — one for the source, one for the layer. Walk through what each is doing.
m.source :service_areas, url: "/service_areas.json" —
declares a source named :service_areas. The url: argument
points at the controller endpoint we built in Lesson 1.
MapLibre will fetch from this URL when the map initialises.
The source’s name (:service_areas) is how the layer
references it.
The source type defaults to :geojson, which is what we want.
Other types (vector tiles, raster tiles, image overlays) exist
for different data shapes; :geojson is correct here because
that’s what our endpoint produces.
m.layer :service_areas_fill, source: :service_areas, type: :fill, paint: { ... } —
declares a layer that draws polygons from the :service_areas
source as filled shapes. The paint: hash controls visual
properties.
fill_color: "#1e3a5f" — the polygon fill colour. This is
the dispatch deck’s primary blue from the design tokens. Most
fills will eventually use this colour.
fill_opacity: 0.15 — keeps the fill very translucent.
Polygons are large, and a fully-opaque fill would obscure the
basemap underneath. A 15% opacity lets the basemap show
through while still indicating each polygon’s presence.
A small note on naming: the layer’s ID is
:service_areas_fill — the source name plus a suffix
indicating what this particular layer does. Lesson 3 will add
an outline layer with the ID :service_areas_outline, both
referencing the same source. The naming pattern keeps related
layers grouped when you’re scanning the code or the MapLibre
debug output.
Lesson 3 explores fills, outlines, and hover states in proper depth. For now, flat-fill is enough to verify that the data is flowing correctly.
Activity 1 — Visit the Lab page and see polygons
Start the Rails server if it isn’t running:
|
|
Visit http://localhost:3000/lab in your browser. You should
see:
- The basemap centred on Australia (as before)
- A network of translucent blue polygons covering the entire country
- The polygons have no outlines and no hover behaviour yet — just flat fills
Pan and zoom. The polygons stay correctly anchored to the map because MapLibre re-projects them as you move. Zoom into Sydney and you can pick out individual SA3s. Zoom out to the whole country and Australia is fully covered.
If you don’t see anything:
- Open the browser’s Developer Tools (F12) and check the
Network tab. Look for a request to
/service_areas.json. It should return 200 OK with a JSON response of a few MB. If it’s missing, your block syntax may be wrong; if it’s a 404, check the route from Lesson 1. - Check the Console tab for any MapLibre errors. The most
common cause of “no polygons” is a typo in the source ID
reference — the layer’s
source:must exactly match the source’s first argument. - Make sure your Rails server is still running and reachable from the page.
Activity 2 — Inspect the source and layer
MapLibre keeps its current state inspectable through the browser console. With the Lab page open and Developer Tools showing, switch to the Console tab and have a poke around.
Open the Network tab first and find the service_areas.json
request. Click it, look at the Response tab — you should see
the GeoJSON FeatureCollection from Lesson 1, ~6 MB of polygon
data. That’s MapLibre’s view of the source data, fetched once
when the map loaded.
You can also confirm the layer rendered correctly by looking at
the canvas. The Lab page’s map div has a Stimulus controller
attached (vera--map) and inside it MapLibre creates a <canvas>
element where the actual rendering happens. In the Elements tab
you can see the canvas is live; in the Console you can see no
errors during initialisation.
This kind of inspection is genuinely useful for debugging. When polygons don’t render the way you expect, the answer is usually visible somewhere in the network requests, the console output, or the rendered DOM.
Where this leaves us
Real spatial data on the map. The pipeline:
- ServiceArea records in the database (Module 3)
- Service object converts records to GeoJSON (Lesson 1)
- Controller serves the JSON (Lesson 1)
- Map source fetches the JSON (this lesson)
- Map layer renders the polygons (this lesson)
Each piece does one thing. When something goes wrong in later modules, you can identify which piece is at fault and look at just that piece. This separation pays off.
Lesson 3 turns the bare flat-fill into styling that looks like
part of a real product — outlines, hover states, the visual
identity that makes the polygons feel like dispatch
infrastructure rather than test data. The data flow stays
identical; what changes is the layer’s paint: hash and one
or two new layers referencing the same source.