|
2 | 2 | import {
|
3 | 3 | MapLibre,
|
4 | 4 | GeoJSON,
|
| 5 | + VectorTileSource, |
5 | 6 | FillLayer,
|
6 | 7 | LineLayer,
|
7 | 8 | zoomTransition,
|
|
23 | 24 | filterGeo,
|
24 | 25 | jenksBreaks,
|
25 | 26 | quantileBreaks,
|
| 27 | + createPaintObjectFromMetric, |
| 28 | + extractVectorMetricValues, |
26 | 29 | } from "./mapUtils.js";
|
27 | 30 | import NonStandardControls from "./NonStandardControls.svelte";
|
28 | 31 | import { replaceState } from "$app/navigation";
|
|
65 | 68 | hoverOpacity = 0.8,
|
66 | 69 | center = [-2.5, 53],
|
67 | 70 | zoom = 5,
|
68 |
| - minZoom = undefined, |
69 |
| - maxZoom = undefined, |
| 71 | + minZoom = 6, |
| 72 | + maxZoom = 14, |
70 | 73 | maxBoundsCoords = [
|
71 | 74 | [-10, 49],
|
72 | 75 | [5, 60],
|
|
96 | 99 | onstyleload,
|
97 | 100 | onstyledata,
|
98 | 101 | 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, |
99 | 110 | }: {
|
100 | 111 | data: object[];
|
| 112 | + paintObject?: object; |
101 | 113 | customPalette?: object[];
|
102 | 114 | cooperativeGestures?: boolean;
|
103 | 115 | standardControls?: boolean;
|
|
159 | 171 | onstyleload?: (e: StyleLoadEvent) => void;
|
160 | 172 | onstyledata?: (e: maplibregl.MapStyleDataEvent) => void;
|
161 | 173 | 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; |
162 | 182 | } = $props();
|
163 | 183 |
|
| 184 | + const tileSourceId = "lsoas"; |
| 185 | + const promoteProperty = "LSOA21NM"; |
| 186 | +
|
164 | 187 | let styleLookup = {
|
165 | 188 | "Carto-light":
|
166 | 189 | "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json",
|
|
199 | 222 | : colorbrewer[colorPalette][breakCount],
|
200 | 223 | );
|
201 | 224 |
|
202 |
| - let borderColor = "#003300"; |
| 225 | + let tooFewColors = $derived(fillColors.length < breakCount); |
203 | 226 |
|
| 227 | + $effect(() => { |
| 228 | + if (tooFewColors) { |
| 229 | + console.warn("Too few colours for the number of breaks"); |
| 230 | + } |
| 231 | + }); |
204 | 232 | let map: maplibregl.Map | undefined = $state();
|
205 | 233 |
|
206 | 234 | let loaded = $state(false);
|
|
245 | 273 |
|
246 | 274 | if (cooperativeGestures) {
|
247 | 275 | map?.cooperativeGestures.enable();
|
248 |
| - $inspect(cooperativeGestures); |
249 | 276 | } else {
|
250 | 277 | map?.cooperativeGestures.disable();
|
251 |
| - $inspect(cooperativeGestures); |
252 | 278 | }
|
253 | 279 |
|
254 | 280 | if (interactive) {
|
|
270 | 296 | }
|
271 | 297 |
|
272 | 298 | map?.setMaxBounds(bounds);
|
| 299 | +
|
| 300 | + map?.setMaxZoom(maxZoom); |
| 301 | + map?.setMinZoom(minZoom); |
273 | 302 | });
|
274 | 303 |
|
275 | 304 | let vals = $derived(
|
|
283 | 312 | ? quantileBreaks(vals, breakCount)
|
284 | 313 | : customBreaks,
|
285 | 314 | );
|
| 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 | + ); |
286 | 325 |
|
287 | 326 | let dataWithColor = $derived(
|
288 | 327 | filteredMapData.map((d) => {
|
|
382 | 421 | {style}
|
383 | 422 | {center}
|
384 | 423 | {zoom}
|
385 |
| - {maxZoom} |
386 |
| - {minZoom} |
387 | 424 | standardControls={interactive && standardControls}
|
388 | 425 | {hash}
|
389 | 426 | {updateHash}
|
|
437 | 474 | {:else if !interactive}
|
438 | 475 | <ScaleControl position={scaleControlPosition} unit={scaleControlUnit} />
|
439 | 476 | {/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" |
469 | 481 | 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, |
472 | 486 | }}
|
473 | 487 | 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} |
474 | 542 | />
|
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} |
477 | 563 |
|
| 564 | + <!-- Important note: sourceLayer must match `-l` value from tippecanoe --> |
478 | 565 | {#if interactive && tooltip}
|
479 | 566 | <Tooltip
|
480 | 567 | {currentMousePosition}
|
481 | 568 | {hoveredArea}
|
482 | 569 | {hoveredAreaData}
|
483 |
| - {year} |
484 |
| - {metric} |
| 570 | + metric={geoSource == "tiles" ? vectorMetricProperty : metric} |
485 | 571 | />
|
486 | 572 | {/if}
|
487 | 573 | </MapLibre>
|
|
0 commit comments