diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md index e756969e4f4..4ffff79fa95 100644 --- a/src/Map/CHANGELOG.md +++ b/src/Map/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +## 2.28 + +- Add support for creating `Circle` by passing a `Point` and a radius (in meters) to the `Circle` constructor, e.g.: +```php +$map->addCircle(new Circle( + center: new Point(48.856613, 2.352222), // Paris + radius: 5_000 // 5km +)); +``` ## 2.27 - The `fitBoundsToMarkers` option is not overridden anymore when using the `Map` LiveComponent, but now respects the value you defined. diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index a593f9ee8a7..f96f2e27bd5 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -48,6 +48,14 @@ export type PolylineDefinition = WithIdentif rawOptions?: PolylineOptions; extra: Record; }>; +export type CircleDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; + center: Point; + radius: number; + title: string | null; + rawOptions?: CircleOptions; + extra: Record; +}>; export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -58,7 +66,7 @@ export type InfoWindowDefinition = { extra: Record; }; export type InfoWindowWithoutPositionDefinition = Omit, 'position'>; -export default abstract class extends Controller { +export default abstract class extends Controller { static values: { providerOptions: ObjectConstructor; center: ObjectConstructor; @@ -67,6 +75,7 @@ export default abstract class>; polygonsValue: Array>; polylinesValue: Array>; + circlesValue: Array>; optionsValue: MapOptions; hasCenterValue: boolean; hasZoomValue: boolean; @@ -82,27 +92,31 @@ export default abstract class; protected polygons: globalThis.Map; protected polylines: globalThis.Map; + protected circles: globalThis.Map; protected infoWindows: Array; private isConnected; private createMarker; private createPolygon; private createPolyline; + private createCircle; protected abstract dispatchEvent(name: string, payload: Record): void; connect(): void; createInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline; + element: Marker | Polygon | Polyline | Circle; }): InfoWindow; abstract centerValueChanged(): void; abstract zoomValueChanged(): void; markersValueChanged(): void; polygonsValueChanged(): void; polylinesValueChanged(): void; + circlesValueChanged(): void; protected abstract doCreateMap({ center, zoom, options, }: { center: Point | null; zoom: number | null; @@ -121,9 +135,13 @@ export default abstract class; }): Polyline; protected abstract doRemovePolyline(polyline: Polyline): void; + protected abstract doCreateCircle({ definition, }: { + definition: CircleDefinition; + }): Circle; + protected abstract doRemoveCircle(circle: Circle): void; protected abstract doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline; + element: Marker | Polygon | Polyline | Circle; }): InfoWindow; protected abstract doCreateIcon({ definition, element, }: { definition: Icon; diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js index 228967fdc41..1fd266e01ec 100644 --- a/src/Map/assets/dist/abstract_map_controller.js +++ b/src/Map/assets/dist/abstract_map_controller.js @@ -11,6 +11,7 @@ class default_1 extends Controller { this.markers = new Map(); this.polygons = new Map(); this.polylines = new Map(); + this.circles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -20,6 +21,7 @@ class default_1 extends Controller { 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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -28,6 +30,7 @@ class default_1 extends Controller { this.markersValue.forEach((definition) => this.createMarker({ definition })); this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); + this.circlesValue.forEach((definition) => this.createCircle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -36,6 +39,7 @@ class default_1 extends Controller { markers: [...this.markers.values()], polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], + circles: [...this.circles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -68,6 +72,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); } + circlesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -104,6 +114,7 @@ default_1.values = { markers: Array, polygons: Array, polylines: Array, + circles: Array, options: Object, }; diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index f41134fa873..8b0c5a70b37 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -80,6 +80,24 @@ export type PolylineDefinition = WithIdentif extra: Record; }>; +export type CircleDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; + center: Point; + radius: number; + title: string | null; + /** + * Raw options passed to the circle constructor, specific to the map provider (e.g.: `L.circle()` for Leaflet). + */ + rawOptions?: CircleOptions; + /** + * 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:circle:before-create` + * - `ux:map:circle:after-create` + */ + extra: Record; +}>; + export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -116,6 +134,8 @@ export default abstract class< Polygon, PolylineOptions, Polyline, + CircleOptions, + Circle, > extends Controller { static values = { providerOptions: Object, @@ -125,6 +145,7 @@ export default abstract class< markers: Array, polygons: Array, polylines: Array, + circles: Array, options: Object, }; @@ -134,6 +155,7 @@ export default abstract class< declare markersValue: Array>; declare polygonsValue: Array>; declare polylinesValue: Array>; + declare circlesValue: Array>; declare optionsValue: MapOptions; declare hasCenterValue: boolean; @@ -142,12 +164,14 @@ export default abstract class< declare hasMarkersValue: boolean; declare hasPolygonsValue: boolean; declare hasPolylinesValue: boolean; + declare hasCirclesValue: boolean; declare hasOptionsValue: boolean; protected map: Map; protected markers = new Map(); protected polygons = new Map(); protected polylines = new Map(); + protected circles = new Map(); protected infoWindows: Array = []; private isConnected = false; @@ -160,6 +184,9 @@ export default abstract class< private createPolyline: ({ definition, }: { definition: PolylineDefinition }) => Polyline; + private createCircle: ({ + definition, + }: { definition: CircleDefinition }) => Circle; protected abstract dispatchEvent(name: string, payload: Record): void; @@ -171,6 +198,7 @@ export default abstract class< 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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, @@ -180,6 +208,7 @@ export default abstract class< this.markersValue.forEach((definition) => this.createMarker({ definition })); this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); + this.circlesValue.forEach((definition) => this.createCircle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); @@ -190,6 +219,7 @@ export default abstract class< markers: [...this.markers.values()], polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], + circles: [...this.circles.values()], infoWindows: this.infoWindows, }); @@ -202,7 +232,7 @@ export default abstract class< element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline; + element: Marker | Polygon | Polyline | Circle; }): InfoWindow { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); @@ -248,6 +278,14 @@ export default abstract class< this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); } + public circlesValueChanged(): void { + if (!this.isConnected) { + return; + } + + this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); + } + //endregion //region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider @@ -285,12 +323,20 @@ export default abstract class< protected abstract doRemovePolyline(polyline: Polyline): void; + protected abstract doCreateCircle({ + definition, + }: { + definition: CircleDefinition; + }): Circle; + + protected abstract doRemoveCircle(circle: Circle): void; + protected abstract doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline; + element: Marker | Polygon | Polyline | Circle; }): InfoWindow; protected abstract doCreateIcon({ definition, @@ -318,11 +364,20 @@ export default abstract class< draws: typeof this.polylines, factory: typeof this.doCreatePolyline ): typeof this.doCreatePolyline; + private createDrawingFactory( + type: 'circle', + draws: typeof this.circles, + factory: typeof this.doCreateCircle + ): typeof this.doCreateCircle; private createDrawingFactory< - Factory extends typeof this.doCreateMarker | typeof this.doCreatePolygon | typeof this.doCreatePolyline, + Factory extends + | typeof this.doCreateMarker + | typeof this.doCreatePolygon + | typeof this.doCreatePolyline + | typeof this.doCreateCircle, Draw extends ReturnType, >( - type: 'marker' | 'polygon' | 'polyline', + type: 'marker' | 'polygon' | 'polyline' | 'circle', draws: globalThis.Map, Draw>, factory: Factory ): Factory { @@ -360,6 +415,12 @@ export default abstract class< factory: typeof this.createPolyline, remover: typeof this.doRemovePolyline ): void; + private onDrawChanged( + draws: typeof this.circles, + newDrawDefinitions: typeof this.circlesValue, + factory: typeof this.createCircle, + remover: typeof this.doRemoveCircle + ): void; private onDrawChanged>>( draws: globalThis.Map, Draw>, newDrawDefinitions: Array, diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts index eee7def47db..abef85efe33 100644 --- a/src/Map/assets/test/abstract_map_controller.test.ts +++ b/src/Map/assets/test/abstract_map_controller.test.ts @@ -44,6 +44,15 @@ class MyMapController extends AbstractMapController { return polyline; } + doCreateCircle({ definition }) { + const circle = { circle: 'circle', title: definition.title }; + + if (definition.infoWindow) { + this.createInfoWindow({ definition: definition.infoWindow, element: circle }); + } + return circle; + } + doCreateInfoWindow({ definition, element }) { if (element.marker) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title }; @@ -54,6 +63,9 @@ class MyMapController extends AbstractMapController { if (element.polyline) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polyline: element.title }; } + if (element.circle) { + return { infoWindow: 'infoWindow', headerContent: definition.headerContent, circle: element.title }; + } } doFitBoundsToMarkers() { @@ -72,17 +84,18 @@ describe('AbstractMapController', () => { beforeEach(() => { container = mountDOM(` -
`); @@ -92,7 +105,7 @@ describe('AbstractMapController', () => { clearDOM(); }); - it('connect and create map, marker, polygon, polyline and info window', async () => { + it('connect and create map, marker, polygon, polyline, circle and info window', async () => { const div = getByTestId(container, 'map'); expect(div).not.toHaveClass('connected'); @@ -115,6 +128,7 @@ describe('AbstractMapController', () => { ]) ); expect(controller.polylines).toEqual(new Map([['0fa955da866c7720', { polyline: 'polyline', title: null }]])); + expect(controller.circles).toEqual(new Map([['7c3e1a9b5f2d4e81', { circle: 'circle', title: null }]])); expect(controller.infoWindows).toEqual([ { headerContent: 'Paris', @@ -141,6 +155,11 @@ describe('AbstractMapController', () => { infoWindow: 'infoWindow', polyline: null, }, + { + headerContent: 'Circle', + infoWindow: 'infoWindow', + circle: null, + }, ]); }); }); diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst index 6779d78c3f2..8ed36788ffd 100644 --- a/src/Map/doc/index.rst +++ b/src/Map/doc/index.rst @@ -208,21 +208,37 @@ You can add Polylines, which represents a path made by a series of ``Point`` ins ), )); +Add Circles +~~~~~~~~~~~ + +You can add Circles, which represents a circular area defined by a center point and a radius (in meters):: + + $map->addCircle(new Circle( + center: new Point(48.8566, 2.3522), + radius: 5_000, // 5km + title: 'Paris', + infoWindow: new InfoWindow( + content: 'A 5km radius circle centered on Paris', + ), + )); + Remove elements from Map ~~~~~~~~~~~~~~~~~~~~~~~~ -It is possible to remove elements like ``Marker``, ``Polygon`` and ``Polyline`` instances by using ``Map::remove*()`` methods. +It is possible to remove elements like ``Marker``, ``Polygon``, ``Polyline`` and ``Circle`` instances by using ``Map::remove*()`` methods. It's useful when :ref:`using a Map inside a Live Component `:: // Add elements $map->addMarker($marker = new Marker(/* ... */)); $map->addPolygon($polygon = new Polygon(/* ... */)); $map->addPolyline($polyline = new Polyline(/* ... */)); + $map->addCircle($circle = new Circle(/* ... */)); // And later, remove those elements $map->removeMarker($marker); $map->removePolygon($polygon); $map->removePolyline($polyline); + $map->removeCircle($circle); If you haven't stored the element instance, you can still remove them by passing the identifier string:: 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 6f2efe0aae3..a83ce8eb3b3 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,8 +1,8 @@ import type { LoaderOptions } from '@googlemaps/js-api-loader'; import AbstractMapController from '@symfony/ux-map'; -import type { Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; type MapOptions = Pick; -export default class extends AbstractMapController { +export default class extends AbstractMapController { providerOptionsValue: Pick; map: google.maps.Map; parser: DOMParser; @@ -27,6 +27,10 @@ export default class extends AbstractMapController; }): google.maps.Polyline; protected doRemovePolyline(polyline: google.maps.Polyline): void; + protected doCreateCircle({ definition, }: { + definition: CircleDefinition; + }): google.maps.Circle; + protected doRemoveCircle(circle: google.maps.Circle): void; protected doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; element: google.maps.marker.AdvancedMarkerElement | google.maps.Polygon | google.maps.Polyline; 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 1bc19b99e50..13f951a7e60 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -12,6 +12,7 @@ class default_1 extends Controller { this.markers = new Map(); this.polygons = new Map(); this.polylines = new Map(); + this.circles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -21,6 +22,7 @@ class default_1 extends Controller { 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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -29,6 +31,7 @@ class default_1 extends Controller { this.markersValue.forEach((definition) => this.createMarker({ definition })); this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); + this.circlesValue.forEach((definition) => this.createCircle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -37,6 +40,7 @@ class default_1 extends Controller { markers: [...this.markers.values()], polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], + circles: [...this.circles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -69,6 +73,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); } + circlesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -105,6 +115,7 @@ default_1.values = { markers: Array, polygons: Array, polylines: Array, + circles: Array, options: Object, }; @@ -220,6 +231,25 @@ class map_controller extends default_1 { doRemovePolyline(polyline) { polyline.setMap(null); } + doCreateCircle({ definition, }) { + const { '@id': _id, center, radius, title, infoWindow, rawOptions = {} } = definition; + const circle = new _google.maps.Circle({ + ...rawOptions, + center, + radius, + map: this.map, + }); + if (title) { + circle.set('title', title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: circle }); + } + return circle; + } + doRemoveCircle(circle) { + circle.setMap(null); + } doCreateInfoWindow({ definition, element, }) { const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition; const infoWindow = new _google.maps.InfoWindow({ 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 db073bc7732..49fbf706eef 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -11,6 +11,7 @@ import type { LoaderOptions } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader'; import AbstractMapController, { IconTypes } from '@symfony/ux-map'; import type { + CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, @@ -49,7 +50,9 @@ export default class extends AbstractMapController< google.maps.PolygonOptions, google.maps.Polygon, google.maps.PolylineOptions, - google.maps.Polyline + google.maps.Polyline, + google.maps.CircleOptions, + google.maps.Circle > { declare providerOptionsValue: Pick< LoaderOptions, @@ -226,6 +229,35 @@ export default class extends AbstractMapController< polyline.setMap(null); } + protected doCreateCircle({ + definition, + }: { + definition: CircleDefinition; + }): google.maps.Circle { + const { '@id': _id, center, radius, title, infoWindow, rawOptions = {} } = definition; + + const circle = new _google.maps.Circle({ + ...rawOptions, + center, + radius, + map: this.map, + }); + + if (title) { + circle.set('title', title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: circle }); + } + + return circle; + } + + protected doRemoveCircle(circle: google.maps.Circle): void { + circle.setMap(null); + } + protected doCreateInfoWindow({ definition, element, diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php index a2f7e7dc127..1882da41683 100644 --- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php +++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php @@ -14,6 +14,7 @@ use Symfony\UX\Icons\IconRendererInterface; use Symfony\UX\Map\Bridge\Google\GoogleOptions; use Symfony\UX\Map\Bridge\Google\Renderer\GoogleRenderer; +use Symfony\UX\Map\Circle; use Symfony\UX\Map\Icon\Icon; use Symfony\UX\Map\Icon\UxIconRenderer; use Symfony\UX\Map\InfoWindow; @@ -101,6 +102,15 @@ public static function provideTestRenderMap(): iterable ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))), ]; + yield 'with circles and infoWindows' => [ + 'renderer' => new GoogleRenderer(new StimulusHelper(null), new UxIconRenderer(null), apiKey: 'api_key'), + 'map' => (new Map()) + ->center(new Point(48.8566, 2.3522)) + ->zoom(12) + ->addCircle(new Circle(center: new Point(48.8566, 2.3522), radius: 500, infoWindow: new InfoWindow(content: 'Circle'))) + ->addCircle(new Circle(center: new Point(1.1, 2.2), radius: 1000, infoWindow: new InfoWindow(content: 'Circle'))), + ]; + yield 'with controls enabled' => [ 'renderer' => new GoogleRenderer(new StimulusHelper(null), new UxIconRenderer(null), apiKey: 'api_key'), 'map' => (new Map()) diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set markers with icons__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set markers with icons__1.txt index 155943865d5..9559402431d 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set markers with icons__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set markers with icons__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":{"type":"url","width":32,"height":32,"url":"https:\/\/cdn.jsdelivr.net\/npm\/bootstrap-icons@1.11.3\/icons\/geo-alt.svg"},"extra":[],"id":null,"@id":"217fa57668ad8e64"},{"position":{"lat":45.764,"lng":4.8357},"title":"Lyon","infoWindow":null,"icon":{"type":"ux-icon","width":32,"height":32,"name":"fa:map-marker","_generated_html":"<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"24\" height=\"24\">...<\/svg>"},"extra":[],"id":null,"@id":"255b208136900fc0"},{"position":{"lat":45.8566,"lng":2.3522},"title":"Dijon","infoWindow":null,"icon":{"type":"svg","width":24,"height":24,"html":"<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"24\" height=\"24\">...<\/svg>"},"extra":[],"id":null,"@id":"1a410e92214f770c"}]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set simple map, with minimum options__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set simple map, with minimum options__1.txt index 91cbdc7634d..8f7cd39eced 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set simple map, with minimum options__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set simple map, with minimum options__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with all markers removed__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with all markers removed__1.txt index 91cbdc7634d..8f7cd39eced 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with all markers removed__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with all markers removed__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with circles and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with circles and infoWindows__1.txt new file mode 100644 index 00000000000..1ca243a562e --- /dev/null +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with circles and infoWindows__1.txt @@ -0,0 +1,14 @@ + +
\ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with controls enabled__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with controls enabled__1.txt index 91cbdc7634d..8f7cd39eced 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with controls enabled__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with controls enabled__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with custom attributes__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with custom attributes__1.txt index f1f0a9a5909..bdb68d0e800 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with custom attributes__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with custom attributes__1.txt @@ -10,5 +10,6 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" class="map" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id overridden by option mapId__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id overridden by option mapId__1.txt index 365f78a9269..cf902c81083 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id overridden by option mapId__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id overridden by option mapId__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id, when passing options (except the mapId)__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id, when passing options (except the mapId)__1.txt index 6d398ba08c5..7a9a93e8f7d 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id, when passing options (except the mapId)__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id, when passing options (except the mapId)__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id__1.txt index 6d398ba08c5..7a9a93e8f7d 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with default map id__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with every options__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with every options__1.txt index db10b08c850..b48e93d3cf1 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with every options__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with every options__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt index 04d1e3965fb..581ad31a65c 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":null,"extra":[],"id":"marker1","@id":"872feba9ebf3905d"},{"position":{"lat":48.8566,"lng":2.3522},"title":"Lyon","infoWindow":{"headerContent":null,"content":"Lyon","position":null,"opened":false,"autoClose":true,"extra":[]},"icon":null,"extra":[],"id":"marker2","@id":"6028bf5e41f644ab"}]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with markers and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with markers and infoWindows__1.txt index c185e4fb2a1..0f83755efa8 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with markers and infoWindows__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with markers and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":null,"extra":[],"id":"marker1","@id":"872feba9ebf3905d"},{"position":{"lat":48.8566,"lng":2.3522},"title":"Lyon","infoWindow":{"headerContent":null,"content":"Lyon","position":null,"opened":false,"autoClose":true,"extra":[]},"icon":null,"extra":[],"id":null,"@id":"bce206d73dc5c164"}]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt index 76f32b102f7..d615a208ea0 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[{"points":[{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522}],"title":null,"infoWindow":null,"extra":[],"id":null,"@id":"7cdd432ea54d0ce9"},{"points":[{"lat":1.1,"lng":2.2},{"lat":3.3,"lng":4.4},{"lat":5.5,"lng":6.6}],"title":null,"infoWindow":{"headerContent":null,"content":"Polygon","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"9074e0a9ead08c1e"}]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt index a1d6ecf8754..ad046db849d 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[{"points":[{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522}],"title":null,"infoWindow":null,"extra":[],"id":null,"@id":"7cdd432ea54d0ce9"},{"points":[{"lat":1.1,"lng":2.2},{"lat":3.3,"lng":4.4},{"lat":5.5,"lng":6.6}],"title":null,"infoWindow":{"headerContent":null,"content":"Polygon","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"9074e0a9ead08c1e"}]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set without controls enabled__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set without controls enabled__1.txt index 3e44c36583a..09aa85802d3 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set without controls enabled__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set without controls enabled__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" + data-symfony--ux-google-map--map-circles-value="[]" > \ No newline at end of file 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 c7c80e91b7e..4d705693924 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,8 +1,8 @@ import AbstractMapController from '@symfony/ux-map'; -import type { Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; -import type { ControlPosition, MapOptions as LeafletMapOptions, MarkerOptions, PolylineOptions as PolygonOptions, PolylineOptions, PopupOptions } from 'leaflet'; +import type { CircleOptions, ControlPosition, MapOptions as LeafletMapOptions, MarkerOptions, PolylineOptions as PolygonOptions, PolylineOptions, PopupOptions } from 'leaflet'; type MapOptions = Pick & { attributionControlOptions?: { position: ControlPosition; @@ -21,7 +21,7 @@ type MapOptions = Pick; } | false; }; -export default class extends AbstractMapController { +export default class extends AbstractMapController { map: L.Map; connect(): void; centerValueChanged(): void; @@ -44,9 +44,13 @@ export default class extends AbstractMapController; }): L.Polyline; protected doRemovePolyline(polyline: L.Polyline): void; + protected doCreateCircle({ definition }: { + definition: CircleDefinition; + }): L.Circle; + protected doRemoveCircle(circle: L.Circle): void; protected doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: L.Marker | L.Polygon | L.Polyline; + element: L.Marker | L.Polygon | L.Polyline | L.Circle; }): L.Popup; protected doCreateIcon({ definition, element, }: { definition: Icon; 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 747d858e4f1..dff15b2ce3d 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -13,6 +13,7 @@ class default_1 extends Controller { this.markers = new Map(); this.polygons = new Map(); this.polylines = new Map(); + this.circles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -22,6 +23,7 @@ class default_1 extends Controller { 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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -30,6 +32,7 @@ class default_1 extends Controller { this.markersValue.forEach((definition) => this.createMarker({ definition })); this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); + this.circlesValue.forEach((definition) => this.createCircle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -38,6 +41,7 @@ class default_1 extends Controller { markers: [...this.markers.values()], polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], + circles: [...this.circles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -70,6 +74,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline); } + circlesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -106,6 +116,7 @@ default_1.values = { markers: Array, polygons: Array, polylines: Array, + circles: Array, options: Object, }; @@ -203,6 +214,20 @@ class map_controller extends default_1 { doRemovePolyline(polyline) { polyline.remove(); } + doCreateCircle({ definition }) { + const { '@id': _id, center, radius, title, infoWindow, rawOptions = {} } = definition; + const circle = L.circle(center, { radius, ...rawOptions }).addTo(this.map); + if (title) { + circle.bindPopup(title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: circle }); + } + return circle; + } + doRemoveCircle(circle) { + circle.remove(); + } doCreateInfoWindow({ definition, element, }) { const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; element.bindPopup([headerContent, content].filter((x) => x).join('
'), { ...otherOptions, ...rawOptions }); 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 21845d0f24c..96314e6ed13 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,6 @@ import AbstractMapController, { IconTypes } from '@symfony/ux-map'; import type { + CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, @@ -10,6 +11,7 @@ import type { import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; import type { + CircleOptions, ControlPosition, LatLngBoundsExpression, MapOptions as LeafletMapOptions, @@ -41,7 +43,9 @@ export default class extends AbstractMapController< PolygonOptions, L.Polygon, PolylineOptions, - L.Polyline + L.Polyline, + CircleOptions, + L.Circle > { declare map: L.Map; @@ -176,12 +180,32 @@ export default class extends AbstractMapController< polyline.remove(); } + protected doCreateCircle({ definition }: { definition: CircleDefinition }): L.Circle { + const { '@id': _id, center, radius, title, infoWindow, rawOptions = {} } = definition; + + const circle = L.circle(center, { radius, ...rawOptions }).addTo(this.map); + + if (title) { + circle.bindPopup(title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: circle }); + } + + return circle; + } + + protected doRemoveCircle(circle: L.Circle): void { + circle.remove(); + } + protected doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: L.Marker | L.Polygon | L.Polyline; + element: L.Marker | L.Polygon | L.Polyline | L.Circle; }): L.Popup { const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php index da0a05c2f56..f1561e3488e 100644 --- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php +++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php @@ -13,6 +13,7 @@ use Symfony\UX\Icons\IconRendererInterface; use Symfony\UX\Map\Bridge\Leaflet\Renderer\LeafletRenderer; +use Symfony\UX\Map\Circle; use Symfony\UX\Map\Icon\Icon; use Symfony\UX\Map\Icon\UxIconRenderer; use Symfony\UX\Map\InfoWindow; @@ -96,6 +97,15 @@ public static function provideTestRenderMap(): iterable ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polyline'), id: 'polyline2')), ]; + yield 'with circles and infoWindows' => [ + 'renderer' => new LeafletRenderer(new StimulusHelper(null), new UxIconRenderer(null)), + 'map' => (new Map()) + ->center(new Point(48.8566, 2.3522)) + ->zoom(12) + ->addCircle(new Circle(center: new Point(48.8566, 2.3522), radius: 1000000, title: 'Paris', id: 'circle1')) + ->addCircle(new Circle(center: new Point(1.1, 2.2), radius: 500, infoWindow: new InfoWindow(content: 'Circle'), id: 'circle2')), + ]; + yield 'markers with icons' => [ 'renderer' => new LeafletRenderer( new StimulusHelper(null), diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set markers with icons__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set markers with icons__1.txt index a30fe285064..9e43016115b 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set markers with icons__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set markers with icons__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":{"type":"url","width":32,"height":32,"url":"https:\/\/cdn.jsdelivr.net\/npm\/bootstrap-icons@1.11.3\/icons\/geo-alt.svg"},"extra":[],"id":null,"@id":"217fa57668ad8e64"},{"position":{"lat":45.764,"lng":4.8357},"title":"Lyon","infoWindow":null,"icon":{"type":"ux-icon","width":32,"height":32,"name":"fa:map-marker","_generated_html":"<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"24\" height=\"24\">...<\/svg>"},"extra":[],"id":null,"@id":"255b208136900fc0"},{"position":{"lat":45.8566,"lng":2.3522},"title":"Dijon","infoWindow":null,"icon":{"type":"svg","width":24,"height":24,"html":"<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"24\" height=\"24\">...<\/svg>"},"extra":[],"id":null,"@id":"1a410e92214f770c"}]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set simple map__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set simple map__1.txt index 0e99279fcb5..7e2de5e6716 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set simple map__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set simple map__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with all markers removed__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with all markers removed__1.txt index 0e99279fcb5..7e2de5e6716 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with all markers removed__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with all markers removed__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with circles and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with circles and infoWindows__1.txt new file mode 100644 index 00000000000..11da769576b --- /dev/null +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with circles and infoWindows__1.txt @@ -0,0 +1,14 @@ + +
\ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with custom attributes__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with custom attributes__1.txt index b4b78c96372..119080dae97 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with custom attributes__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with custom attributes__1.txt @@ -10,5 +10,6 @@ data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" class="map" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt index 65e01c12fe5..63832d46d80 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with marker remove and new ones added__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":null,"extra":[],"id":"marker1","@id":"872feba9ebf3905d"},{"position":{"lat":48.8566,"lng":2.3522},"title":"Lyon","infoWindow":{"headerContent":null,"content":"Lyon","position":null,"opened":false,"autoClose":true,"extra":[]},"icon":null,"extra":[],"id":"marker2","@id":"6028bf5e41f644ab"}]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with markers and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with markers and infoWindows__1.txt index d8bc48d5eb1..ac12e7b2d75 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with markers and infoWindows__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with markers and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null,"icon":null,"extra":[],"id":"marker1","@id":"872feba9ebf3905d"},{"position":{"lat":48.8566,"lng":2.3522},"title":"Lyon","infoWindow":{"headerContent":null,"content":"Lyon","position":null,"opened":false,"autoClose":true,"extra":[]},"icon":null,"extra":[],"id":null,"@id":"bce206d73dc5c164"}]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt index 7b6ae68b194..1bad78f87b8 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polygons and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[{"points":[{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522}],"title":null,"infoWindow":null,"extra":[],"id":"polygon1","@id":"35bfa920335b849d"},{"points":[{"lat":1.1,"lng":2.2},{"lat":3.3,"lng":4.4},{"lat":5.5,"lng":6.6}],"title":null,"infoWindow":{"headerContent":null,"content":"Polygon","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"polygon2","@id":"7be1fe9f10489d73"}]" data-symfony--ux-leaflet-map--map-polylines-value="[]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt index eebd057985a..cd7fe232df0 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with polylines and infoWindows__1.txt @@ -10,4 +10,5 @@ data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[{"points":[{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522},{"lat":48.8566,"lng":2.3522}],"title":null,"infoWindow":null,"extra":[],"id":"polyline1","@id":"823f6ee5acdb5db3"},{"points":[{"lat":1.1,"lng":2.2},{"lat":3.3,"lng":4.4},{"lat":5.5,"lng":6.6}],"title":null,"infoWindow":{"headerContent":null,"content":"Polyline","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"polyline2","@id":"77fb0e390b5e91f1"}]" + data-symfony--ux-leaflet-map--map-circles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Circle.php b/src/Map/src/Circle.php new file mode 100644 index 00000000000..74786eb07f4 --- /dev/null +++ b/src/Map/src/Circle.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map; + +use Symfony\UX\Map\Exception\InvalidArgumentException; + +/** + * Represents a circle on a map. + * + * @author Valmont Pehaut-Pietri + */ +final class Circle implements Element +{ + /** + * @param array $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side + * @param float $radius The radius of the circle in meters + */ + public function __construct( + public readonly Point $center, + public readonly float $radius, + public readonly ?string $title = null, + public readonly ?InfoWindow $infoWindow = null, + public readonly array $extra = [], + public readonly ?string $id = null, + ) { + if ($radius <= 0) { + throw new InvalidArgumentException(\sprintf('Radius must be greater than 0, "%s" given.', $radius)); + } + } + + /** + * Convert the circle to an array representation. + * + * @return array{ + * center: array{lat: float, lng: float}, + * radius: float, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * id: string|null + * } + */ + public function toArray(): array + { + return [ + 'center' => $this->center->toArray(), + 'radius' => $this->radius, + 'title' => $this->title, + 'infoWindow' => $this->infoWindow?->toArray(), + 'extra' => $this->extra, + 'id' => $this->id, + ]; + } + + /** + * @param array{ + * center: array{lat: float, lng: float}, + * radius: float, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * id: string|null + * } $circle + * + * @internal + */ + public static function fromArray(array $circle): self + { + if (!isset($circle['center'])) { + throw new InvalidArgumentException('The "center" parameter is required.'); + } + if (!isset($circle['radius'])) { + throw new InvalidArgumentException('The "radius" parameter is required.'); + } + + $circle['center'] = Point::fromArray($circle['center']); + + if (isset($circle['infoWindow'])) { + $circle['infoWindow'] = InfoWindow::fromArray($circle['infoWindow']); + } + + return new self(...$circle); + } +} diff --git a/src/Map/src/Circles.php b/src/Map/src/Circles.php new file mode 100644 index 00000000000..9f474588d55 --- /dev/null +++ b/src/Map/src/Circles.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map; + +/** + * Represents a Circle collection. + * + * @author Valmont Pehaut-Pietri + * + * @internal + */ +final class Circles extends Elements +{ + public static function fromArray(array $elements): self + { + $elementObjects = []; + + foreach ($elements as $element) { + $elementObjects[] = Circle::fromArray($element); + } + + return new self(elements: $elementObjects); + } +} diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php index 52832a8f311..cee3730284f 100644 --- a/src/Map/src/Map.php +++ b/src/Map/src/Map.php @@ -23,11 +23,13 @@ final class Map private Markers $markers; private Polygons $polygons; private Polylines $polylines; + private Circles $circles; /** * @param Marker[] $markers * @param Polygon[] $polygons * @param Polyline[] $polylines + * @param Circle[] $circles */ public function __construct( private readonly ?string $rendererName = null, @@ -38,10 +40,12 @@ public function __construct( array $markers = [], array $polygons = [], array $polylines = [], + array $circles = [], ) { $this->markers = new Markers($markers); $this->polygons = new Polygons($polygons); $this->polylines = new Polylines($polylines); + $this->circles = new Circles($circles); } public function getRendererName(): ?string @@ -129,6 +133,20 @@ public function removePolyline(Polyline|string $polylineOrId): self return $this; } + public function addCircle(Circle $circle): self + { + $this->circles->add($circle); + + return $this; + } + + public function removeCircle(Circle|string $circleOrId): self + { + $this->circles->remove($circleOrId); + + return $this; + } + public function toArray(): array { if (!$this->fitBoundsToMarkers) { @@ -149,6 +167,7 @@ public function toArray(): array 'markers' => $this->markers->toArray(), 'polygons' => $this->polygons->toArray(), 'polylines' => $this->polylines->toArray(), + 'circles' => $this->circles->toArray(), ]; } @@ -159,6 +178,7 @@ public function toArray(): array * markers?: list, * polygons?: list, * polylines?: list, + * circles?: list, * fitBoundsToMarkers?: bool, * options?: array, * } $map @@ -193,6 +213,12 @@ public static function fromArray(array $map): self } $map['polylines'] = array_map(Polyline::fromArray(...), $map['polylines']); + $map['circles'] ??= []; + if (!\is_array($map['circles'])) { + throw new InvalidArgumentException('The "circles" parameter must be an array.'); + } + $map['circles'] = array_map(Circle::fromArray(...), $map['circles']); + return new self(...$map); } } diff --git a/src/Map/tests/CircleTest.php b/src/Map/tests/CircleTest.php new file mode 100644 index 00000000000..8d853a5b248 --- /dev/null +++ b/src/Map/tests/CircleTest.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Circle; +use Symfony\UX\Map\Exception\InvalidArgumentException; +use Symfony\UX\Map\InfoWindow; +use Symfony\UX\Map\Point; + +class CircleTest extends TestCase +{ + public function testToArray() + { + $center = new Point(1.1, 2.2); + $infoWindow = new InfoWindow('info content'); + + $circle = new Circle( + center: $center, + radius: 500, + title: 'Test Circle', + infoWindow: $infoWindow, + extra: ['foo' => 'bar'], + id: 'circle1' + ); + + $array = $circle->toArray(); + self::assertSame([ + 'center' => ['lat' => 1.1, 'lng' => 2.2], + 'radius' => 500.0, + 'title' => 'Test Circle', + 'infoWindow' => [ + 'headerContent' => 'info content', + 'content' => null, + 'position' => null, + 'opened' => false, + 'autoClose' => true, + 'extra' => $array['infoWindow']['extra'], + ], + 'extra' => ['foo' => 'bar'], + 'id' => 'circle1', + ], $array); + } + + public function testFromArray() + { + $data = [ + 'center' => ['lat' => 1.1, 'lng' => 2.2], + 'radius' => 500, + 'title' => 'Test Circle', + 'infoWindow' => ['content' => 'info content'], + 'extra' => ['foo' => 'bar'], + 'id' => 'circle1', + ]; + + $circle = Circle::fromArray($data); + + self::assertInstanceOf(Circle::class, $circle); + + $array = $circle->toArray(); + self::assertSame([ + 'center' => ['lat' => 1.1, 'lng' => 2.2], + 'radius' => 500.0, + 'title' => 'Test Circle', + 'infoWindow' => [ + 'headerContent' => null, + 'content' => 'info content', + 'position' => null, + 'opened' => false, + 'autoClose' => true, + 'extra' => $array['infoWindow']['extra'], + ], + 'extra' => ['foo' => 'bar'], + 'id' => 'circle1', + ], $array); + } + + public function testFromArrayThrowsExceptionIfCenterMissing() + { + $this->expectException(InvalidArgumentException::class); + Circle::fromArray(['radius' => 500, 'invalid' => 'No center']); + } + + public function testFromArrayThrowsExceptionIfRadiusMissing() + { + $this->expectException(InvalidArgumentException::class); + Circle::fromArray(['center' => ['lat' => 1.1, 'lng' => 2.2], 'invalid' => 'No radius']); + } + + public function testConstructorThrowsExceptionIfRadiusNotPositive() + { + $this->expectException(InvalidArgumentException::class); + new Circle(new Point(1.1, 2.2), 0); + } +} diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php index c16fe3d1b78..dad69e5172d 100644 --- a/src/Map/tests/MapTest.php +++ b/src/Map/tests/MapTest.php @@ -12,6 +12,7 @@ namespace Symfony\UX\Map\Tests; use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Circle; use Symfony\UX\Map\Exception\InvalidArgumentException; use Symfony\UX\Map\InfoWindow; use Symfony\UX\Map\Map; @@ -68,6 +69,7 @@ public function testZoomAndCenterCanBeOmittedIfFitBoundsToMarkers(): void 'markers' => [], 'polygons' => [], 'polylines' => [], + 'circles' => [], ], $array); } @@ -88,6 +90,7 @@ public function testWithMinimumConfiguration(): void 'markers' => [], 'polygons' => [], 'polylines' => [], + 'circles' => [], ], $array); } @@ -163,6 +166,30 @@ public function testWithMaximumConfiguration(): void autoClose: true, ), )) + ->addCircle(new Circle( + center: new Point(48.8566, 2.3522), + radius: 500, + title: 'Circle around Paris', + infoWindow: new InfoWindow( + headerContent: 'Circle around Paris', + content: 'A circle with a radius of 500 meters around Paris.', + position: new Point(48.8566, 2.3522), + opened: true, + autoClose: true, + ), + )) + ->addCircle(new Circle( + center: new Point(45.764, 4.8357), + radius: 300, + title: 'Circle around Lyon', + infoWindow: new InfoWindow( + headerContent: 'Circle around Lyon', + content: 'A circle with a radius of 300 meters around Lyon.', + position: new Point(45.764, 4.8357), + opened: true, + autoClose: true, + ), + )) ; self::assertEquals([ @@ -283,6 +310,38 @@ public function testWithMaximumConfiguration(): void 'id' => null, ], ], + 'circles' => [ + [ + 'center' => ['lat' => 48.8566, 'lng' => 2.3522], + 'radius' => 500, + 'title' => 'Circle around Paris', + 'infoWindow' => [ + 'headerContent' => 'Circle around Paris', + 'content' => 'A circle with a radius of 500 meters around Paris.', + 'position' => ['lat' => 48.8566, 'lng' => 2.3522], + 'opened' => true, + 'autoClose' => true, + 'extra' => [], + ], + 'extra' => [], + 'id' => null, + ], + [ + 'center' => ['lat' => 45.764, 'lng' => 4.8357], + 'radius' => 300, + 'title' => 'Circle around Lyon', + 'infoWindow' => [ + 'headerContent' => 'Circle around Lyon', + 'content' => 'A circle with a radius of 300 meters around Lyon.', + 'position' => ['lat' => 45.764, 'lng' => 4.8357], + 'opened' => true, + 'autoClose' => true, + 'extra' => [], + ], + 'extra' => [], + 'id' => null, + ], + ], ], $map->toArray()); } }