Skip to content

Commit d76465c

Browse files
authored
Merge pull request #253 from fieldsoftheworld/predictions-layer-performance
Improve global predictions performance
2 parents 8d7335f + 4284c4f commit d76465c

6 files changed

Lines changed: 126 additions & 73 deletions

File tree

package-lock.json

Lines changed: 18 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"geojson": "^0.5.0",
2020
"jwt-decode": "^4.0.0",
2121
"jwt-encode": "^1.0.1",
22-
"ol": "^10.8.0",
22+
"ol": "^10.9.0",
2323
"ol-mapbox-style": "^13.4.0",
2424
"ol-pmtiles": "^2.0.2",
2525
"pmtiles": "^4.4.0",

src/components/MapComponent.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useAreaOfInterest from '../composables/useAreaOfInterest'
1212
import usePermalink from '../composables/usePermalink'
1313
import useMap from '../composables/useMap'
1414
import useSettings from '../composables/useSettings'
15+
import { createXYZ } from 'ol/tilegrid'
1516
1617
const {
1718
map,
@@ -43,8 +44,9 @@ onMounted(async () => {
4344
target: 'map',
4445
layers: [createLabelLayer()],
4546
view: new View({
47+
maxResolution: createXYZ({ tileSize: 512 }).getResolution(0), // use Mapbox/MapLibre compatible resolutions
4648
center: [0, 0],
47-
zoom: 2,
49+
zoom: 1,
4850
}),
4951
})
5052

src/composables/useMap.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,19 @@ watch(globalOverviewLayer, (newLayer) => {
139139
untrackGlobalOverview = src ? trackTileSource(src) : null
140140
})
141141

142+
let thresholdDebounce: ReturnType<typeof setTimeout> | null = null
142143
watch(
143144
() => settings.value.threshold,
144145
() => {
145-
if (globalOverviewLayer.value) {
146-
updateGlobalOverviewLayer(globalOverviewLayer.value, settings.value)
147-
}
148-
if (globalPredictionsLayer.value) {
149-
updateGlobalPredictionsLayer(globalPredictionsLayer.value, settings.value)
150-
}
146+
if (thresholdDebounce) clearTimeout(thresholdDebounce)
147+
thresholdDebounce = setTimeout(() => {
148+
if (globalOverviewLayer.value) {
149+
updateGlobalOverviewLayer(globalOverviewLayer.value, settings.value)
150+
}
151+
if (globalPredictionsLayer.value) {
152+
updateGlobalPredictionsLayer(globalPredictionsLayer.value, settings.value)
153+
}
154+
}, 80)
151155
},
152156
)
153157

src/layers/Global-Predictions-Layer.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import { confidenceColorScale, getColorForValue } from './color-scales'
1111

1212
export function createGlobalPredictionsLayer(settings: Settings) {
1313
const layer = new VectorTileLayer({
14+
declutter: false,
1415
source: new PMTilesVectorSource({
16+
overlaps: false,
1517
url: get_global_pmtiles_url(settings.year),
1618
}),
1719
minZoom: GLOBAL_DATA_MAP_FIELD_START_ZOOM_LEVEL,
@@ -25,17 +27,28 @@ export function createGlobalPredictionsLayer(settings: Settings) {
2527

2628
export function updateGlobalPredictionsLayer(layer: VectorTileLayer, settings: Settings) {
2729
const key = `confidence_${GLOBAL_DATA_PMTILES_THRESHOLD_METRIC}`
28-
layer.setStyle((feature) => {
30+
const stroke = new Stroke({
31+
color: '',
32+
width: 1,
33+
lineCap: 'butt',
34+
lineJoin: 'miter',
35+
miterLimit: 1,
36+
})
37+
const fill = new Fill({ color: '' })
38+
const polyStyle = new Style({ stroke, fill })
39+
const smallStyle = new Style({ stroke })
40+
layer.setStyle((feature, resolution) => {
2941
const confidence = feature.get(key)
3042
if (confidence <= settings.threshold) return undefined
31-
return new Style({
32-
stroke: new Stroke({
33-
color: getColorForValue(confidenceColorScale, confidence, 1),
34-
width: 1,
35-
}),
36-
fill: new Fill({
37-
color: getColorForValue(confidenceColorScale, confidence, 0.3),
38-
}),
39-
})
43+
const strokeColor = getColorForValue(confidenceColorScale, confidence, 1)
44+
stroke.setColor(strokeColor)
45+
const extent = feature.getGeometry()!.getExtent()
46+
const widthPx = (extent[2] - extent[0]) / resolution
47+
const heightPx = (extent[3] - extent[1]) / resolution
48+
if (widthPx < 3 && heightPx < 3) {
49+
return smallStyle
50+
}
51+
fill.setColor(getColorForValue(confidenceColorScale, confidence, 0.3))
52+
return polyStyle
4053
})
4154
}

src/layers/color-scales.ts

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,80 @@ export const confidenceColorScale: ColorStop[] = [
2929
{ value: 0.58, color: '#33a02c', label: '58' },
3030
]
3131

32-
function hexToRgb(hex: string): [number, number, number] {
33-
return [
34-
parseInt(hex.slice(1, 3), 16),
35-
parseInt(hex.slice(3, 5), 16),
36-
parseInt(hex.slice(5, 7), 16),
37-
]
32+
const LUT_SIZE = 256
33+
34+
interface PrecomputedScale {
35+
lut: Uint8Array
36+
firstValue: number
37+
rangeInv: number
3838
}
3939

40-
export function getColorForValue(colorScale: ColorStop[], value: number, alpha = 1): string {
41-
const first = colorScale[0]
42-
const last = colorScale[colorScale.length - 1]
43-
let hex: string
44-
if (value <= first.value) {
45-
hex = first.color
46-
} else if (value >= last.value) {
47-
hex = last.color
48-
} else {
49-
hex = last.color
50-
for (let i = 0; i < colorScale.length - 1; i++) {
51-
if (value <= colorScale[i + 1].value) {
52-
const t = (value - colorScale[i].value) / (colorScale[i + 1].value - colorScale[i].value)
53-
const [r1, g1, b1] = hexToRgb(colorScale[i].color)
54-
const [r2, g2, b2] = hexToRgb(colorScale[i + 1].color)
55-
const r = Math.round(r1 + (r2 - r1) * t)
56-
const g = Math.round(g1 + (g2 - g1) * t)
57-
const b = Math.round(b1 + (b2 - b1) * t)
58-
hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
59-
break
40+
const scaleCache = new Map<ColorStop[], PrecomputedScale>()
41+
42+
function precomputeScale(colorScale: ColorStop[]): PrecomputedScale {
43+
const n = colorScale.length
44+
const stopValues = new Float64Array(n)
45+
const stopR = new Uint8Array(n)
46+
const stopG = new Uint8Array(n)
47+
const stopB = new Uint8Array(n)
48+
for (let i = 0; i < n; i++) {
49+
const hex = colorScale[i].color
50+
stopValues[i] = colorScale[i].value
51+
stopR[i] = parseInt(hex.slice(1, 3), 16)
52+
stopG[i] = parseInt(hex.slice(3, 5), 16)
53+
stopB[i] = parseInt(hex.slice(5, 7), 16)
54+
}
55+
56+
const firstValue = stopValues[0]
57+
const range = stopValues[n - 1] - firstValue
58+
const lut = new Uint8Array(LUT_SIZE * 3)
59+
60+
for (let i = 0; i < LUT_SIZE; i++) {
61+
const value = firstValue + (i / (LUT_SIZE - 1)) * range
62+
const offset = i * 3
63+
64+
let lo = 0
65+
let hi = n - 1
66+
if (value <= stopValues[0]) {
67+
lo = hi = 0
68+
} else if (value >= stopValues[n - 1]) {
69+
lo = hi = n - 1
70+
} else {
71+
for (let j = 0; j < n - 1; j++) {
72+
if (value <= stopValues[j + 1]) {
73+
lo = j
74+
hi = j + 1
75+
break
76+
}
6077
}
6178
}
79+
80+
if (lo === hi) {
81+
lut[offset] = stopR[lo]
82+
lut[offset + 1] = stopG[lo]
83+
lut[offset + 2] = stopB[lo]
84+
} else {
85+
const t = (value - stopValues[lo]) / (stopValues[hi] - stopValues[lo])
86+
lut[offset] = Math.round(stopR[lo] + (stopR[hi] - stopR[lo]) * t)
87+
lut[offset + 1] = Math.round(stopG[lo] + (stopG[hi] - stopG[lo]) * t)
88+
lut[offset + 2] = Math.round(stopB[lo] + (stopB[hi] - stopB[lo]) * t)
89+
}
90+
}
91+
92+
return { lut, firstValue, rangeInv: (LUT_SIZE - 1) / range }
93+
}
94+
95+
function getPrecomputed(colorScale: ColorStop[]): PrecomputedScale {
96+
let cached = scaleCache.get(colorScale)
97+
if (!cached) {
98+
cached = precomputeScale(colorScale)
99+
scaleCache.set(colorScale, cached)
62100
}
63-
const [r, g, b] = hexToRgb(hex)
64-
return `rgba(${r}, ${g}, ${b}, ${alpha})`
101+
return cached
102+
}
103+
104+
export function getColorForValue(colorScale: ColorStop[], value: number, alpha = 1): string {
105+
const { lut, firstValue, rangeInv } = getPrecomputed(colorScale)
106+
const idx = Math.min(Math.max(Math.round((value - firstValue) * rangeInv), 0), LUT_SIZE - 1) * 3
107+
return `rgba(${lut[idx]}, ${lut[idx + 1]}, ${lut[idx + 2]}, ${alpha})`
65108
}

0 commit comments

Comments
 (0)