Skip to content

Commit

Permalink
feat: add uncluster plugin support #226
Browse files Browse the repository at this point in the history
  • Loading branch information
wazolab committed Aug 28, 2024
1 parent 478d0e3 commit 7e62ba6
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 201 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "maplibre-gl-teritorio-cluster"]
path = maplibre-gl-teritorio-cluster
url = https://github.com/teritorio/maplibre-gl-teritorio-cluster.git
29 changes: 4 additions & 25 deletions components/MainMap/MapFeatures.vue
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export default defineNuxtComponent({
data(): {
map: Map
markers: { [id: string]: Marker }
selectedFeatureMarker?: Marker
selectedBackground: MapStyleEnum
} {
return {
Expand All @@ -155,7 +154,7 @@ export default defineNuxtComponent({
return [MapStyleEnum.vector, MapStyleEnum.aerial, MapStyleEnum.bicycle]
},
// Workarround typing issue
// Workaround typing issue
mapTyped(): Map {
return this.map as Map
},
Expand Down Expand Up @@ -280,22 +279,17 @@ export default defineNuxtComponent({
)
if (selectedFeatures.length > 0) {
// Set temp partial data from vector tiles. Then fetch full data
this.updateSelectedFeature(
vectorTilesPoi2ApiPoi(selectedFeatures[0]),
undefined,
true,
)
this.updateSelectedFeature(vectorTilesPoi2ApiPoi(selectedFeatures[0]), true)
this.showSelectedFeature()
}
else {
this.updateSelectedFeature(null, undefined)
this.updateSelectedFeature(null)
}
},
updateSelectedFeature(feature: ApiPoi | null, marker?: Marker, fetch = false) {
updateSelectedFeature(feature: ApiPoi | null, fetch = false) {
if (this.selectedFeature !== feature) {
this.mapStore.setSelectedFeature(feature)
this.setSelectedFeatureMarker(marker)
if (feature && fetch && feature.properties.metadata.id) {
try {
Expand All @@ -318,14 +312,6 @@ export default defineNuxtComponent({
}
},
// Map view
onMapRender() {
if (this.mapStyleLoaded && this.selectedFeature) {
const marker = createMarker((this.selectedFeature.geometry as GeoJSON.Point).coordinates as [number, number])
this.setSelectedFeatureMarker(marker)
}
},
goTo(feature: ApiPoi) {
if (!feature || !('coordinates' in feature.geometry))
return
Expand Down Expand Up @@ -437,11 +423,6 @@ export default defineNuxtComponent({
filterRouteByCategories(this.map as Map, this.selectedCategoriesIds)
}
},
setSelectedFeatureMarker(marker?: Marker) {
this.selectedFeatureMarker?.remove()
this.selectedFeatureMarker = marker?.addTo(this.map as Map)
},
},
})
</script>
Expand All @@ -454,8 +435,6 @@ export default defineNuxtComponent({
:map-style="selectedBackground" :rotate="!device.touch" :show-attribution="!small"
:off-map-attribution="device.smallScreen && !small" :hide-control="small" :style-icon-filter="styleIconFilter"
:cooperative-gestures="cooperativeGestures" :boundary-area="boundaryArea" hash="map" @map-init="onMapInit"
@map-data="onMapRender" @map-drag-end="onMapRender" @map-move-end="onMapRender" @map-resize="onMapRender"
@map-rotate-end="onMapRender" @map-touch-move="onMapRender" @map-zoom-end="onMapRender"
@map-style-load="onMapStyleLoad" @feature-click="updateSelectedFeature"
>
<template #controls>
Expand Down
67 changes: 31 additions & 36 deletions components/Map/MapBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ import type {
} from 'maplibre-gl'
import type { PropType } from 'vue'
import { UnCluster } from '~/maplibre-gl-teritorio-cluster/src/index'
import { defineNuxtComponent } from '#app'
import Attribution from '~/components/Map/Attribution.vue'
import Map from '~/components/Map/Map.vue'
import type { ApiPoi } from '~/lib/apiPois'
import { MAP_ZOOM } from '~/lib/constants'
import type { MapPoi } from '~/lib/mapPois'
import { markerLayerTextFactory, updateMarkers } from '~/lib/markerLayerFactory'
import { markerLayerTextFactory } from '~/lib/markerLayerFactory'
import { displayCluster, displayMarker, displayPinMarker } from '~/lib/clusters'
import type { MapStyleEnum } from '~/utils/types'
const POI_SOURCE = 'poi'
Expand Down Expand Up @@ -109,13 +111,15 @@ export default defineNuxtComponent({
poiLayerTemplate: LayerSpecification | undefined
markers: { [id: string]: Marker }
fullAttribution: string
uncluster: UnCluster | null
} {
return {
map: null!,
poiFilter: null,
poiLayerTemplate: undefined,
markers: {},
fullAttribution: '',
uncluster: null,
}
},
Expand Down Expand Up @@ -165,7 +169,7 @@ export default defineNuxtComponent({
object,
) => true,
mapStyleLoad: (_style: StyleSpecification) => true,
featureClick: (_feature: ApiPoi, _marker?: Marker) => true,
featureClick: (_feature: ApiPoi) => true,
},
methods: {
Expand Down Expand Up @@ -244,7 +248,7 @@ export default defineNuxtComponent({
cluster: cluster === undefined ? true : cluster,
clusterRadius: 32,
clusterProperties: clusterProps,
clusterMaxZoom: 15,
clusterMaxZoom: 22,
tolerance: 0.6,
data: {
type: 'FeatureCollection',
Expand All @@ -266,6 +270,19 @@ export default defineNuxtComponent({
onMapInit(map: MapGL) {
this.map = map
this.uncluster = new UnCluster(
map,
POI_SOURCE,
{
clusterMode: displayCluster,
markerMode: displayMarker,
markerSize: 32,
unClusterMode: 'circle',
// Maybe maplibre-gl mismatch version
pinMarkerMode: displayPinMarker,
},
)
this.$emit('mapInit', map)
},
Expand Down Expand Up @@ -361,13 +378,8 @@ export default defineNuxtComponent({
&& this.map.getSource(POI_SOURCE)
&& this.map.isSourceLoaded(POI_SOURCE)
) {
this.markers = updateMarkers(
this.map as MapGL,
this.markers,
POI_SOURCE,
this.fitBounds,
(feature: ApiPoi, marker?: Marker) => this.$emit('featureClick', feature, marker),
)
this.uncluster?.render()
this.uncluster?.addEventListener('teritorioClick', (e: Event) => this.$emit('featureClick', (e as CustomEvent).detail.selectedFeature))
}
// @ts-expect-error: eventName is not in events definition
Expand All @@ -380,28 +392,14 @@ export default defineNuxtComponent({
<template>
<div id="map-container" class="tw-w-full tw-h-full tw-flex tw-flex-col">
<Map
:center="center"
:bounds="bounds"
:fit-bounds-options="fitBoundsOptions()"
:zoom="selectionZoom.poi"
:fullscreen-control="fullscreenControl"
:extra-attributions="extraAttributions"
:map-style="mapStyle"
:rotate="rotate"
:show-attribution="showAttribution && !offMapAttribution"
:hide-control="hideControl"
:hash="hash"
:cooperative-gestures="cooperativeGestures"
class="tw-grow tw-h-full"
@map-init="onMapInit($event)"
@map-data="onMapRender('mapData', $event)"
@map-drag-end="onMapRender('mapDragEnd', $event)"
@map-move-end="onMapRender('mapMoveEnd', $event)"
@map-resize="onMapRender('mapResize', $event)"
@map-rotate-end="onMapRender('mapRotateEnd', $event)"
@map-touch-move="onMapRender('mapTouchMove', $event)"
@map-zoom-end="onMapRender('mapZoomEnd', $event)"
@map-style-load="onMapStyleLoad($event)"
:center="center" :bounds="bounds" :fit-bounds-options="fitBoundsOptions()" :zoom="selectionZoom.poi"
:fullscreen-control="fullscreenControl" :extra-attributions="extraAttributions" :map-style="mapStyle"
:rotate="rotate" :show-attribution="showAttribution && !offMapAttribution" :hide-control="hideControl"
:hash="hash" :cooperative-gestures="cooperativeGestures" class="tw-grow tw-h-full" @map-init="onMapInit($event)"
@map-data="onMapRender('mapData', $event)" @map-drag-end="onMapRender('mapDragEnd', $event)"
@map-move-end="onMapRender('mapMoveEnd', $event)" @map-resize="onMapRender('mapResize', $event)"
@map-rotate-end="onMapRender('mapRotateEnd', $event)" @map-touch-move="onMapRender('mapTouchMove', $event)"
@map-zoom-end="onMapRender('mapZoomEnd', $event)" @map-style-load="onMapStyleLoad($event)"
@full-attribution="fullAttribution = $event"
>
<template #controls>
Expand All @@ -411,10 +409,7 @@ export default defineNuxtComponent({
<slot name="body" />
</template>
</Map>
<Attribution
v-if="showAttribution && offMapAttribution"
:attribution="fullAttribution"
/>
<Attribution v-if="showAttribution && offMapAttribution" :attribution="fullAttribution" />
</div>
</template>

Expand Down
72 changes: 53 additions & 19 deletions lib/clusters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import type { LngLatLike, MapGeoJSONFeature, Point } from 'maplibre-gl'
import { Marker } from 'maplibre-gl'
import { createApp } from 'vue'
import TeritorioIconBadge from '~/components/UI/TeritorioIconBadge.vue'

function getMarkerDonutSegment(start: number, end: number, r: number, r0: number, colorFill: string): string {
if (end - start === 1)
end -= 0.00001
Expand Down Expand Up @@ -39,45 +44,74 @@ function getMarkerDonutSegment(start: number, end: number, r: number, r0: number
].join(' ')
}

export function createMarkerDonutChart(countPerColor: Record<string, number>, totalCount: number): HTMLElement {
const r
= totalCount >= 1000
? 40
: totalCount >= 100
? 32
: totalCount >= 10
? 24
: 16
export function displayCluster(element: HTMLDivElement, props: MapGeoJSONFeature['properties']) {
const {
cluster: _a,
cluster_id: _b,
point_count,
_c: _d,
point_count_abbreviated: _e,
...countPercolor
} = props

const r = point_count >= 1000
? 40
: point_count >= 100
? 32
: point_count >= 10
? 24
: 16
const r0 = r - 5
const w = r * 2

let html = `<svg width="${w}" height="${w}" viewbox="0 0 ${w} ${w}" text-anchor="middle" class="cluster-donut">`

let total = 0
let offsets = 0
Object.entries(countPerColor).forEach(([color, count]) => {
Object.entries(countPercolor).forEach(([color, count]) => {
total += count
html += getMarkerDonutSegment(
offsets / totalCount,
total / totalCount,
offsets / point_count,
total / point_count,
r,
r0,
color,
)
offsets = total
})

if (total !== totalCount)
html += getMarkerDonutSegment(total / totalCount, 1, r, r0, '#ccc')
if (total !== point_count)
html += getMarkerDonutSegment(total / point_count, 1, r, r0, '#ccc')

html += `<circle cx="${r}" cy="${r}" r="${r0}" fill="white" />
<text dominant-baseline="central" transform="translate(${r}, ${r})">
${totalCount.toLocaleString()}
${point_count.toLocaleString()}
</text>
</svg>`

const el = document.createElement('div')
el.classList.add('cluster-item')
el.innerHTML = html
return el
element.classList.add('cluster-item')
element.innerHTML = html
}

export function displayMarker(element: HTMLDivElement, feature: MapGeoJSONFeature) {
if (typeof feature.properties?.metadata === 'string')
feature.properties.metadata = JSON.parse(feature.properties.metadata)

if (typeof feature.properties?.display === 'string')
feature.properties.display = JSON.parse(feature.properties?.display)

if (typeof feature.properties?.editorial === 'string')
feature.properties.editorial = JSON.parse(feature.properties?.editorial)

createApp(TeritorioIconBadge, {
colorFill: feature.properties.display?.color_fill,
picto: feature.properties.display?.icon,
image: feature.properties!['image:thumbnail'],
size: null,
text: feature.properties.display?.text,
}).mount(element)
}

export function displayPinMarker(coords: LngLatLike, offset: Point): Marker {
return new Marker({ scale: 1.3, color: '#f44336', anchor: 'bottom' }).setLngLat(coords).setOffset(offset)
}
Loading

0 comments on commit 7e62ba6

Please sign in to comment.