Skip to content

Commit 61f5140

Browse files
committed
Display conflated POIs as a third map option.
1 parent 5dde199 commit 61f5140

11 files changed

Lines changed: 453 additions & 12 deletions

File tree

site/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,12 @@
33
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
44

55
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
6+
7+
## Maintenance
8+
9+
### Conflated category labels
10+
11+
The conflated data source filter uses a static list of `shared_label` values
12+
defined in `src/constants.js` (`CONFLATED_LABELS`). This list is sourced from
13+
`src/openpois/conflation/data/match_radii.csv`. If the taxonomy crosswalk adds
14+
or removes labels, update both files to keep them in sync.

site/src/App.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111
:active-source="activeSource"
1212
:osm-filters="osmFilters"
1313
:overture-filters="overtureFilters"
14+
:conflated-filters="conflatedFilters"
1415
@zoom-changed="currentZoom = $event"
1516
/>
1617
<AmenityFilter
1718
:active-source="activeSource"
1819
:osm-filters="osmFilters"
1920
:overture-filters="overtureFilters"
21+
:conflated-filters="conflatedFilters"
22+
:conflated-labels="CONFLATED_LABELS"
2023
@update:osm-filters="osmFilters = $event"
2124
@update:overture-filters="overtureFilters = $event"
25+
@update:conflated-filters="conflatedFilters = $event"
2226
/>
2327
</template>
2428

@@ -28,7 +32,11 @@ import SourceToggle from './components/SourceToggle.vue'
2832
import SearchBar from './components/SearchBar.vue'
2933
import MapContainer from './components/MapContainer.vue'
3034
import AmenityFilter from './components/AmenityFilter.vue'
31-
import { OSM_FILTER_KEYS, OVERTURE_CATEGORIES } from './constants.js'
35+
import {
36+
OSM_FILTER_KEYS,
37+
OVERTURE_CATEGORIES,
38+
CONFLATED_LABELS,
39+
} from './constants.js'
3240
3341
const activeSource = ref('osm')
3442
const currentZoom = ref(4)
@@ -40,6 +48,12 @@ const osmFilters = ref(
4048
const overtureFilters = ref(
4149
OVERTURE_CATEGORIES.reduce((acc, c) => ({ ...acc, [c.key]: true }), {})
4250
)
51+
const conflatedFilters = ref(
52+
CONFLATED_LABELS.reduce((acc, lbl) => ({
53+
...acc,
54+
[lbl]: !lbl.startsWith('Other '),
55+
}), {})
56+
)
4357
4458
function setSource(src) {
4559
activeSource.value = src

site/src/assets/styles.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,30 @@ body,
254254
cursor: pointer;
255255
}
256256

257+
.filter-actions {
258+
display: flex;
259+
gap: 6px;
260+
margin-bottom: 6px;
261+
}
262+
263+
.filter-action-btn {
264+
padding: 2px 8px;
265+
border: 1px solid #ccc;
266+
border-radius: 3px;
267+
background: #f5f5f5;
268+
font-size: 11px;
269+
cursor: pointer;
270+
}
271+
272+
.filter-action-btn:hover {
273+
background: #e5e5e5;
274+
}
275+
276+
.conflated-filter-list {
277+
max-height: 240px;
278+
overflow-y: auto;
279+
}
280+
257281
/* POI popup */
258282
.poi-popup {
259283
background: #fff;

site/src/components/AmenityFilter.vue

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,22 @@
2020
Overture Maps tiles use granular subcategories. Filtering will be available after the taxonomy stabilizes in June 2026.
2121
</p>
2222
</template>
23+
<template v-else-if="activeSource === 'conflated'">
24+
<div class="filter-actions">
25+
<button class="filter-action-btn" @click="selectAllConflated">All</button>
26+
<button class="filter-action-btn" @click="selectNoneConflated">None</button>
27+
</div>
28+
<div class="conflated-filter-list">
29+
<label v-for="lbl in conflatedLabels" :key="lbl">
30+
<input
31+
type="checkbox"
32+
:checked="conflatedFilters[lbl]"
33+
@change="toggleConflated(lbl)"
34+
/>
35+
{{ lbl }}
36+
</label>
37+
</div>
38+
</template>
2339

2440
</div>
2541
</div>
@@ -33,9 +49,15 @@ const props = defineProps({
3349
activeSource: { type: String, required: true },
3450
osmFilters: { type: Object, required: true },
3551
overtureFilters: { type: Object, required: true },
52+
conflatedFilters: { type: Object, required: true },
53+
conflatedLabels: { type: Array, required: true },
3654
})
3755
38-
const emit = defineEmits(['update:osm-filters', 'update:overture-filters'])
56+
const emit = defineEmits([
57+
'update:osm-filters',
58+
'update:overture-filters',
59+
'update:conflated-filters',
60+
])
3961
const collapsed = ref(false)
4062
const osmFilterKeys = OSM_FILTER_KEYS
4163
const overtureCategories = OVERTURE_CATEGORIES
@@ -51,5 +73,23 @@ function toggleOverture(key) {
5173
})
5274
}
5375
76+
function toggleConflated(label) {
77+
emit('update:conflated-filters', {
78+
...props.conflatedFilters,
79+
[label]: !props.conflatedFilters[label],
80+
})
81+
}
82+
83+
function selectAllConflated() {
84+
const all = {}
85+
for (const lbl of props.conflatedLabels) all[lbl] = true
86+
emit('update:conflated-filters', all)
87+
}
88+
89+
function selectNoneConflated() {
90+
const none = {}
91+
for (const lbl of props.conflatedLabels) none[lbl] = false
92+
emit('update:conflated-filters', none)
93+
}
5494
5595
</script>

site/src/components/MapContainer.vue

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
</button>
1717

1818
<ConfidenceLegend
19-
v-if="props.activeSource === 'osm' || props.activeSource === 'overture'"
19+
v-if="props.activeSource === 'osm' || props.activeSource === 'overture' || props.activeSource === 'conflated'"
2020
/>
2121

2222
<div class="basemap-switcher">
@@ -55,6 +55,7 @@ import { useDuckDB } from '../composables/useDuckDB.js'
5555
import { useGeolocation } from '../composables/useGeolocation.js'
5656
import { createQueryDebouncer } from '../queries/queryDebouncer.js'
5757
import { buildOsmQuery } from '../queries/osmQuery.js'
58+
import { buildConflatedQuery } from '../queries/conflatedQuery.js'
5859
import {
5960
getOsmLayer,
6061
updateOsmFeatures,
@@ -65,6 +66,11 @@ import {
6566
updateOvertureFilters,
6667
wrapOvertureFeature,
6768
} from '../layers/overtureLayer.js'
69+
import {
70+
getConflatedLayer,
71+
updateConflatedFeatures,
72+
updateConflatedClusterMode,
73+
} from '../layers/conflatedLayer.js'
6874
import { arrowToFeatures, extentToLonLatBbox } from '../utils.js'
6975
import {
7076
BASE_MAP_STYLES,
@@ -77,7 +83,7 @@ const props = defineProps({
7783
activeSource: { type: String, required: true },
7884
osmFilters: { type: Object, required: true },
7985
overtureFilters: { type: Object, required: true },
80-
86+
conflatedFilters: { type: Object, required: true },
8187
})
8288
8389
const emit = defineEmits(['zoom-changed'])
@@ -100,7 +106,7 @@ let geoOverlay = null
100106
101107
// Helper: get all data layers
102108
function getDataLayers() {
103-
return [getOsmLayer(), getOvertureLayer()]
109+
return [getOsmLayer(), getOvertureLayer(), getConflatedLayer()]
104110
}
105111
106112
onMounted(async () => {
@@ -120,13 +126,15 @@ onMounted(async () => {
120126
121127
const osmLyr = getOsmLayer()
122128
const overtureLyr = getOvertureLayer()
129+
const conflatedLyr = getConflatedLayer()
123130
osmLyr.setVisible(true)
124131
overtureLyr.setVisible(false)
132+
conflatedLyr.setVisible(false)
125133
126134
const olMap = new Map({
127135
target: mapEl.value,
128136
view,
129-
layers: [fallbackBase, osmLyr, overtureLyr],
137+
layers: [fallbackBase, osmLyr, overtureLyr, conflatedLyr],
130138
})
131139
map.value = olMap
132140
@@ -155,6 +163,7 @@ onMounted(async () => {
155163
const z = view.getZoom()
156164
currentZoom.value = z
157165
updateClusterMode(z)
166+
updateConflatedClusterMode(z)
158167
loadData()
159168
})
160169
@@ -214,15 +223,19 @@ function switchBaseMap() {
214223
// ---- Data loading ----
215224
216225
async function loadData() {
217-
if (props.activeSource !== 'osm') return
226+
if (props.activeSource !== 'osm' && props.activeSource !== 'conflated') return
218227
if (currentZoom.value < MIN_ZOOM_FOR_DATA) return
219228
if (!duckReady.value) return
220229
221230
try {
222231
await debouncer.schedule(async () => {
223232
loading.value = true
224233
try {
225-
await loadOsmData()
234+
if (props.activeSource === 'osm') {
235+
await loadOsmData()
236+
} else if (props.activeSource === 'conflated') {
237+
await loadConflatedData()
238+
}
226239
} finally {
227240
loading.value = false
228241
}
@@ -258,6 +271,27 @@ async function loadOsmData() {
258271
updateOsmFeatures(features)
259272
}
260273
274+
async function loadConflatedData() {
275+
const extent = map.value.getView().calculateExtent()
276+
const bbox = extentToLonLatBbox(extent)
277+
278+
const enabledLabels = Object.entries(props.conflatedFilters)
279+
.filter(([, v]) => v)
280+
.map(([k]) => k)
281+
282+
if (enabledLabels.length === 0) {
283+
updateConflatedFeatures([])
284+
return
285+
}
286+
287+
const sql = buildConflatedQuery(bbox, enabledLabels)
288+
if (!sql) return
289+
290+
const result = await runQuery(sql)
291+
const features = arrowToFeatures(result)
292+
updateConflatedFeatures(features)
293+
}
294+
261295
// ---- Interaction ----
262296
263297
function handleClick(evt) {
@@ -271,7 +305,7 @@ function handleClick(evt) {
271305
return true
272306
}
273307
274-
// OSM VectorLayer (with Cluster)
308+
// OSM or Conflated VectorLayer (with Cluster)
275309
const subFeatures = feature.get('features')
276310
if (subFeatures && subFeatures.length > 1) {
277311
const geom = feature.getGeometry()
@@ -356,6 +390,7 @@ defineExpose({ flyToBbox })
356390
watch(() => props.activeSource, (src) => {
357391
getOsmLayer().setVisible(src === 'osm')
358392
getOvertureLayer().setVisible(src === 'overture')
393+
getConflatedLayer().setVisible(src === 'conflated')
359394
closePopup()
360395
loadData()
361396
})
@@ -372,5 +407,10 @@ watch(
372407
{ deep: true }
373408
)
374409
410+
watch(
411+
() => props.conflatedFilters,
412+
() => { if (props.activeSource === 'conflated') loadData() },
413+
{ deep: true }
414+
)
375415
376416
</script>

0 commit comments

Comments
 (0)