Skip to content

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<% content_for   :title, "Lab — Vera Dispatch Manager" %>

<%= render Components::PageHeader.new(
      title:    "Lab",
      subtitle: "A development-only space for experimenting with map components.",
      breadcrumb: ["Development", "Lab"]
    ) %>

<div class="p-8">
  <div class="rounded-lg overflow-hidden border border-border bg-surface">
    <%= render Vera::Map.new(id: "lab-map", height: "600px" ) %>
  </div>
</div>

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:

1
<%= render Vera::Map.new(id: "lab-map", height: "600px" ) %>

to:

1
2
3
4
5
6
7
8
9
<%= render Vera::Map.new(id: "lab-map", height: "600px") do |m|
      m.source :service_areas, url: "/service_areas.json"

      m.layer :service_areas_fill, source: :service_areas, type: :fill,
              paint: {
                fill_color:   "#1e3a5f",
                fill_opacity: 0.15
              }
    end %>

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:

1
bin/rails server

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:

  1. ServiceArea records in the database (Module 3)
  2. Service object converts records to GeoJSON (Lesson 1)
  3. Controller serves the JSON (Lesson 1)
  4. Map source fetches the JSON (this lesson)
  5. 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.