Skip to content

Commit 2276005

Browse files
Merge pull request #148 from communitiesuk/vector-map-source
Vector map source
2 parents 3dbd69a + 4c65a5f commit 2276005

File tree

8 files changed

+343
-59
lines changed

8 files changed

+343
-59
lines changed

docs/TilesMaking.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Introduction
2+
3+
This will guide on how to turn geojson to pdf tiles which can be used with the map conpomenet. These instrutions are for Mac OS
4+
5+
# Steps
6+
7+
1. Get valid geojson. It should looks something like this
8+
9+
```json
10+
{
11+
"type": "FeatureCollection",
12+
"features": [
13+
{
14+
"type": "Feature",
15+
"properties": { "name": "Sample Point" },
16+
"geometry": {
17+
"type": "Point",
18+
"coordinates": [-0.1257, 51.5085]
19+
}
20+
}
21+
]
22+
}
23+
```
24+
25+
At this stage geographic dataa can be added so it can be visulised without need futher external jsons. Hoever if you
26+
27+
2. Convert Geojson to .mbTiles
28+
29+
Install home brew
30+
31+
Install tippe cannoe
32+
33+
run tippe cannoe
34+
35+
3. Convert .mbTiles to .pbf
36+
37+
install mbtiles
38+
39+
```python
40+
pip install mbtiles
41+
```

docs/TilesServer.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Tile server
2+
3+
Set up tile-server to serve the tiles locally
4+
5+
Make a server.cjs file that looks like this:
6+
7+
```javascript
8+
const express = require("express");
9+
const path = require("path");
10+
const fs = require("fs");
11+
const mime = require("mime-types");
12+
const cors = require("cors");
13+
14+
const app = express();
15+
const PORT = 8080;
16+
const TILE_DIR = path.join(__dirname, "tiles-with-imd-data"); // your z/x/y.pbf folder
17+
18+
app.use(cors()); // Enables CORS for all routes
19+
20+
// Set correct headers for .pbf files
21+
app.get("/:z/:x/:y.pbf", (req, res) => {
22+
const { z, x, y } = req.params;
23+
const tilePath = path.join(TILE_DIR, z, x, `${y}.pbf`);
24+
25+
if (!fs.existsSync(tilePath)) {
26+
return res.status(404).send("Tile not found");
27+
}
28+
res.setHeader("Access-Control-Allow-Origin", "*");
29+
res.setHeader("Content-Type", "application/x-protobuf");
30+
res.setHeader("Content-Encoding", "gzip");
31+
32+
fs.createReadStream(tilePath).pipe(res);
33+
});
34+
35+
// Optional: serve an index or static frontend here
36+
37+
app.listen(PORT, () => {
38+
console.log(`Tile server running at http://localhost:${PORT}/`);
39+
});
40+
```
41+
42+
Then from the terminal run:
43+
44+
```console
45+
node server.cjs
46+
```

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"sveaflet": "^0.1.4",
119119
"svelte-maplibre": "^1.0.0-next.12",
120120
"svelte-preprocess": "^6.0.3",
121+
"tileserver-gl": "^5.3.1",
121122
"topojson-client": "^3.1.0"
122123
}
123124
}

src/lib/components/data-vis/map/Map.svelte

Lines changed: 128 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
MapLibre,
44
GeoJSON,
5+
VectorTileSource,
56
FillLayer,
67
LineLayer,
78
zoomTransition,
@@ -23,6 +24,8 @@
2324
filterGeo,
2425
jenksBreaks,
2526
quantileBreaks,
27+
createPaintObjectFromMetric,
28+
extractVectorMetricValues,
2629
} from "./mapUtils.js";
2730
import NonStandardControls from "./NonStandardControls.svelte";
2831
import { replaceState } from "$app/navigation";
@@ -65,8 +68,8 @@
6568
hoverOpacity = 0.8,
6669
center = [-2.5, 53],
6770
zoom = 5,
68-
minZoom = undefined,
69-
maxZoom = undefined,
71+
minZoom = 6,
72+
maxZoom = 14,
7073
maxBoundsCoords = [
7174
[-10, 49],
7275
[5, 60],
@@ -96,8 +99,17 @@
9699
onstyleload,
97100
onstyledata,
98101
onidle,
102+
geoSource = "file",
103+
tileSource = "http://localhost:8080/{z}/{x}/{y}.pbf",
104+
geojsonPromoteId = "areanm",
105+
vectorMetricProperty = "Index of Multiple Deprivation (IMD) Decile",
106+
vectorLayerName = "LSOA",
107+
borderColor = "#003300",
108+
labelSourceLayer = "place",
109+
externalData = null,
99110
}: {
100111
data: object[];
112+
paintObject?: object;
101113
customPalette?: object[];
102114
cooperativeGestures?: boolean;
103115
standardControls?: boolean;
@@ -159,8 +171,19 @@
159171
onstyleload?: (e: StyleLoadEvent) => void;
160172
onstyledata?: (e: maplibregl.MapStyleDataEvent) => void;
161173
onidle?: (e: maplibregl.MapLibreEvent) => void;
174+
geoSource: "file" | "tiles" | "none";
175+
tileSource?: string;
176+
geojsonPromoteId?: string;
177+
vectorMetricProperty?: string;
178+
vectorLayerName?: string;
179+
borderColor?: string;
180+
labelSourceLayer?: string;
181+
externalData?: object;
162182
} = $props();
163183
184+
const tileSourceId = "lsoas";
185+
const promoteProperty = "LSOA21NM";
186+
164187
let styleLookup = {
165188
"Carto-light":
166189
"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
@@ -199,8 +222,13 @@
199222
: colorbrewer[colorPalette][breakCount],
200223
);
201224
202-
let borderColor = "#003300";
225+
let tooFewColors = $derived(fillColors.length < breakCount);
203226
227+
$effect(() => {
228+
if (tooFewColors) {
229+
console.warn("Too few colours for the number of breaks");
230+
}
231+
});
204232
let map: maplibregl.Map | undefined = $state();
205233
206234
let loaded = $state(false);
@@ -245,10 +273,8 @@
245273
246274
if (cooperativeGestures) {
247275
map?.cooperativeGestures.enable();
248-
$inspect(cooperativeGestures);
249276
} else {
250277
map?.cooperativeGestures.disable();
251-
$inspect(cooperativeGestures);
252278
}
253279
254280
if (interactive) {
@@ -270,6 +296,9 @@
270296
}
271297
272298
map?.setMaxBounds(bounds);
299+
300+
map?.setMaxZoom(maxZoom);
301+
map?.setMinZoom(minZoom);
273302
});
274303
275304
let vals = $derived(
@@ -283,6 +312,16 @@
283312
? quantileBreaks(vals, breakCount)
284313
: customBreaks,
285314
);
315+
let vectorPaintObject = $derived(
316+
externalData != null
317+
? createPaintObjectFromMetric(metric, breaks, fillColors, fillOpacity)
318+
: createPaintObjectFromMetric(
319+
vectorMetricProperty,
320+
breaks,
321+
fillColors,
322+
fillOpacity,
323+
),
324+
);
286325
287326
let dataWithColor = $derived(
288327
filteredMapData.map((d) => {
@@ -382,8 +421,6 @@
382421
{style}
383422
{center}
384423
{zoom}
385-
{maxZoom}
386-
{minZoom}
387424
standardControls={interactive && standardControls}
388425
{hash}
389426
{updateHash}
@@ -437,51 +474,100 @@
437474
{:else if !interactive}
438475
<ScaleControl position={scaleControlPosition} unit={scaleControlUnit} />
439476
{/if}
440-
441-
<GeoJSON id="areas" data={merged} promoteId="areanm">
442-
<FillLayer
443-
paint={{
444-
"fill-color": ["coalesce", ["get", "color"], "lightgrey"],
445-
"fill-opacity": changeOpacityOnHover
446-
? hoverStateFilter(fillOpacity, hoverOpacity)
447-
: fillOpacity,
448-
}}
449-
beforeLayerType="symbol"
450-
manageHoverState={interactive}
451-
onclick={interactive ? (e) => zoomToArea(e) : undefined}
452-
onmousemove={interactive
453-
? (e) => {
454-
hoveredArea = e.features[0].id;
455-
hoveredAreaData = e.features[0].properties.metric;
456-
currentMousePosition = e.event.point;
457-
}
458-
: undefined}
459-
onmouseleave={interactive
460-
? () => {
461-
hoveredArea = null;
462-
hoveredAreaData = null;
463-
}
464-
: undefined}
465-
/>
466-
{#if showBorder}
467-
<LineLayer
468-
layout={{ "line-cap": "round", "line-join": "round" }}
477+
{#if geoSource == "file"}
478+
<GeoJSON id="areas" data={merged} promoteId={geojsonPromoteId}>
479+
<FillLayer
480+
id="main-fill-layer"
469481
paint={{
470-
"line-color": hoverStateFilter(borderColor, "orange"),
471-
"line-width": zoomTransition(3, 0, 12, maxBorderWidth),
482+
"fill-color": ["coalesce", ["get", "color"], "lightgrey"],
483+
"fill-opacity": changeOpacityOnHover
484+
? hoverStateFilter(fillOpacity, hoverOpacity)
485+
: fillOpacity,
472486
}}
473487
beforeLayerType="symbol"
488+
manageHoverState={interactive}
489+
onclick={interactive ? (e) => zoomToArea(e) : undefined}
490+
onmousemove={interactive
491+
? (e) => {
492+
hoveredArea = e.features[0].id;
493+
hoveredAreaData = e.features[0].properties.metric;
494+
currentMousePosition = e.event.point;
495+
}
496+
: undefined}
497+
onmouseleave={interactive
498+
? () => {
499+
hoveredArea = null;
500+
hoveredAreaData = null;
501+
}
502+
: undefined}
503+
/>
504+
{#if showBorder}
505+
<LineLayer
506+
id="border-layer"
507+
layout={{ "line-cap": "round", "line-join": "round" }}
508+
paint={{
509+
"line-color": hoverStateFilter(borderColor, "orange"),
510+
"line-width": zoomTransition(3, 0, 12, maxBorderWidth),
511+
}}
512+
beforeLayerType="symbol"
513+
/>
514+
{/if}
515+
</GeoJSON>
516+
{:else if geoSource == "tiles"}
517+
<VectorTileSource
518+
id={tileSourceId}
519+
promoteId={promoteProperty}
520+
tiles={[tileSource]}
521+
>
522+
<FillLayer
523+
paint={vectorPaintObject}
524+
sourceLayer={vectorLayerName}
525+
onclick={interactive ? zoomToArea : undefined}
526+
onmousemove={interactive
527+
? (e) => {
528+
if (e.features?.[0]) {
529+
hoveredArea = e.features[0].id;
530+
hoveredAreaData =
531+
e.features[0].properties[vectorMetricProperty];
532+
currentMousePosition = e.event.point;
533+
}
534+
}
535+
: undefined}
536+
onmouseleave={interactive
537+
? () => {
538+
hoveredArea = null;
539+
hoveredAreaData = null;
540+
}
541+
: undefined}
474542
/>
475-
{/if}
476-
</GeoJSON>
543+
{#if showBorder}
544+
<LineLayer
545+
layout={{ "line-cap": "round", "line-join": "round" }}
546+
paint={{
547+
"line-color": hoverStateFilter(borderColor, "orange"),
548+
"line-width": zoomTransition(
549+
minZoom ?? 3,
550+
0,
551+
maxZoom ?? 14,
552+
maxBorderWidth,
553+
),
554+
}}
555+
beforeLayerType="symbol"
556+
sourceLayer={vectorLayerName}
557+
/>
558+
{/if}
559+
</VectorTileSource>
560+
{:else}
561+
<p>No data</p>
562+
{/if}
477563

564+
<!-- Important note: sourceLayer must match `-l` value from tippecanoe -->
478565
{#if interactive && tooltip}
479566
<Tooltip
480567
{currentMousePosition}
481568
{hoveredArea}
482569
{hoveredAreaData}
483-
{year}
484-
{metric}
570+
metric={geoSource == "tiles" ? vectorMetricProperty : metric}
485571
/>
486572
{/if}
487573
</MapLibre>

src/lib/components/data-vis/map/Tooltip.svelte

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script>
2-
let { currentMousePosition, hoveredArea, hoveredAreaData, year, metric } =
3-
$props();
2+
let { currentMousePosition, hoveredArea, hoveredAreaData, metric } = $props();
43
let tooltipHeight = $state();
54
let tooltipWidth = $state();
5+
66
// $inspect(tooltipHeight, tooltipWidth, currentMousePosition);
77
</script>
88

@@ -15,8 +15,7 @@
1515
>
1616
<p>{hoveredArea}</p>
1717
<p class="detail">
18-
{year}
19-
{metric}: {isNaN(hoveredAreaData) ? "No data" : hoveredAreaData}
18+
{metric}: {hoveredAreaData ?? "No data"}
2019
</p>
2120
</div>
2221
{/if}

0 commit comments

Comments
 (0)