From 57509eda33530a843e7f0e0913014ca3113dc8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9=20Viricel?= Date: Wed, 20 Nov 2024 01:15:15 +0100 Subject: [PATCH] test: setSelectedFeature method #29 --- README.md | 12 +++---- src/teritorio-cluster.ts | 52 +++++++++++++++--------------- src/utils/helpers.ts | 12 +++---- tests/mocks/maplibre-gl.mock.ts | 9 ++++++ tests/set-selected-feature.test.ts | 49 ++++++++++++++++++++++++++++ 5 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 tests/set-selected-feature.test.ts diff --git a/README.md b/README.md index a1a4efd..b68331d 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,12 @@ Create a new Maplibre GL JS plugin for feature (cluster / individual marker) ren |-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|----------|-----------------------------------------------------| | clusterMaxZoom | `number` | Maximal zoom level at which we force the rendering of the Unfolded Cluster | ❌ | `17` | | clusterMinZoom | `number` | Minimal zoom level at which we force the rendering of the Unfolded Cluster | ❌ | `0` | -| clusterRenderFn | `(element: HTMLDivElement, props: MapGeoJSONFeature['properties']): void` | Cluster render function | ❌ | `src/utils/helpers.ts/clusterRenderDefault()` | +| clusterRenderFn | `(element: HTMLDivElement, props: NonNullable): void` | Cluster render function | ❌ | `src/utils/helpers.ts/clusterRenderDefault()` | | fitBoundsOptions | [`FitBoundsOptions`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FitBoundsOptions) | Options for [Map#fitBounds](https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/#fitbounds) method | ❌ | `{ padding: 20 }` | -| initialFeature | [`MapGeoJSONFeature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapGeoJSONFeature/) | Feature to select on initial rendering | ❌ | `undefined` | -| markerRenderFn | `(element: HTMLDivElement, feature: MapGeoJSONFeature, markerSize: number): void` | Individual Marker render function | ❌ | `src/utils/helpers.ts/markerRenderDefault()` | +| initialFeature | [`GeoJSON.Feature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/GeoJSON.Feature/) | Feature to select on initial rendering | ❌ | `undefined` | +| markerRenderFn | `(element: HTMLDivElement, feature: GeoJSON.Feature, markerSize: number): void` | Individual Marker render function | ❌ | `src/utils/helpers.ts/markerRenderDefault()` | | markerSize | `number` (in px) | Size of Marker | ❌ | `24` | -| unfoldedClusterRenderFn | `(parent: HTMLDivElement, items: MapGeoJSONFeature[], markerSize: number, renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, clickHandler: (e: Event, feature: MapGeoJSONFeature) => void) => void` | Unfolded Cluster render function | ❌ | `src/utils/helpers.ts/unfoldedClusterRenderSmart()` | +| unfoldedClusterRenderFn | `(parent: HTMLDivElement, items: GeoJSON.Feature[], markerSize: number, renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, clickHandler: (e: Event, feature: GeoJSON.Feature) => void) => void` | Unfolded Cluster render function | ❌ | `src/utils/helpers.ts/unfoldedClusterRenderSmart()` | | unfoldedClusterRenderSmart | Mix between Circular and HexaShape shape Unfolded Cluster render function | - | - | - | | unfoldedClusterRenderGrid | Grid shape Unfolded Cluster render function function | - | - | - | | unfoldedClusterRenderCircle | Circular shape Unfolded Cluster render function function | - | - | - | @@ -133,10 +133,10 @@ Create a new Maplibre GL JS plugin for feature (cluster / individual marker) ren #### Methods | Name | Type | Description | |----------------------|------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| -| addEventListener | ('feature-click', (e: Event) => void) | Listen to feature click and return a [`MapGeoJSONFeature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapGeoJSONFeature/) from `e.detail.selectedFeature` for external control. | +| addEventListener | ('feature-click', (e: Event) => void) | Listen to feature click and return a [`GeoJSON.Feature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/GeoJSON.Feature/) from `e.detail.selectedFeature` for external control. | | resetSelectedFeature | () => void | Remove selected feature and associated Pin Marker | | setBoundsOptions | (options: [`FitBoundsOptions`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FitBoundsOptions)) => void | Update Map's visible area | -| setSelectedFeature | (feature: [`MapGeoJSONFeature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapGeoJSONFeature/)) => void | Set selected feature and display Pin Marker on top of it | +| setSelectedFeature | (feature: [`GeoJSON.Feature`](https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/GeoJSON.Feature/)) => void | Set selected feature and display Pin Marker on top of it | ## Dev Install dependencies diff --git a/src/teritorio-cluster.ts b/src/teritorio-cluster.ts index 76fdcd5..d2997cc 100644 --- a/src/teritorio-cluster.ts +++ b/src/teritorio-cluster.ts @@ -1,4 +1,4 @@ -import type { FitBoundsOptions, GeoJSONSource, LngLatLike, MapGeoJSONFeature, MapSourceDataEvent } from 'maplibre-gl' +import type { FitBoundsOptions, GeoJSONSource, LngLatLike, MapSourceDataEvent } from 'maplibre-gl' import bbox from '@turf/bbox' import { featureCollection } from '@turf/helpers' import { Marker, Point } from 'maplibre-gl' @@ -13,22 +13,22 @@ import { getFeatureId } from './utils/get-feature-id' type UnfoldedCluster = ( ( parent: HTMLDivElement, - items: MapGeoJSONFeature[], + items: GeoJSON.Feature[], markerSize: number, - renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, - clickHandler: (e: Event, feature: MapGeoJSONFeature) => void + renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, + clickHandler: (e: Event, feature: GeoJSON.Feature) => void ) => void ) type ClusterRender = ( ( element: HTMLDivElement, - props: MapGeoJSONFeature['properties'] + props: NonNullable ) => void ) type MarkerRender = ( ( element: HTMLDivElement, - feature: MapGeoJSONFeature, + feature: GeoJSON.Feature, markerSize: number ) => void ) @@ -38,20 +38,20 @@ type PinMarkerRender = ( offset: Point ) => Marker ) -interface FeatureInClusterMatch { clusterId: string, feature: MapGeoJSONFeature } -type FeatureMatch = FeatureInClusterMatch | MapGeoJSONFeature +interface FeatureInClusterMatch { clusterId: string, feature: GeoJSON.Feature } +type FeatureMatch = FeatureInClusterMatch | GeoJSON.Feature const UnfoldedClusterClass = 'teritorio-unfolded-cluster' export class TeritorioCluster extends EventTarget { map: maplibregl.Map - clusterLeaves: Map + clusterLeaves: Map clusterMaxZoom: number clusterMinZoom: number clusterRender?: ClusterRender - featuresMap: Map + featuresMap: Map fitBoundsOptions: FitBoundsOptions - initialFeature?: MapGeoJSONFeature + initialFeature?: GeoJSON.Feature markerRender?: MarkerRender markerSize: number markersOnScreen: Map @@ -68,12 +68,12 @@ export class TeritorioCluster extends EventTarget { map: maplibregl.Map, sourceId: string, options?: { - clusterMaxZoom?: number - clusterMinZoom?: number - clusterRenderFn?: ClusterRender - fitBoundsOptions?: FitBoundsOptions - initialFeature?: MapGeoJSONFeature - markerRenderFn?: MarkerRender + clusterMaxZoom?: number, + clusterMinZoom?: number, + clusterRenderFn?: ClusterRender, + fitBoundsOptions?: FitBoundsOptions, + initialFeature?: GeoJSON.Feature, + markerRenderFn?: MarkerRender, markerSize?: number unfoldedClusterRenderFn?: UnfoldedCluster unfoldedClusterMaxLeaves?: number @@ -83,11 +83,11 @@ export class TeritorioCluster extends EventTarget { super() this.map = map - this.clusterLeaves = new Map() + this.clusterLeaves = new Map() this.clusterMaxZoom = options?.clusterMaxZoom || 17 this.clusterMinZoom = options?.clusterMinZoom || 0 this.clusterRender = options?.clusterRenderFn - this.featuresMap = new Map() + this.featuresMap = new Map() this.fitBoundsOptions = options?.fitBoundsOptions || { padding: 20 } this.initialFeature = options?.initialFeature this.markerRender = options?.markerRenderFn @@ -178,7 +178,7 @@ export class TeritorioCluster extends EventTarget { return new Point(x - clusterXCenter + (width / 2), y - clusterYCenter + (height / 2)) } - #featureClickHandler = (e: Event, feature: MapGeoJSONFeature): void => { + #featureClickHandler = (e: Event, feature: GeoJSON.Feature): void => { e.stopPropagation() if (!(e.currentTarget instanceof HTMLElement) || this.selectedFeatureId === getFeatureId(feature)) @@ -209,7 +209,7 @@ export class TeritorioCluster extends EventTarget { } } - #fitBoundsToClusterLeaves = (features: MapGeoJSONFeature[]): void => { + #fitBoundsToClusterLeaves = (features: GeoJSON.Feature[]): void => { const bounds = bbox(featureCollection(features)) this.map.fitBounds(bounds as [number, number, number, number], this.fitBoundsOptions) @@ -252,7 +252,7 @@ export class TeritorioCluster extends EventTarget { this.ticking = true } - #renderCluster = (id: string, props: MapGeoJSONFeature['properties']): HTMLDivElement => { + #renderCluster = (id: string, props: NonNullable): HTMLDivElement => { const element = document.createElement('div') element.id = id @@ -276,7 +276,7 @@ export class TeritorioCluster extends EventTarget { return element } - #renderMarker = (feature: MapGeoJSONFeature): HTMLDivElement => { + #renderMarker = (feature: GeoJSON.Feature): HTMLDivElement => { const element = document.createElement('div') element.id = getFeatureId(feature) @@ -295,7 +295,7 @@ export class TeritorioCluster extends EventTarget { this.pinMarker.addTo(this.map) } - #renderUnfoldedCluster = (id: string, leaves: MapGeoJSONFeature[]): HTMLDivElement => { + #renderUnfoldedCluster = (id: string, leaves: GeoJSON.Feature[]): HTMLDivElement => { const element = document.createElement('div') element.id = id element.classList.add(UnfoldedClusterClass) @@ -347,7 +347,7 @@ export class TeritorioCluster extends EventTarget { // Get cluster's leaves if (feature.properties.cluster) { const source = this.map.getSource(this.sourceId) as GeoJSONSource - const leaves = await source.getClusterLeaves(Number.parseInt(id), feature.properties.point_count, 0) as MapGeoJSONFeature[] + const leaves = await source.getClusterLeaves(Number.parseInt(id), feature.properties.point_count, 0) as GeoJSON.Feature[] this.clusterLeaves.set(id, leaves) } } @@ -364,7 +364,7 @@ export class TeritorioCluster extends EventTarget { let marker = this.markersOnScreen.get(id) const props = feature.properties - if (props.cluster) { + if (props?.cluster) { const leaves = this.clusterLeaves.get(id) if (!leaves) { diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 5e55b93..1b6aa57 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,4 +1,4 @@ -import type { LngLatLike, MapGeoJSONFeature } from 'maplibre-gl' +import type { LngLatLike } from 'maplibre-gl' import { Marker, Point } from 'maplibre-gl' // Helper to apply styles on DOM element @@ -10,7 +10,7 @@ export function buildCss(htmlEl: HTMLElement, styles: { [key: string]: string }) } // Circle shape -export function unfoldedClusterRenderCircle(parent: HTMLDivElement, items: MapGeoJSONFeature[], markerSize: number, renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, clickHandler: (e: Event, feature: MapGeoJSONFeature) => void): void { +export function unfoldedClusterRenderCircle(parent: HTMLDivElement, items: GeoJSON.Feature[], markerSize: number, renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, clickHandler: (e: Event, feature: GeoJSON.Feature) => void): void { const radius = (markerSize / 2) / Math.sin(Math.PI / items.length) const angle = 360 / items.length let rot = 0 @@ -39,7 +39,7 @@ export function unfoldedClusterRenderCircle(parent: HTMLDivElement, items: MapGe } // HexaGrid shape -export function unfoldedClusterRenderHexaGrid(parent: HTMLDivElement, items: MapGeoJSONFeature[], markerSize: number, renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, clickHandler: (e: Event, feature: MapGeoJSONFeature) => void): void { +export function unfoldedClusterRenderHexaGrid(parent: HTMLDivElement, items: GeoJSON.Feature[], markerSize: number, renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, clickHandler: (e: Event, feature: GeoJSON.Feature) => void): void { const radius = (markerSize / 2) / Math.sin(Math.PI / items.length) buildCss(parent, { @@ -84,7 +84,7 @@ export function unfoldedClusterRenderHexaGrid(parent: HTMLDivElement, items: Map } // Smart: mix between Circle and HexaGrid shape -export function unfoldedClusterRenderSmart(parent: HTMLDivElement, items: MapGeoJSONFeature[], markerSize: number, renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, clickHandler: (e: Event, feature: MapGeoJSONFeature) => void): void { +export function unfoldedClusterRenderSmart(parent: HTMLDivElement, items: GeoJSON.Feature[], markerSize: number, renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, clickHandler: (e: Event, feature: GeoJSON.Feature) => void): void { if (items.length <= 5) { unfoldedClusterRenderCircle(parent, items, markerSize, renderMarker, clickHandler) } @@ -94,7 +94,7 @@ export function unfoldedClusterRenderSmart(parent: HTMLDivElement, items: MapGeo } // Grid shape -export function unfoldedClusterRenderGrid(parent: HTMLDivElement, items: MapGeoJSONFeature[], _markerSize: number, renderMarker: (feature: MapGeoJSONFeature) => HTMLDivElement, clickHandler: (e: Event, feature: MapGeoJSONFeature) => void): void { +export function unfoldedClusterRenderGrid(parent: HTMLDivElement, items: GeoJSON.Feature[], _markerSize: number, renderMarker: (feature: GeoJSON.Feature) => HTMLDivElement, clickHandler: (e: Event, feature: GeoJSON.Feature) => void): void { buildCss(parent, { 'display': 'flex', 'gap': '2px', @@ -113,7 +113,7 @@ export function unfoldedClusterRenderGrid(parent: HTMLDivElement, items: MapGeoJ } // Cluster default styles -export function clusterRenderDefault(element: HTMLDivElement, props: MapGeoJSONFeature['properties']): void { +export function clusterRenderDefault(element: HTMLDivElement, props: NonNullable): void { element.innerHTML = props.point_count buildCss(element, { diff --git a/tests/mocks/maplibre-gl.mock.ts b/tests/mocks/maplibre-gl.mock.ts index 0cb66d2..1a57424 100644 --- a/tests/mocks/maplibre-gl.mock.ts +++ b/tests/mocks/maplibre-gl.mock.ts @@ -10,5 +10,14 @@ vi.mock('maplibre-gl', () => { }), fitBounds: vi.fn(), })), + Marker: vi.fn().mockImplementation(() => ({ + setLngLat: vi.fn().mockReturnThis(), + addTo: vi.fn().mockReturnThis(), + setOffset: vi.fn().mockReturnThis(), + })), + Point: vi.fn().mockImplementation((x, y) => ({ + x, + y, + })), } }) diff --git a/tests/set-selected-feature.test.ts b/tests/set-selected-feature.test.ts new file mode 100644 index 0000000..3f3a493 --- /dev/null +++ b/tests/set-selected-feature.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { TeritorioCluster } from '../src/teritorio-cluster' +import { Map as MapGL } from 'maplibre-gl' +import { getFeatureId } from '../src/utils/get-feature-id'; + +vi.mock('../src/utils/get-feature-id', () => ({ + getFeatureId: vi.fn(), +})); + +describe('setSelectedFeature', () => { + let map: MapGL + let teritorioCluster: TeritorioCluster + + beforeEach(() => { + map = new MapGL({ container: 'map'}) + teritorioCluster = new TeritorioCluster(map, 'sourceId') + }) + + it('should render pin marker when feature is not found and is a Point', () => { + const feature = { + type: 'Feature', + geometry: { type: 'Point', coordinates: [10, 20] }, + properties: null + } satisfies GeoJSON.Feature + + vi.mocked(getFeatureId).mockReturnValue('some-unique-id') + + teritorioCluster.setSelectedFeature(feature); + + expect(teritorioCluster.selectedFeatureId).toBe('some-unique-id'); + expect(teritorioCluster.pinMarker?.setLngLat).toHaveBeenCalledWith([10, 20]); + expect(teritorioCluster.pinMarker?.addTo).toHaveBeenCalledWith(map); + }); + + it('should log an error if feature is not a Point and not found', () => { + const feature = { + type: 'Feature', + geometry: { type: 'Polygon', coordinates: [[[]]] }, + properties: null + } satisfies GeoJSON.Feature + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + vi.mocked(getFeatureId).mockReturnValue('some-unique-id') + + teritorioCluster.setSelectedFeature(feature); + + expect(consoleErrorSpy).toHaveBeenCalledWith('Feature some-unique-id is not of type \'Point\', and is not supported.'); + }); +}) \ No newline at end of file