Skip to content

Commit 14d6280

Browse files
authored
Merge pull request #33 from henryspatialanalysis/feature/pmtiles-z10
Allow zooms down to Z10
2 parents 9877a67 + c858b26 commit 14d6280

8 files changed

Lines changed: 87 additions & 35 deletions

File tree

config.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,13 @@ publish:
322322
osm_snapshot_date: "2026-04-17" # YYYY-MM-DD Geofabrik download date
323323
overture_release: "2026-04-15.0" # Overture Maps release ID, https://docs.overturemaps.org/release-calendar/
324324
model_commit: null # null → use current git HEAD; set a short SHA to pin
325-
# PMTiles generation — single-zoom archive at z14 for both OSM and conflated.
326-
# Site's View.minZoom is 14; OpenLayers over-zooms past z14 natively so
327-
# z15-20 render as lossless geometric scale-ups of the z14 tile.
325+
# PMTiles generation — multi-zoom archive z10–z14 for both OSM and conflated.
326+
# Site's View.minZoom is 10 (full-metro view). z10–z14 are served directly
327+
# from the PMTiles; z15+ render as lossless OL over-zooms of the z14 tile.
328+
# drop-densest-as-needed silently drops features at lower zooms to keep
329+
# each tile under ~500 KB; per-zoom point radius scales down on the site.
328330
pmtiles:
329-
min_zoom: 14
331+
min_zoom: 10
330332
max_zoom: 14
331333
drop_strategy: "drop-densest-as-needed"
332334
osm_layer_name: "osm_pois"

scripts/conflation/prepare_pmtiles.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""
22
Build conflated.pmtiles from the conflated POI dataset.
33
4-
Output is a single-zoom PMTiles archive (z14) keyed by the config's
5-
``upload.pmtiles`` block. OpenLayers over-zooms z15-20 natively, and the site
6-
never renders below z14, so tiling extra zoom levels would just waste disk and
7-
wall time.
4+
Output is a multi-zoom PMTiles archive (z10-z14 by default) keyed by the
5+
config's ``publish.pmtiles`` block. ``drop-densest-as-needed`` silently
6+
drops features at lower zooms to keep each tile under ~500 KB; the site
7+
scales the point radius down at lower zooms to match. OpenLayers
8+
over-zooms z15+ natively as lossless geometric scale-ups of the z14 tile.
89
910
Intermediate FlatGeobuf is staged next to the output and deleted on success.
1011
"""

scripts/osm_snapshot/prepare_pmtiles.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""
22
Build osm_snapshot.pmtiles from the rated OSM snapshot.
33
4-
Output is a single-zoom PMTiles archive (z14) keyed by the config's
5-
``upload.pmtiles`` block. OpenLayers over-zooms z15-20 natively, and the site
6-
never renders below z14, so tiling extra zoom levels would just waste disk and
7-
wall time.
4+
Output is a multi-zoom PMTiles archive (z10-z14 by default) keyed by the
5+
config's ``publish.pmtiles`` block. ``drop-densest-as-needed`` silently
6+
drops features at lower zooms to keep each tile under ~500 KB; the site
7+
scales the point radius down at lower zooms to match. OpenLayers
8+
over-zooms z15+ natively as lossless geometric scale-ups of the z14 tile.
89
910
Intermediate FlatGeobuf is staged next to the output and deleted on success.
1011
"""

site/src/constants.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ export const BASE_MAP_STYLES = [
9292
export const CONFLATED_LABELS = SHARED_LABELS
9393

9494
// Zoom thresholds — PMTiles min_zoom (site can't zoom out below this).
95-
// Points above z14 are rendered via ol-pmtiles over-zoom.
96-
export const MIN_ZOOM = 14
95+
// PMTiles archives carry z10–z14; z15+ render via ol-pmtiles over-zoom.
96+
// Per-layer point radius scales down at lower zooms (see utils.js).
97+
export const MIN_ZOOM = 10
9798

9899
// Stadia Maps Geocoding
99100
export const STADIA_GEOCODING_URL =

site/src/layers/conflatedLayer.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import VectorTileLayer from 'ol/layer/VectorTile'
22
import { PMTilesVectorSource } from 'ol-pmtiles'
33
import { Style, Circle, Fill, Stroke } from 'ol/style'
4-
import { confidenceColor, discretizeConf } from '../utils.js'
4+
import {
5+
confidenceColor,
6+
discretizeConf,
7+
zoomTierFromResolution,
8+
POI_DOT_BY_ZOOM,
9+
} from '../utils.js'
510
import { CONFLATED_PMTILES_URL } from '../constants.js'
611

712
let layer = null
@@ -40,25 +45,28 @@ export function updateConflatedFilters(filtersObj) {
4045
if (layer) layer.changed()
4146
}
4247

43-
function conflatedTileStyle(feature) {
48+
function conflatedTileStyle(feature, resolution) {
4449
if (enabledLabels !== null) {
4550
const label = feature.get('shared_label')
4651
if (!enabledLabels.has(label)) return null
4752
}
4853

4954
const conf = feature.get('conf_mean')
5055
const bucket = discretizeConf(conf)
51-
if (!styleCache[bucket]) {
56+
const tier = zoomTierFromResolution(resolution)
57+
const key = `${bucket}|${tier}`
58+
if (!styleCache[key]) {
5259
const color = confidenceColor(conf == null || isNaN(conf) ? null : conf)
53-
styleCache[bucket] = new Style({
60+
const { radius, stroke } = POI_DOT_BY_ZOOM[tier]
61+
styleCache[key] = new Style({
5462
image: new Circle({
55-
radius: 5,
63+
radius,
5664
fill: new Fill({ color }),
57-
stroke: new Stroke({ color: '#fff', width: 1 }),
65+
stroke: new Stroke({ color: '#fff', width: stroke }),
5866
}),
5967
})
6068
}
61-
return styleCache[bucket]
69+
return styleCache[key]
6270
}
6371

6472
/**

site/src/layers/osmLayer.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import VectorTileLayer from 'ol/layer/VectorTile'
22
import { PMTilesVectorSource } from 'ol-pmtiles'
33
import { Style, Circle, Fill, Stroke } from 'ol/style'
4-
import { confidenceColor, discretizeConf } from '../utils.js'
4+
import {
5+
confidenceColor,
6+
discretizeConf,
7+
zoomTierFromResolution,
8+
POI_DOT_BY_ZOOM,
9+
} from '../utils.js'
510
import { OSM_PMTILES_URL } from '../constants.js'
611

712
// OSM filter keys that drive feature visibility. A feature is visible when at
@@ -39,7 +44,7 @@ export function updateOsmFilters(filtersObj) {
3944
if (layer) layer.changed()
4045
}
4146

42-
function osmTileStyle(feature) {
47+
function osmTileStyle(feature, resolution) {
4348
if (enabledKeys.size === 0) return null
4449

4550
// Visibility: feature must have at least one enabled key set.
@@ -54,17 +59,20 @@ function osmTileStyle(feature) {
5459

5560
const conf = feature.get('conf_mean')
5661
const bucket = discretizeConf(conf)
57-
if (!styleCache[bucket]) {
62+
const tier = zoomTierFromResolution(resolution)
63+
const key = `${bucket}|${tier}`
64+
if (!styleCache[key]) {
5865
const color = confidenceColor(conf == null || isNaN(conf) ? null : conf)
59-
styleCache[bucket] = new Style({
66+
const { radius, stroke } = POI_DOT_BY_ZOOM[tier]
67+
styleCache[key] = new Style({
6068
image: new Circle({
61-
radius: 5,
69+
radius,
6270
fill: new Fill({ color }),
63-
stroke: new Stroke({ color: '#fff', width: 1 }),
71+
stroke: new Stroke({ color: '#fff', width: stroke }),
6472
}),
6573
})
6674
}
67-
return styleCache[bucket]
75+
return styleCache[key]
6876
}
6977

7078
/**

site/src/layers/overtureLayer.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import VectorTileLayer from 'ol/layer/VectorTile'
22
import { PMTilesVectorSource } from 'ol-pmtiles'
33
import { Style, Circle, Fill, Stroke } from 'ol/style'
4-
import { confidenceColor, discretizeConf } from '../utils.js'
4+
import {
5+
confidenceColor,
6+
discretizeConf,
7+
zoomTierFromResolution,
8+
POI_DOT_BY_ZOOM,
9+
} from '../utils.js'
510
import { OVERTURE_PMTILES_URL } from '../constants.js'
611

712
let layer = null
@@ -28,23 +33,26 @@ export function getOvertureLayer() {
2833
*/
2934
export function updateOvertureFilters(_filtersObj) {}
3035

31-
function overtureTileStyle(feature) {
36+
function overtureTileStyle(feature, resolution) {
3237
const cats = tryParse(feature.get('categories'))
3338
if (cats?.primary === 'parking') return null
3439

3540
const conf = feature.get('confidence')
3641
const bucket = discretizeConf(conf)
37-
if (!styleCache[bucket]) {
42+
const tier = zoomTierFromResolution(resolution)
43+
const key = `${bucket}|${tier}`
44+
if (!styleCache[key]) {
3845
const color = confidenceColor(conf ?? null)
39-
styleCache[bucket] = new Style({
46+
const { radius, stroke } = POI_DOT_BY_ZOOM[tier]
47+
styleCache[key] = new Style({
4048
image: new Circle({
41-
radius: 5,
49+
radius,
4250
fill: new Fill({ color }),
43-
stroke: new Stroke({ color: '#fff', width: 1 }),
51+
stroke: new Stroke({ color: '#fff', width: stroke }),
4452
}),
4553
})
4654
}
47-
return styleCache[bucket]
55+
return styleCache[key]
4856
}
4957

5058
/**

site/src/utils.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,26 @@ export function confidenceColor(value) {
4242
const b = lerpChannel(lo.b, hi.b, t)
4343
return `rgb(${r},${g},${b})`
4444
}
45+
46+
// Spherical-mercator resolution → integer zoom tier in [10, 14].
47+
// 40075016.6855784 / 256 ≈ 156543.03 m/px at z0; resolution halves per zoom.
48+
// Clamps to the tile range that prepare_pmtiles emits, so z15+ over-zoom and
49+
// any sub-z10 view (shouldn't happen — View.minZoom is 10) both fall through.
50+
export function zoomTierFromResolution(resolution) {
51+
const z = Math.log2(156543.03392804097 / resolution)
52+
if (z >= 13.5) return 14
53+
if (z >= 12.5) return 13
54+
if (z >= 11.5) return 12
55+
if (z >= 10.5) return 11
56+
return 10
57+
}
58+
59+
// Radius / stroke width by zoom tier. Tuned so dense urban areas stop smearing
60+
// at low zoom while keeping z14 visually identical to the prior fixed-5 dots.
61+
export const POI_DOT_BY_ZOOM = {
62+
14: { radius: 5, stroke: 1 },
63+
13: { radius: 4, stroke: 1 },
64+
12: { radius: 3, stroke: 0.75 },
65+
11: { radius: 2.5, stroke: 0.5 },
66+
10: { radius: 2, stroke: 0.5 },
67+
}

0 commit comments

Comments
 (0)