diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index 7bfd532550e..308613a9988 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -3,30 +3,31 @@ export type Point = { lat: number; lng: number; }; -export type MarkerDefinition = { - '@id': string; +export type Identifier = string; +export type WithIdentifier> = T & { + '@id': Identifier; +}; +export type MarkerDefinition = WithIdentifier<{ position: Point; title: string | null; - infoWindow?: Omit, 'position'>; + infoWindow?: InfoWindowWithoutPositionDefinition; rawOptions?: MarkerOptions; extra: Record; -}; -export type PolygonDefinition = { - '@id': string; - infoWindow?: Omit, 'position'>; +}>; +export type PolygonDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; points: Array; title: string | null; rawOptions?: PolygonOptions; extra: Record; -}; -export type PolylineDefinition = { - '@id': string; - infoWindow?: Omit, 'position'>; +}>; +export type PolylineDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; points: Array; title: string | null; rawOptions?: PolylineOptions; extra: Record; -}; +}>; export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -36,6 +37,7 @@ export type InfoWindowDefinition = { rawOptions?: InfoWindowOptions; extra: Record; }; +export type InfoWindowWithoutPositionDefinition = Omit, 'position'>; export default abstract class extends Controller { static values: { providerOptions: ObjectConstructor; @@ -55,50 +57,47 @@ export default abstract class>; optionsValue: MapOptions; protected map: Map; - protected markers: globalThis.Map; + protected markers: globalThis.Map; + protected polygons: globalThis.Map; + protected polylines: globalThis.Map; protected infoWindows: Array; - protected polygons: globalThis.Map; - protected polylines: globalThis.Map; + private isConnected; + private createMarker; + private createPolygon; + private createPolyline; + protected abstract dispatchEvent(name: string, payload: Record): void; connect(): void; + createInfoWindow({ definition, element, }: { + definition: InfoWindowWithoutPositionDefinition; + element: Marker | Polygon | Polyline; + }): InfoWindow; + abstract centerValueChanged(): void; + abstract zoomValueChanged(): void; + markersValueChanged(): void; + polygonsValueChanged(): void; + polylinesValueChanged(): void; protected abstract doCreateMap({ center, zoom, options, }: { center: Point | null; zoom: number | null; options: MapOptions; }): Map; - createMarker(definition: MarkerDefinition): Marker; - protected abstract removeMarker(marker: Marker): void; - protected abstract doCreateMarker(definition: MarkerDefinition): Marker; - createPolygon(definition: PolygonDefinition): Polygon; - protected abstract removePolygon(polygon: Polygon): void; - protected abstract doCreatePolygon(definition: PolygonDefinition): Polygon; - createPolyline(definition: PolylineDefinition): Polyline; - protected abstract removePolyline(polyline: Polyline): void; - protected abstract doCreatePolyline(definition: PolylineDefinition): Polyline; - protected createInfoWindow({ definition, element, }: { - definition: MarkerDefinition['infoWindow']; - element: Marker; - } | { - definition: PolygonDefinition['infoWindow']; - element: Polygon; - } | { - definition: PolylineDefinition['infoWindow']; - element: Polyline; - }): InfoWindow; + protected abstract doFitBoundsToMarkers(): void; + protected abstract doCreateMarker({ definition, }: { + definition: MarkerDefinition; + }): Marker; + protected abstract doRemoveMarker(marker: Marker): void; + protected abstract doCreatePolygon({ definition, }: { + definition: PolygonDefinition; + }): Polygon; + protected abstract doRemovePolygon(polygon: Polygon): void; + protected abstract doCreatePolyline({ definition, }: { + definition: PolylineDefinition; + }): Polyline; + protected abstract doRemovePolyline(polyline: Polyline): void; protected abstract doCreateInfoWindow({ definition, element, }: { - definition: MarkerDefinition['infoWindow']; - element: Marker; - } | { - definition: PolygonDefinition['infoWindow']; - element: Polygon; - } | { - definition: PolylineDefinition['infoWindow']; - element: Polyline; + definition: InfoWindowWithoutPositionDefinition; + element: Marker | Polygon | Polyline; }): InfoWindow; - protected abstract doFitBoundsToMarkers(): void; - protected abstract dispatchEvent(name: string, payload: Record): void; - abstract centerValueChanged(): void; - abstract zoomValueChanged(): void; - markersValueChanged(): void; - polygonsValueChanged(): void; - polylinesValueChanged(): void; + private createDrawingFactory; + private onDrawChanged; } diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js index 9381817e978..60f5cb45235 100644 --- a/src/Map/assets/dist/abstract_map_controller.js +++ b/src/Map/assets/dist/abstract_map_controller.js @@ -4,17 +4,21 @@ class default_1 extends Controller { constructor() { super(...arguments); this.markers = new Map(); - this.infoWindows = []; this.polygons = new Map(); this.polylines = new Map(); + this.infoWindows = []; + this.isConnected = false; } connect() { const options = this.optionsValue; this.dispatchEvent('pre-connect', { options }); + this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this)); + this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this)); + this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this)); this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); - this.markersValue.forEach((marker) => this.createMarker(marker)); - this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); - this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); + this.markersValue.forEach((definition) => this.createMarker({ definition })); + this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); + this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -25,30 +29,7 @@ class default_1 extends Controller { polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); - } - createMarker(definition) { - this.dispatchEvent('marker:before-create', { definition }); - const marker = this.doCreateMarker(definition); - this.dispatchEvent('marker:after-create', { marker }); - marker['@id'] = definition['@id']; - this.markers.set(definition['@id'], marker); - return marker; - } - createPolygon(definition) { - this.dispatchEvent('polygon:before-create', { definition }); - const polygon = this.doCreatePolygon(definition); - this.dispatchEvent('polygon:after-create', { polygon }); - polygon['@id'] = definition['@id']; - this.polygons.set(definition['@id'], polygon); - return polygon; - } - createPolyline(definition) { - this.dispatchEvent('polyline:before-create', { definition }); - const polyline = this.doCreatePolyline(definition); - this.dispatchEvent('polyline:after-create', { polyline }); - polyline['@id'] = definition['@id']; - this.polylines.set(definition['@id'], polyline); - return polyline; + this.isConnected = true; } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); @@ -58,53 +39,50 @@ class default_1 extends Controller { return infoWindow; } markersValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.markers.forEach((marker) => { - if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) { - this.removeMarker(marker); - this.markers.delete(marker['@id']); - } - }); - this.markersValue.forEach((marker) => { - if (!this.markers.has(marker['@id'])) { - this.createMarker(marker); - } - }); + this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } } polygonsValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polygons.forEach((polygon) => { - if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - this.removePolygon(polygon); - this.polygons.delete(polygon['@id']); - } - }); - this.polygonsValue.forEach((polygon) => { - if (!this.polygons.has(polygon['@id'])) { - this.createPolygon(polygon); - } - }); + this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon); } polylinesValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polylines.forEach((polyline) => { - if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { - this.removePolyline(polyline); - this.polylines.delete(polyline['@id']); - } + this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); + } + createDrawingFactory(type, draws, factory) { + const eventBefore = `${type}:before-create`; + const eventAfter = `${type}:after-create`; + return ({ definition }) => { + this.dispatchEvent(eventBefore, { definition }); + const drawing = factory({ definition }); + this.dispatchEvent(eventAfter, { [type]: drawing }); + draws.set(definition['@id'], drawing); + return drawing; + }; + } + onDrawChanged(draws, newDrawDefinitions, factory, remover) { + const idsToRemove = new Set(draws.keys()); + newDrawDefinitions.forEach((definition) => { + idsToRemove.delete(definition['@id']); + }); + idsToRemove.forEach((id) => { + const draw = draws.get(id); + remover(draw); + draws.delete(id); }); - this.polylinesValue.forEach((polyline) => { - if (!this.polylines.has(polyline['@id'])) { - this.createPolyline(polyline); + newDrawDefinitions.forEach((definition) => { + if (!draws.has(definition['@id'])) { + factory({ definition }); } }); } diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index a1f9de68810..e9e7797ad24 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -2,11 +2,13 @@ import { Controller } from '@hotwired/stimulus'; export type Point = { lat: number; lng: number }; -export type MarkerDefinition = { - '@id': string; +export type Identifier = string; +export type WithIdentifier> = T & { '@id': Identifier }; + +export type MarkerDefinition = WithIdentifier<{ position: Point; title: string | null; - infoWindow?: Omit, 'position'>; + infoWindow?: InfoWindowWithoutPositionDefinition; /** * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet). */ @@ -18,25 +20,41 @@ export type MarkerDefinition = { * - `ux:map:marker:after-create` */ extra: Record; -}; +}>; -export type PolygonDefinition = { - '@id': string; - infoWindow?: Omit, 'position'>; +export type PolygonDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; points: Array; title: string | null; + /** + * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet). + */ rawOptions?: PolygonOptions; + /** + * Extra data defined by the developer. + * They are not directly used by the Stimulus controller, but they can be used by the developer with event listeners: + * - `ux:map:polygon:before-create` + * - `ux:map:polygon:after-create` + */ extra: Record; -}; +}>; -export type PolylineDefinition = { - '@id': string; - infoWindow?: Omit, 'position'>; +export type PolylineDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; points: Array; title: string | null; + /** + * Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet). + */ rawOptions?: PolylineOptions; + /** + * Extra data defined by the developer. + * They are not directly used by the Stimulus controller, but they can be used by the developer with event listeners: + * - `ux:map:polyline:before-create` + * - `ux:map:polyline:after-create` + */ extra: Record; -}; +}>; export type InfoWindowDefinition = { headerContent: string | null; @@ -58,6 +76,11 @@ export type InfoWindowDefinition = { extra: Record; }; +export type InfoWindowWithoutPositionDefinition = Omit< + InfoWindowDefinition, + 'position' +>; + export default abstract class< MapOptions, Map, @@ -90,23 +113,37 @@ export default abstract class< declare optionsValue: MapOptions; protected map: Map; - protected markers = new Map(); + protected markers = new Map(); + protected polygons = new Map(); + protected polylines = new Map(); protected infoWindows: Array = []; - protected polygons = new Map(); - protected polylines = new Map(); + + private isConnected = false; + private createMarker: ({ + definition, + }: { definition: MarkerDefinition }) => Marker; + private createPolygon: ({ + definition, + }: { definition: PolygonDefinition }) => Polygon; + private createPolyline: ({ + definition, + }: { definition: PolylineDefinition }) => Polyline; + + protected abstract dispatchEvent(name: string, payload: Record): void; connect() { const options = this.optionsValue; this.dispatchEvent('pre-connect', { options }); - this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); - - this.markersValue.forEach((marker) => this.createMarker(marker)); - - this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); + this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this)); + this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this)); + this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this)); - this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); + this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); + this.markersValue.forEach((definition) => this.createMarker({ definition })); + this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); + this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); @@ -119,76 +156,18 @@ export default abstract class< polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); - } - - protected abstract doCreateMap({ - center, - zoom, - options, - }: { - center: Point | null; - zoom: number | null; - options: MapOptions; - }): Map; - - public createMarker(definition: MarkerDefinition): Marker { - this.dispatchEvent('marker:before-create', { definition }); - const marker = this.doCreateMarker(definition); - this.dispatchEvent('marker:after-create', { marker }); - - marker['@id'] = definition['@id']; - - this.markers.set(definition['@id'], marker); - - return marker; - } - - protected abstract removeMarker(marker: Marker): void; - - protected abstract doCreateMarker(definition: MarkerDefinition): Marker; - - public createPolygon(definition: PolygonDefinition): Polygon { - this.dispatchEvent('polygon:before-create', { definition }); - const polygon = this.doCreatePolygon(definition); - this.dispatchEvent('polygon:after-create', { polygon }); - - polygon['@id'] = definition['@id']; - - this.polygons.set(definition['@id'], polygon); - - return polygon; - } - - protected abstract removePolygon(polygon: Polygon): void; - - protected abstract doCreatePolygon(definition: PolygonDefinition): Polygon; - - public createPolyline(definition: PolylineDefinition): Polyline { - this.dispatchEvent('polyline:before-create', { definition }); - const polyline = this.doCreatePolyline(definition); - this.dispatchEvent('polyline:after-create', { polyline }); - - polyline['@id'] = definition['@id']; - this.polylines.set(definition['@id'], polyline); - - return polyline; + this.isConnected = true; } - protected abstract removePolyline(polyline: Polyline): void; - - protected abstract doCreatePolyline(definition: PolylineDefinition): Polyline; - - protected createInfoWindow({ + //region Public API + public createInfoWindow({ definition, element, - }: - | { definition: MarkerDefinition['infoWindow']; element: Marker } - | { definition: PolygonDefinition['infoWindow']; element: Polygon } - | { - definition: PolylineDefinition['infoWindow']; - element: Polyline; - }): InfoWindow { + }: { + definition: InfoWindowWithoutPositionDefinition; + element: Marker | Polygon | Polyline; + }): InfoWindow { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); this.dispatchEvent('info-window:after-create', { infoWindow, element }); @@ -198,42 +177,19 @@ export default abstract class< return infoWindow; } - protected abstract doCreateInfoWindow({ - definition, - element, - }: - | { definition: MarkerDefinition['infoWindow']; element: Marker } - | { definition: PolygonDefinition['infoWindow']; element: Polygon } - | { - definition: PolylineDefinition['infoWindow']; - element: Polyline; - }): InfoWindow; - - protected abstract doFitBoundsToMarkers(): void; - - protected abstract dispatchEvent(name: string, payload: Record): void; + //endregion + //region Hooks called by Stimulus when the values change public abstract centerValueChanged(): void; public abstract zoomValueChanged(): void; public markersValueChanged(): void { - if (!this.map) { + if (!this.isConnected) { return; } - this.markers.forEach((marker) => { - if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) { - this.removeMarker(marker); - this.markers.delete(marker['@id']); - } - }); - - this.markersValue.forEach((marker) => { - if (!this.markers.has(marker['@id'])) { - this.createMarker(marker); - } - }); + this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); @@ -241,40 +197,149 @@ export default abstract class< } public polygonsValueChanged(): void { - if (!this.map) { + if (!this.isConnected) { return; } - this.polygons.forEach((polygon) => { - if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - this.removePolygon(polygon); - this.polygons.delete(polygon['@id']); - } - }); - - this.polygonsValue.forEach((polygon) => { - if (!this.polygons.has(polygon['@id'])) { - this.createPolygon(polygon); - } - }); + this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon); } public polylinesValueChanged(): void { - if (!this.map) { + if (!this.isConnected) { return; } - this.polylines.forEach((polyline) => { - if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { - this.removePolyline(polyline); - this.polylines.delete(polyline['@id']); - } + this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); + } + + //endregion + + //region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider + protected abstract doCreateMap({ + center, + zoom, + options, + }: { + center: Point | null; + zoom: number | null; + options: MapOptions; + }): Map; + + protected abstract doFitBoundsToMarkers(): void; + + protected abstract doCreateMarker({ + definition, + }: { definition: MarkerDefinition }): Marker; + + protected abstract doRemoveMarker(marker: Marker): void; + + protected abstract doCreatePolygon({ + definition, + }: { + definition: PolygonDefinition; + }): Polygon; + + protected abstract doRemovePolygon(polygon: Polygon): void; + + protected abstract doCreatePolyline({ + definition, + }: { + definition: PolylineDefinition; + }): Polyline; + + protected abstract doRemovePolyline(polyline: Polyline): void; + + protected abstract doCreateInfoWindow({ + definition, + element, + }: { + definition: InfoWindowWithoutPositionDefinition; + element: Marker | Polygon | Polyline; + }): InfoWindow; + + //endregion + + //region Private APIs + private createDrawingFactory( + type: 'marker', + draws: typeof this.markers, + factory: typeof this.doCreateMarker + ): typeof this.doCreateMarker; + private createDrawingFactory( + type: 'polygon', + draws: typeof this.polygons, + factory: typeof this.doCreatePolygon + ): typeof this.doCreatePolygon; + private createDrawingFactory( + type: 'polyline', + draws: typeof this.polylines, + factory: typeof this.doCreatePolyline + ): typeof this.doCreatePolyline; + private createDrawingFactory< + Factory extends typeof this.doCreateMarker | typeof this.doCreatePolygon | typeof this.doCreatePolyline, + Draw extends ReturnType, + >( + type: 'marker' | 'polygon' | 'polyline', + draws: globalThis.Map, Draw>, + factory: Factory + ): Factory { + const eventBefore = `${type}:before-create`; + const eventAfter = `${type}:after-create`; + + // @ts-expect-error IDK what to do with this error + // 'Factory' could be instantiated with an arbitrary type which could be unrelated to '({ definition }: { definition: WithIdentifier; }) => Draw' + return ({ definition }: { definition: WithIdentifier }) => { + this.dispatchEvent(eventBefore, { definition }); + const drawing = factory({ definition }) as Draw; + this.dispatchEvent(eventAfter, { [type]: drawing }); + + draws.set(definition['@id'], drawing); + + return drawing; + }; + } + + private onDrawChanged( + draws: typeof this.markers, + newDrawDefinitions: typeof this.markersValue, + factory: typeof this.createMarker, + remover: typeof this.doRemoveMarker + ): void; + private onDrawChanged( + draws: typeof this.polygons, + newDrawDefinitions: typeof this.polygonsValue, + factory: typeof this.createPolygon, + remover: typeof this.doRemovePolygon + ): void; + private onDrawChanged( + draws: typeof this.polylines, + newDrawDefinitions: typeof this.polylinesValue, + factory: typeof this.createPolyline, + remover: typeof this.doRemovePolyline + ): void; + private onDrawChanged>>( + draws: globalThis.Map, Draw>, + newDrawDefinitions: Array, + factory: (args: { definition: DrawDefinition }) => Draw, + remover: (args: Draw) => void + ): void { + const idsToRemove = new Set(draws.keys()); + newDrawDefinitions.forEach((definition) => { + idsToRemove.delete(definition['@id']); + }); + + idsToRemove.forEach((id) => { + // biome-ignore lint/style/noNonNullAssertion: the ids are coming from the keys of the map + const draw = draws.get(id)!; + remover(draw); + draws.delete(id); }); - this.polylinesValue.forEach((polyline) => { - if (!this.polylines.has(polyline['@id'])) { - this.createPolyline(polyline); + newDrawDefinitions.forEach((definition) => { + if (!draws.has(definition['@id'])) { + factory({ definition }); } }); } + //endregion } diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts index b3d2623a898..691f2c5d5ec 100644 --- a/src/Map/assets/test/abstract_map_controller.test.ts +++ b/src/Map/assets/test/abstract_map_controller.test.ts @@ -16,7 +16,7 @@ class MyMapController extends AbstractMapController { return { map: 'map', center, zoom, options }; } - doCreateMarker(definition) { + doCreateMarker({ definition }) { const marker = { marker: 'marker', title: definition.title }; if (definition.infoWindow) { @@ -26,7 +26,7 @@ class MyMapController extends AbstractMapController { return marker; } - doCreatePolygon(definition) { + doCreatePolygon({ definition }) { const polygon = { polygon: 'polygon', title: definition.title }; if (definition.infoWindow) { @@ -35,7 +35,7 @@ class MyMapController extends AbstractMapController { return polygon; } - doCreatePolyline(definition) { + doCreatePolyline({ definition }) { const polyline = { polyline: 'polyline', title: definition.title }; if (definition.infoWindow) { @@ -103,20 +103,18 @@ describe('AbstractMapController', () => { expect(controller.map).toEqual({ map: 'map', center: { lat: 48.8566, lng: 2.3522 }, zoom: 4, options: {} }); expect(controller.markers).toEqual( new Map([ - ['a69f13edd2e571f3', { '@id': 'a69f13edd2e571f3', marker: 'marker', title: 'Paris' }], - ['cb9c1a30d562694b', { '@id': 'cb9c1a30d562694b', marker: 'marker', title: 'Lyon' }], - ['e6b3acef1325fb52', { '@id': 'e6b3acef1325fb52', marker: 'marker', title: 'Toulouse' }], + ['a69f13edd2e571f3', { marker: 'marker', title: 'Paris' }], + ['cb9c1a30d562694b', { marker: 'marker', title: 'Lyon' }], + ['e6b3acef1325fb52', { marker: 'marker', title: 'Toulouse' }], ]) ); expect(controller.polygons).toEqual( new Map([ - ['228ae6f5c1b17cfd', { '@id': '228ae6f5c1b17cfd', polygon: 'polygon', title: null }], - ['9874334e4e8caa16', { '@id': '9874334e4e8caa16', polygon: 'polygon', title: null }], + ['228ae6f5c1b17cfd', { polygon: 'polygon', title: null }], + ['9874334e4e8caa16', { polygon: 'polygon', title: null }], ]) ); - expect(controller.polylines).toEqual( - new Map([['0fa955da866c7720', { '@id': '0fa955da866c7720', polyline: 'polyline', title: null }]]) - ); + expect(controller.polylines).toEqual(new Map([['0fa955da866c7720', { polyline: 'polyline', title: null }]])); expect(controller.infoWindows).toEqual([ { headerContent: 'Paris', diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts index 2ce6fcc0bad..9f99a6c354c 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts @@ -1,36 +1,37 @@ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition, InfoWindowWithoutPositionDefinition } from '@symfony/ux-map'; import type { LoaderOptions } from '@googlemaps/js-api-loader'; type MapOptions = Pick; export default class extends AbstractMapController { providerOptionsValue: Pick; + map: google.maps.Map; connect(): Promise; + centerValueChanged(): void; + zoomValueChanged(): void; protected dispatchEvent(name: string, payload?: Record): void; protected doCreateMap({ center, zoom, options, }: { center: Point | null; zoom: number | null; options: MapOptions; }): google.maps.Map; - protected doCreateMarker(definition: MarkerDefinition): google.maps.marker.AdvancedMarkerElement; - protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void; - protected doCreatePolygon(definition: PolygonDefinition): google.maps.Polygon; - protected removePolygon(polygon: google.maps.Polygon): void; - protected doCreatePolyline(definition: PolylineDefinition): google.maps.Polyline; - protected removePolyline(polyline: google.maps.Polyline): void; + protected doCreateMarker({ definition, }: { + definition: MarkerDefinition; + }): google.maps.marker.AdvancedMarkerElement; + protected doRemoveMarker(marker: google.maps.marker.AdvancedMarkerElement): void; + protected doCreatePolygon({ definition, }: { + definition: PolygonDefinition; + }): google.maps.Polygon; + protected doRemovePolygon(polygon: google.maps.Polygon): void; + protected doCreatePolyline({ definition, }: { + definition: PolylineDefinition; + }): google.maps.Polyline; + protected doRemovePolyline(polyline: google.maps.Polyline): void; protected doCreateInfoWindow({ definition, element, }: { - definition: MarkerDefinition['infoWindow']; - element: google.maps.marker.AdvancedMarkerElement; - } | { - definition: PolygonDefinition['infoWindow']; - element: google.maps.Polygon; - } | { - definition: PolylineDefinition['infoWindow']; - element: google.maps.Polyline; + definition: InfoWindowWithoutPositionDefinition; + element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon | google.maps.Polyline; }): google.maps.InfoWindow; + protected doFitBoundsToMarkers(): void; private createTextOrElement; private closeInfoWindowsExcept; - protected doFitBoundsToMarkers(): void; - centerValueChanged(): void; - zoomValueChanged(): void; } export {}; diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js index 4307c67af49..00317a50a2a 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -5,17 +5,21 @@ class default_1 extends Controller { constructor() { super(...arguments); this.markers = new Map(); - this.infoWindows = []; this.polygons = new Map(); this.polylines = new Map(); + this.infoWindows = []; + this.isConnected = false; } connect() { const options = this.optionsValue; this.dispatchEvent('pre-connect', { options }); + this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this)); + this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this)); + this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this)); this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); - this.markersValue.forEach((marker) => this.createMarker(marker)); - this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); - this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); + this.markersValue.forEach((definition) => this.createMarker({ definition })); + this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); + this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -26,30 +30,7 @@ class default_1 extends Controller { polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); - } - createMarker(definition) { - this.dispatchEvent('marker:before-create', { definition }); - const marker = this.doCreateMarker(definition); - this.dispatchEvent('marker:after-create', { marker }); - marker['@id'] = definition['@id']; - this.markers.set(definition['@id'], marker); - return marker; - } - createPolygon(definition) { - this.dispatchEvent('polygon:before-create', { definition }); - const polygon = this.doCreatePolygon(definition); - this.dispatchEvent('polygon:after-create', { polygon }); - polygon['@id'] = definition['@id']; - this.polygons.set(definition['@id'], polygon); - return polygon; - } - createPolyline(definition) { - this.dispatchEvent('polyline:before-create', { definition }); - const polyline = this.doCreatePolyline(definition); - this.dispatchEvent('polyline:after-create', { polyline }); - polyline['@id'] = definition['@id']; - this.polylines.set(definition['@id'], polyline); - return polyline; + this.isConnected = true; } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); @@ -59,53 +40,50 @@ class default_1 extends Controller { return infoWindow; } markersValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.markers.forEach((marker) => { - if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) { - this.removeMarker(marker); - this.markers.delete(marker['@id']); - } - }); - this.markersValue.forEach((marker) => { - if (!this.markers.has(marker['@id'])) { - this.createMarker(marker); - } - }); + this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } } polygonsValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polygons.forEach((polygon) => { - if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - this.removePolygon(polygon); - this.polygons.delete(polygon['@id']); - } - }); - this.polygonsValue.forEach((polygon) => { - if (!this.polygons.has(polygon['@id'])) { - this.createPolygon(polygon); - } - }); + this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon); } polylinesValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polylines.forEach((polyline) => { - if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { - this.removePolyline(polyline); - this.polylines.delete(polyline['@id']); - } + this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); + } + createDrawingFactory(type, draws, factory) { + const eventBefore = `${type}:before-create`; + const eventAfter = `${type}:after-create`; + return ({ definition }) => { + this.dispatchEvent(eventBefore, { definition }); + const drawing = factory({ definition }); + this.dispatchEvent(eventAfter, { [type]: drawing }); + draws.set(definition['@id'], drawing); + return drawing; + }; + } + onDrawChanged(draws, newDrawDefinitions, factory, remover) { + const idsToRemove = new Set(draws.keys()); + newDrawDefinitions.forEach((definition) => { + idsToRemove.delete(definition['@id']); + }); + idsToRemove.forEach((id) => { + const draw = draws.get(id); + remover(draw); + draws.delete(id); }); - this.polylinesValue.forEach((polyline) => { - if (!this.polylines.has(polyline['@id'])) { - this.createPolyline(polyline); + newDrawDefinitions.forEach((definition) => { + if (!draws.has(definition['@id'])) { + factory({ definition }); } }); } @@ -142,6 +120,16 @@ class map_controller extends default_1 { } super.connect(); } + centerValueChanged() { + if (this.map && this.centerValue) { + this.map.setCenter(this.centerValue); + } + } + zoomValueChanged() { + if (this.map && this.zoomValue) { + this.map.setZoom(this.zoomValue); + } + } dispatchEvent(name, payload = {}) { this.dispatch(name, { prefix: 'ux:map', @@ -162,7 +150,7 @@ class map_controller extends default_1 { zoom, }); } - doCreateMarker(definition) { + doCreateMarker({ definition, }) { const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition; const marker = new _google.maps.marker.AdvancedMarkerElement({ position, @@ -176,10 +164,10 @@ class map_controller extends default_1 { } return marker; } - removeMarker(marker) { + doRemoveMarker(marker) { marker.map = null; } - doCreatePolygon(definition) { + doCreatePolygon({ definition, }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = new _google.maps.Polygon({ ...rawOptions, @@ -194,10 +182,10 @@ class map_controller extends default_1 { } return polygon; } - removePolygon(polygon) { + doRemovePolygon(polygon) { polygon.setMap(null); } - doCreatePolyline(definition) { + doCreatePolyline({ definition, }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = new _google.maps.Polyline({ ...rawOptions, @@ -212,7 +200,7 @@ class map_controller extends default_1 { } return polyline; } - removePolyline(polyline) { + doRemovePolyline(polyline) { polyline.setMap(null); } doCreateInfoWindow({ definition, element, }) { @@ -253,6 +241,19 @@ class map_controller extends default_1 { } return infoWindow; } + doFitBoundsToMarkers() { + if (this.markers.size === 0) { + return; + } + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + bounds.extend(marker.position); + }); + this.map.fitBounds(bounds); + } createTextOrElement(content) { if (!content) { return null; @@ -271,29 +272,6 @@ class map_controller extends default_1 { } }); } - doFitBoundsToMarkers() { - if (this.markers.length === 0) { - return; - } - const bounds = new google.maps.LatLngBounds(); - this.markers.forEach((marker) => { - if (!marker.position) { - return; - } - bounds.extend(marker.position); - }); - this.map.fitBounds(bounds); - } - centerValueChanged() { - if (this.map && this.centerValue) { - this.map.setCenter(this.centerValue); - } - } - zoomValueChanged() { - if (this.map && this.zoomValue) { - this.map.setZoom(this.zoomValue); - } - } } export { map_controller as default }; diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts index df5e843c1d5..6f5b9939faf 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -8,7 +8,13 @@ */ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { + Point, + MarkerDefinition, + PolygonDefinition, + PolylineDefinition, + InfoWindowWithoutPositionDefinition, +} from '@symfony/ux-map'; import type { LoaderOptions } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader'; @@ -47,9 +53,11 @@ export default class extends AbstractMapController< 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries' >; + declare map: google.maps.Map; + async connect() { if (!_google) { - _google = { maps: {} }; + _google = { maps: {} as typeof google.maps }; let { libraries = [], ...loaderOptions } = this.providerOptionsValue; @@ -77,6 +85,18 @@ export default class extends AbstractMapController< super.connect(); } + public centerValueChanged(): void { + if (this.map && this.centerValue) { + this.map.setCenter(this.centerValue); + } + } + + public zoomValueChanged(): void { + if (this.map && this.zoomValue) { + this.map.setZoom(this.zoomValue); + } + } + protected dispatchEvent(name: string, payload: Record = {}): void { this.dispatch(name, { prefix: 'ux:map', @@ -109,9 +129,11 @@ export default class extends AbstractMapController< }); } - protected doCreateMarker( - definition: MarkerDefinition - ): google.maps.marker.AdvancedMarkerElement { + protected doCreateMarker({ + definition, + }: { + definition: MarkerDefinition; + }): google.maps.marker.AdvancedMarkerElement { const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition; const marker = new _google.maps.marker.AdvancedMarkerElement({ @@ -129,13 +151,15 @@ export default class extends AbstractMapController< return marker; } - protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void { + protected doRemoveMarker(marker: google.maps.marker.AdvancedMarkerElement): void { marker.map = null; } - protected doCreatePolygon( - definition: PolygonDefinition - ): google.maps.Polygon { + protected doCreatePolygon({ + definition, + }: { + definition: PolygonDefinition; + }): google.maps.Polygon { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = new _google.maps.Polygon({ @@ -155,13 +179,15 @@ export default class extends AbstractMapController< return polygon; } - protected removePolygon(polygon: google.maps.Polygon) { + protected doRemovePolygon(polygon: google.maps.Polygon) { polygon.setMap(null); } - protected doCreatePolyline( - definition: PolylineDefinition - ): google.maps.Polyline { + protected doCreatePolyline({ + definition, + }: { + definition: PolylineDefinition; + }): google.maps.Polyline { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = new _google.maps.Polyline({ @@ -181,29 +207,17 @@ export default class extends AbstractMapController< return polyline; } - protected removePolyline(polyline: google.maps.Polyline): void { + protected doRemovePolyline(polyline: google.maps.Polyline): void { polyline.setMap(null); } protected doCreateInfoWindow({ definition, element, - }: - | { - definition: MarkerDefinition< - google.maps.marker.AdvancedMarkerElementOptions, - google.maps.InfoWindowOptions - >['infoWindow']; - element: google.maps.marker.AdvancedMarkerElement; - } - | { - definition: PolygonDefinition['infoWindow']; - element: google.maps.Polygon; - } - | { - definition: PolylineDefinition['infoWindow']; - element: google.maps.Polyline; - }): google.maps.InfoWindow { + }: { + definition: InfoWindowWithoutPositionDefinition; + element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon | google.maps.Polyline; + }): google.maps.InfoWindow { const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition; const infoWindow = new _google.maps.InfoWindow({ @@ -246,6 +260,23 @@ export default class extends AbstractMapController< return infoWindow; } + protected doFitBoundsToMarkers(): void { + if (this.markers.size === 0) { + return; + } + + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + + bounds.extend(marker.position); + }); + + this.map.fitBounds(bounds); + } + private createTextOrElement(content: string | null): string | HTMLElement | null { if (!content) { return null; @@ -268,33 +299,4 @@ export default class extends AbstractMapController< } }); } - - protected doFitBoundsToMarkers(): void { - if (this.markers.length === 0) { - return; - } - - const bounds = new google.maps.LatLngBounds(); - this.markers.forEach((marker) => { - if (!marker.position) { - return; - } - - bounds.extend(marker.position); - }); - - this.map.fitBounds(bounds); - } - - public centerValueChanged(): void { - if (this.map && this.centerValue) { - this.map.setCenter(this.centerValue); - } - } - - public zoomValueChanged(): void { - if (this.map && this.zoomValue) { - this.map.setZoom(this.zoomValue); - } - } } diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts index ccca26086a2..fbe96749d1d 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts @@ -1,5 +1,5 @@ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition, InfoWindowWithoutPositionDefinition } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolylineOptions as PolygonOptions, PolylineOptions } from 'leaflet'; @@ -10,32 +10,33 @@ type MapOptions = Pick & { options: Record; }; }; -export default class extends AbstractMapController { +export default class extends AbstractMapController { + map: L.Map; connect(): void; + centerValueChanged(): void; + zoomValueChanged(): void; protected dispatchEvent(name: string, payload?: Record): void; protected doCreateMap({ center, zoom, options, }: { center: Point | null; zoom: number | null; options: MapOptions; }): L.Map; - protected doCreateMarker(definition: MarkerDefinition): L.Marker; - protected removeMarker(marker: L.Marker): void; - protected doCreatePolygon(definition: PolygonDefinition): L.Polygon; - protected removePolygon(polygon: L.Polygon): void; - protected doCreatePolyline(definition: PolylineDefinition): L.Polyline; - protected removePolyline(polyline: L.Polyline): void; + protected doCreateMarker({ definition }: { + definition: MarkerDefinition; + }): L.Marker; + protected doRemoveMarker(marker: L.Marker): void; + protected doCreatePolygon({ definition, }: { + definition: PolygonDefinition; + }): L.Polygon; + protected doRemovePolygon(polygon: L.Polygon): void; + protected doCreatePolyline({ definition, }: { + definition: PolylineDefinition; + }): L.Polyline; + protected doRemovePolyline(polyline: L.Polyline): void; protected doCreateInfoWindow({ definition, element, }: { - definition: MarkerDefinition['infoWindow']; - element: L.Marker; - } | { - definition: PolygonDefinition['infoWindow']; - element: L.Polygon; - } | { - definition: PolylineDefinition['infoWindow']; - element: L.Polyline; + definition: InfoWindowWithoutPositionDefinition; + element: L.Marker | L.Polygon | L.Polyline; }): L.Popup; protected doFitBoundsToMarkers(): void; - centerValueChanged(): void; - zoomValueChanged(): void; } export {}; diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js index 088c27063b6..f5bf72b5ff8 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -6,17 +6,21 @@ class default_1 extends Controller { constructor() { super(...arguments); this.markers = new Map(); - this.infoWindows = []; this.polygons = new Map(); this.polylines = new Map(); + this.infoWindows = []; + this.isConnected = false; } connect() { const options = this.optionsValue; this.dispatchEvent('pre-connect', { options }); + this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this)); + this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this)); + this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this)); this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); - this.markersValue.forEach((marker) => this.createMarker(marker)); - this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); - this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); + this.markersValue.forEach((definition) => this.createMarker({ definition })); + this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); + this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -27,30 +31,7 @@ class default_1 extends Controller { polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); - } - createMarker(definition) { - this.dispatchEvent('marker:before-create', { definition }); - const marker = this.doCreateMarker(definition); - this.dispatchEvent('marker:after-create', { marker }); - marker['@id'] = definition['@id']; - this.markers.set(definition['@id'], marker); - return marker; - } - createPolygon(definition) { - this.dispatchEvent('polygon:before-create', { definition }); - const polygon = this.doCreatePolygon(definition); - this.dispatchEvent('polygon:after-create', { polygon }); - polygon['@id'] = definition['@id']; - this.polygons.set(definition['@id'], polygon); - return polygon; - } - createPolyline(definition) { - this.dispatchEvent('polyline:before-create', { definition }); - const polyline = this.doCreatePolyline(definition); - this.dispatchEvent('polyline:after-create', { polyline }); - polyline['@id'] = definition['@id']; - this.polylines.set(definition['@id'], polyline); - return polyline; + this.isConnected = true; } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); @@ -60,53 +41,50 @@ class default_1 extends Controller { return infoWindow; } markersValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.markers.forEach((marker) => { - if (!this.markersValue.find((m) => m['@id'] === marker['@id'])) { - this.removeMarker(marker); - this.markers.delete(marker['@id']); - } - }); - this.markersValue.forEach((marker) => { - if (!this.markers.has(marker['@id'])) { - this.createMarker(marker); - } - }); + this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } } polygonsValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polygons.forEach((polygon) => { - if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - this.removePolygon(polygon); - this.polygons.delete(polygon['@id']); - } - }); - this.polygonsValue.forEach((polygon) => { - if (!this.polygons.has(polygon['@id'])) { - this.createPolygon(polygon); - } - }); + this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon); } polylinesValueChanged() { - if (!this.map) { + if (!this.isConnected) { return; } - this.polylines.forEach((polyline) => { - if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { - this.removePolyline(polyline); - this.polylines.delete(polyline['@id']); - } + this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); + } + createDrawingFactory(type, draws, factory) { + const eventBefore = `${type}:before-create`; + const eventAfter = `${type}:after-create`; + return ({ definition }) => { + this.dispatchEvent(eventBefore, { definition }); + const drawing = factory({ definition }); + this.dispatchEvent(eventAfter, { [type]: drawing }); + draws.set(definition['@id'], drawing); + return drawing; + }; + } + onDrawChanged(draws, newDrawDefinitions, factory, remover) { + const idsToRemove = new Set(draws.keys()); + newDrawDefinitions.forEach((definition) => { + idsToRemove.delete(definition['@id']); + }); + idsToRemove.forEach((id) => { + const draw = draws.get(id); + remover(draw); + draws.delete(id); }); - this.polylinesValue.forEach((polyline) => { - if (!this.polylines.has(polyline['@id'])) { - this.createPolyline(polyline); + newDrawDefinitions.forEach((definition) => { + if (!draws.has(definition['@id'])) { + factory({ definition }); } }); } @@ -133,6 +111,16 @@ class map_controller extends default_1 { }); super.connect(); } + centerValueChanged() { + if (this.map && this.centerValue && this.zoomValue) { + this.map.setView(this.centerValue, this.zoomValue); + } + } + zoomValueChanged() { + if (this.map && this.zoomValue) { + this.map.setZoom(this.zoomValue); + } + } dispatchEvent(name, payload = {}) { this.dispatch(name, { prefix: 'ux:map', @@ -154,18 +142,18 @@ class map_controller extends default_1 { }).addTo(map); return map; } - doCreateMarker(definition) { + doCreateMarker({ definition }) { const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition; - const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map); + const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo(this.map); if (infoWindow) { this.createInfoWindow({ definition: infoWindow, element: marker }); } return marker; } - removeMarker(marker) { + doRemoveMarker(marker) { marker.remove(); } - doCreatePolygon(definition) { + doCreatePolygon({ definition, }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map); if (title) { @@ -176,10 +164,10 @@ class map_controller extends default_1 { } return polygon; } - removePolygon(polygon) { + doRemovePolygon(polygon) { polygon.remove(); } - doCreatePolyline(definition) { + doCreatePolyline({ definition, }) { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); if (title) { @@ -190,7 +178,7 @@ class map_controller extends default_1 { } return polyline; } - removePolyline(polyline) { + doRemovePolyline(polyline) { polyline.remove(); } doCreateInfoWindow({ definition, element, }) { @@ -206,23 +194,15 @@ class map_controller extends default_1 { return popup; } doFitBoundsToMarkers() { - if (this.markers.length === 0) { + if (this.markers.size === 0) { return; } - this.map.fitBounds(this.markers.map((marker) => { + const bounds = []; + this.markers.forEach((marker) => { const position = marker.getLatLng(); - return [position.lat, position.lng]; - })); - } - centerValueChanged() { - if (this.map) { - this.map.setView(this.centerValue, this.zoomValue); - } - } - zoomValueChanged() { - if (this.map) { - this.map.setZoom(this.zoomValue); - } + bounds.push([position.lat, position.lng]); + }); + this.map.fitBounds(bounds); } } diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts index 86cc334c6cf..f0c796371fd 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -1,5 +1,11 @@ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { + Point, + MarkerDefinition, + PolygonDefinition, + PolylineDefinition, + InfoWindowWithoutPositionDefinition, +} from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; import type { @@ -8,6 +14,7 @@ import type { PopupOptions, PolylineOptions as PolygonOptions, PolylineOptions, + LatLngBoundsExpression, } from 'leaflet'; type MapOptions = Pick & { @@ -16,16 +23,18 @@ type MapOptions = Pick & { export default class extends AbstractMapController< MapOptions, - typeof L.Map, + L.Map, MarkerOptions, - typeof L.Marker, + L.Marker, PopupOptions, - typeof L.Popup, + L.Popup, PolygonOptions, - typeof L.Polygon, + L.Polygon, PolylineOptions, - typeof L.Polyline + L.Polyline > { + declare map: L.Map; + connect(): void { L.Marker.prototype.options.icon = L.divIcon({ html: '', @@ -38,6 +47,18 @@ export default class extends AbstractMapController< super.connect(); } + public centerValueChanged(): void { + if (this.map && this.centerValue && this.zoomValue) { + this.map.setView(this.centerValue, this.zoomValue); + } + } + + public zoomValueChanged(): void { + if (this.map && this.zoomValue) { + this.map.setZoom(this.zoomValue); + } + } + protected dispatchEvent(name: string, payload: Record = {}): void { this.dispatch(name, { prefix: 'ux:map', @@ -67,10 +88,12 @@ export default class extends AbstractMapController< return map; } - protected doCreateMarker(definition: MarkerDefinition): L.Marker { + protected doCreateMarker({ definition }: { definition: MarkerDefinition }): L.Marker { const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition; - const marker = L.marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map); + const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo( + this.map + ); if (infoWindow) { this.createInfoWindow({ definition: infoWindow, element: marker }); @@ -79,11 +102,13 @@ export default class extends AbstractMapController< return marker; } - protected removeMarker(marker: L.Marker): void { + protected doRemoveMarker(marker: L.Marker): void { marker.remove(); } - protected doCreatePolygon(definition: PolygonDefinition): L.Polygon { + protected doCreatePolygon({ + definition, + }: { definition: PolygonDefinition }): L.Polygon { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map); @@ -99,11 +124,13 @@ export default class extends AbstractMapController< return polygon; } - protected removePolygon(polygon: L.Polygon) { + protected doRemovePolygon(polygon: L.Polygon) { polygon.remove(); } - protected doCreatePolyline(definition: PolylineDefinition): L.Polyline { + protected doCreatePolyline({ + definition, + }: { definition: PolylineDefinition }): L.Polyline { const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); @@ -119,23 +146,17 @@ export default class extends AbstractMapController< return polyline; } - protected removePolyline(polyline: L.Polyline): void { + protected doRemovePolyline(polyline: L.Polyline): void { polyline.remove(); } protected doCreateInfoWindow({ definition, element, - }: - | { - definition: MarkerDefinition['infoWindow']; - element: L.Marker; - } - | { definition: PolygonDefinition['infoWindow']; element: L.Polygon } - | { - definition: PolylineDefinition['infoWindow']; - element: L.Polyline; - }): L.Popup { + }: { + definition: InfoWindowWithoutPositionDefinition; + element: L.Marker | L.Polygon | L.Polyline; + }): L.Popup { const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; element.bindPopup([headerContent, content].filter((x) => x).join('
'), { ...otherOptions, ...rawOptions }); @@ -153,28 +174,15 @@ export default class extends AbstractMapController< } protected doFitBoundsToMarkers(): void { - if (this.markers.length === 0) { + if (this.markers.size === 0) { return; } - this.map.fitBounds( - this.markers.map((marker: L.Marker) => { - const position = marker.getLatLng(); - - return [position.lat, position.lng]; - }) - ); - } - - public centerValueChanged(): void { - if (this.map) { - this.map.setView(this.centerValue, this.zoomValue); - } - } - - public zoomValueChanged(): void { - if (this.map) { - this.map.setZoom(this.zoomValue); - } + const bounds: LatLngBoundsExpression = []; + this.markers.forEach((marker) => { + const position = marker.getLatLng(); + bounds.push([position.lat, position.lng]); + }); + this.map.fitBounds(bounds); } }