From 183488d3ff636b17669647c2a1803cb3d4936320 Mon Sep 17 00:00:00 2001 From: Valmonzo Date: Tue, 17 Jun 2025 13:04:50 +0200 Subject: [PATCH 1/4] [Map] Add support for rectangle Co-authored-by: Hugo Alliaume --- src/Map/CHANGELOG.md | 10 ++ .../assets/dist/abstract_map_controller.d.ts | 26 +++++- .../assets/dist/abstract_map_controller.js | 11 +++ src/Map/assets/src/abstract_map_controller.ts | 69 +++++++++++++- .../test/abstract_map_controller.test.ts | 21 ++++- src/Map/doc/index.rst | 14 +++ .../Google/assets/dist/map_controller.d.ts | 8 +- .../Google/assets/dist/map_controller.js | 29 ++++++ .../Google/assets/src/map_controller.ts | 33 ++++++- .../Google/assets/test/map_controller.test.ts | 21 +++-- .../Google/tests/GoogleRendererTest.php | 10 ++ ...ap with data set markers with icons__1.txt | 1 + ...et simple map, with minimum options__1.txt | 1 + ...h data set with all markers removed__1.txt | 1 + ...ta set with circles and infoWindows__1.txt | 1 + ...with data set with controls enabled__1.txt | 1 + ...ith data set with custom attributes__1.txt | 1 + ...t map id overridden by option mapId__1.txt | 1 + ... passing options (except the mapId)__1.txt | 1 + ...p with data set with default map id__1.txt | 1 + ...ap with data set with every options__1.txt | 1 + ...th marker remove and new ones added__1.txt | 1 + ...ta set with markers and infoWindows__1.txt | 1 + ...a set with polygons and infoWindows__1.txt | 1 + ... set with polylines and infoWindows__1.txt | 1 + ...set with rectangles and infoWindows__1.txt | 15 +++ ...h data set without controls enabled__1.txt | 1 + .../Leaflet/assets/dist/map_controller.d.ts | 12 ++- .../Leaflet/assets/dist/map_controller.js | 28 ++++++ .../Leaflet/assets/src/map_controller.ts | 36 +++++++- .../assets/test/map_controller.test.ts | 21 +++-- .../Leaflet/tests/LeafletRendererTest.php | 20 ++++ ...ap with data set markers with icons__1.txt | 1 + ...tRenderMap with data set simple map__1.txt | 1 + ...h data set with all markers removed__1.txt | 1 + ...ta set with circles and infoWindows__1.txt | 1 + ...ith data set with custom attributes__1.txt | 1 + ...th marker remove and new ones added__1.txt | 1 + ...ta set with markers and infoWindows__1.txt | 1 + ...a set with polygons and infoWindows__1.txt | 1 + ... set with polylines and infoWindows__1.txt | 1 + ...set with rectangles and infoWindows__1.txt | 15 +++ src/Map/src/Map.php | 34 ++++++- src/Map/src/Rectangle.php | 85 +++++++++++++++++ src/Map/src/Rectangles.php | 33 +++++++ src/Map/tests/MapTest.php | 63 +++++++++++++ src/Map/tests/RectangleTest.php | 92 +++++++++++++++++++ 47 files changed, 689 insertions(+), 41 deletions(-) create mode 100644 src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt create mode 100644 src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt create mode 100644 src/Map/src/Rectangle.php create mode 100644 src/Map/src/Rectangles.php create mode 100644 src/Map/tests/RectangleTest.php diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md index 4ffff79fa95..d5be76af200 100644 --- a/src/Map/CHANGELOG.md +++ b/src/Map/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG +## 2.29 + +- Add support for creating `Rectangle` by passing two `Point` instances to the `Rectangle` constructor, e.g.: +```php +$map->addRectangle(new Rectangle( + southWest: new Point(48.856613, 2.352222), // Paris + northEast: new Point(48.51238 2.21080) // Gare de Lyon (Paris) +)); +``` + ## 2.28 - Add support for creating `Circle` by passing a `Point` and a radius (in meters) to the `Circle` constructor, e.g.: diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index f96f2e27bd5..dfe93968e41 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -56,6 +56,16 @@ export type CircleDefinition = WithIdentifier< rawOptions?: CircleOptions; extra: Record; }>; +export type RectangleDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; + bounds: { + northEast: Point; + southWest: Point; + }; + title: string | null; + rawOptions?: RectangleOptions; + extra: Record; +}>; export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -66,7 +76,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; @@ -76,6 +86,7 @@ export default abstract class>; polylinesValue: Array>; circlesValue: Array>; + rectanglesValue: Array>; optionsValue: MapOptions; hasCenterValue: boolean; hasZoomValue: boolean; @@ -93,23 +105,26 @@ export default abstract class; protected polygons: globalThis.Map; protected polylines: globalThis.Map; protected circles: globalThis.Map; + protected rectangles: globalThis.Map; protected infoWindows: Array; private isConnected; private createMarker; private createPolygon; private createPolyline; private createCircle; + private createRectangle; protected abstract dispatchEvent(name: string, payload: Record): void; connect(): void; createInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline | Circle; + element: Marker | Polygon | Polyline | Circle | Rectangle; }): InfoWindow; abstract centerValueChanged(): void; abstract zoomValueChanged(): void; @@ -117,6 +132,7 @@ export default abstract class; }): Circle; protected abstract doRemoveCircle(circle: Circle): void; + protected abstract doCreateRectangle({ definition, }: { + definition: RectangleDefinition; + }): Rectangle; + protected abstract doRemoveRectangle(rectangle: Rectangle): void; protected abstract doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline | Circle; + element: Marker | Polygon | Polyline | Circle | Rectangle; }): 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 1fd266e01ec..446fa7c7af9 100644 --- a/src/Map/assets/dist/abstract_map_controller.js +++ b/src/Map/assets/dist/abstract_map_controller.js @@ -12,6 +12,7 @@ class default_1 extends Controller { this.polygons = new Map(); this.polylines = new Map(); this.circles = new Map(); + this.rectangles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -22,6 +23,7 @@ class default_1 extends Controller { 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.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -31,6 +33,7 @@ class default_1 extends Controller { this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); this.circlesValue.forEach((definition) => this.createCircle({ definition })); + this.rectanglesValue.forEach((definition) => this.createRectangle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -40,6 +43,7 @@ class default_1 extends Controller { polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], circles: [...this.circles.values()], + rectangles: [...this.rectangles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -78,6 +82,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); } + rectanglesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -115,6 +125,7 @@ default_1.values = { polygons: Array, polylines: Array, circles: Array, + rectangles: Array, options: Object, }; diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index 8b0c5a70b37..27d0138a1ef 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -98,6 +98,23 @@ export type CircleDefinition = WithIdentifier< extra: Record; }>; +export type RectangleDefinition = WithIdentifier<{ + infoWindow?: InfoWindowWithoutPositionDefinition; + bounds: { northEast: Point; southWest: Point }; + title: string | null; + /** + * Raw options passed to the rectangle constructor, specific to the map provider (e.g.: `L.rectangle()` for Leaflet). + */ + rawOptions?: RectangleOptions; + /** + * 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:rectangle:before-create` + * - `ux:map:rectangle:after-create` + */ + extra: Record; +}>; + export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -136,6 +153,8 @@ export default abstract class< Polyline, CircleOptions, Circle, + RectangleOptions, + Rectangle, > extends Controller { static values = { providerOptions: Object, @@ -146,6 +165,7 @@ export default abstract class< polygons: Array, polylines: Array, circles: Array, + rectangles: Array, options: Object, }; @@ -156,6 +176,7 @@ export default abstract class< declare polygonsValue: Array>; declare polylinesValue: Array>; declare circlesValue: Array>; + declare rectanglesValue: Array>; declare optionsValue: MapOptions; declare hasCenterValue: boolean; @@ -165,6 +186,7 @@ export default abstract class< declare hasPolygonsValue: boolean; declare hasPolylinesValue: boolean; declare hasCirclesValue: boolean; + declare hasRectanglesValue: boolean; declare hasOptionsValue: boolean; protected map: Map; @@ -172,6 +194,7 @@ export default abstract class< protected polygons = new Map(); protected polylines = new Map(); protected circles = new Map(); + protected rectangles = new Map(); protected infoWindows: Array = []; private isConnected = false; @@ -187,6 +210,9 @@ export default abstract class< private createCircle: ({ definition, }: { definition: CircleDefinition }) => Circle; + private createRectangle: ({ + definition, + }: { definition: RectangleDefinition }) => Rectangle; protected abstract dispatchEvent(name: string, payload: Record): void; @@ -199,6 +225,11 @@ export default abstract class< 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.createRectangle = this.createDrawingFactory( + 'rectangle', + this.rectangles, + this.doCreateRectangle.bind(this) + ); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, @@ -209,6 +240,7 @@ export default abstract class< this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); this.circlesValue.forEach((definition) => this.createCircle({ definition })); + this.rectanglesValue.forEach((definition) => this.createRectangle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); @@ -220,6 +252,7 @@ export default abstract class< polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], circles: [...this.circles.values()], + rectangles: [...this.rectangles.values()], infoWindows: this.infoWindows, }); @@ -232,7 +265,7 @@ export default abstract class< element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline | Circle; + element: Marker | Polygon | Polyline | Circle | Rectangle; }): InfoWindow { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); @@ -286,6 +319,14 @@ export default abstract class< this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); } + public rectanglesValueChanged(): void { + if (!this.isConnected) { + return; + } + + this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle); + } + //endregion //region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider @@ -331,12 +372,20 @@ export default abstract class< protected abstract doRemoveCircle(circle: Circle): void; + protected abstract doCreateRectangle({ + definition, + }: { + definition: RectangleDefinition; + }): Rectangle; + + protected abstract doRemoveRectangle(rectangle: Rectangle): void; + protected abstract doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: Marker | Polygon | Polyline | Circle; + element: Marker | Polygon | Polyline | Circle | Rectangle; }): InfoWindow; protected abstract doCreateIcon({ definition, @@ -369,15 +418,21 @@ export default abstract class< draws: typeof this.circles, factory: typeof this.doCreateCircle ): typeof this.doCreateCircle; + private createDrawingFactory( + type: 'rectangle', + draws: typeof this.rectangles, + factory: typeof this.doCreateRectangle + ): typeof this.doCreateRectangle; private createDrawingFactory< Factory extends | typeof this.doCreateMarker | typeof this.doCreatePolygon | typeof this.doCreatePolyline - | typeof this.doCreateCircle, + | typeof this.doCreateCircle + | typeof this.doCreateRectangle, Draw extends ReturnType, >( - type: 'marker' | 'polygon' | 'polyline' | 'circle', + type: 'marker' | 'polygon' | 'polyline' | 'circle' | 'rectangle', draws: globalThis.Map, Draw>, factory: Factory ): Factory { @@ -421,6 +476,12 @@ export default abstract class< factory: typeof this.createCircle, remover: typeof this.doRemoveCircle ): void; + private onDrawChanged( + draws: typeof this.rectangles, + newDrawDefinitions: typeof this.rectanglesValue, + factory: typeof this.createRectangle, + remover: typeof this.doRemoveRectangle + ): 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 abef85efe33..6ead1d86103 100644 --- a/src/Map/assets/test/abstract_map_controller.test.ts +++ b/src/Map/assets/test/abstract_map_controller.test.ts @@ -53,6 +53,15 @@ class MyMapController extends AbstractMapController { return circle; } + doCreateRectangle({ definition }) { + const rectangle = { rectangle: 'rectangle', title: definition.title }; + + if (definition.infoWindow) { + this.createInfoWindow({ definition: definition.infoWindow, element: rectangle }); + } + return rectangle; + } + doCreateInfoWindow({ definition, element }) { if (element.marker) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title }; @@ -66,6 +75,9 @@ class MyMapController extends AbstractMapController { if (element.circle) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, circle: element.title }; } + if (element.rectangle) { + return { infoWindow: 'infoWindow', headerContent: definition.headerContent, rectangle: element.title }; + } } doFitBoundsToMarkers() { @@ -96,6 +108,7 @@ describe('AbstractMapController', () => { data-map-polygons-value="[{"points":[{"lat":48.8566,"lng":2.3522},{"lat":45.75,"lng":4.85},{"lat":43.6047,"lng":1.4442}],"title":null,"infoWindow":null,"extra":[],"@id":"228ae6f5c1b17cfd"},{"points":[{"lat":1.4442,"lng":43.6047},{"lat":4.85,"lng":45.75},{"lat":2.3522,"lng":48.8566}],"title":null,"infoWindow":{"headerContent":"Polygon","content":null,"position":null,"opened":false,"autoClose":true,"extra":{"foo":"bar"}},"extra":{"fillColor":"#ff0000"},"@id":"9874334e4e8caa16"}]" data-map-polylines-value="[{"points":[{"lat":48.1173,"lng":-1.6778},{"lat":48.8566,"lng":2.3522},{"lat":48.2082,"lng":16.3738}],"title":null,"infoWindow":{"headerContent":"Polyline","content":null,"position":null,"opened":false,"autoClose":true,"extra":{"foo":"bar"}},"extra":{"strokeColor":"#ff0000"},"@id":"0fa955da866c7720"}]" data-map-circles-value="[{"center":{"lat":48.8566,"lng":2.3522},"title":null,"infoWindow":{"headerContent":"Circle","content":null,"position":null,"opened":false,"autoClose":true,"extra":{"foo":"bar"}},"extra":{"color":"#ff0000", "radii":1000},"@id":"7c3e1a9b5f2d4e81"}]" + data-map-rectangles-value="[{"bounds":{"northEast":{"lat":48.8566,"lng":2.3522},"southWest":{"lat":45.75,"lng":4.85}},"title":null,"infoWindow":{"headerContent":"Rectangle","content":null,"position":null,"opened":false,"autoClose":true,"extra":{"foo":"bar"}},"extra":{"color":"#ff0000", "width":1000},"@id":"e6b3acef1325fb52"}]" style="height: 600px" > `); @@ -105,7 +118,7 @@ describe('AbstractMapController', () => { clearDOM(); }); - it('connect and create map, marker, polygon, polyline, circle and info window', async () => { + it('connect and create map, marker, polygon, polyline, circle, rectangle and info window', async () => { const div = getByTestId(container, 'map'); expect(div).not.toHaveClass('connected'); @@ -129,6 +142,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.rectangles).toEqual(new Map([['e6b3acef1325fb52', { rectangle: 'rectangle', title: null }]])); expect(controller.infoWindows).toEqual([ { headerContent: 'Paris', @@ -160,6 +174,11 @@ describe('AbstractMapController', () => { infoWindow: 'infoWindow', circle: null, }, + { + headerContent: 'Rectangle', + infoWindow: 'infoWindow', + rectangle: null, + }, ]); }); }); diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst index 8ed36788ffd..ddbd4ba9890 100644 --- a/src/Map/doc/index.rst +++ b/src/Map/doc/index.rst @@ -222,6 +222,20 @@ You can add Circles, which represents a circular area defined by a center point ), )); +Add Rectangles +~~~~~~~~~~~~~~ + +You can add Rectangles, which represents a rectangular area defined by two corner points:: + + $map->addRectangle(new Rectangle( + southWest: new Point(48.8566, 2.3522), // Paris + northEast: new Point(50.6292, 3.0573), // Lille + title: 'Paris to Lille', + infoWindow: new InfoWindow( + content: 'A rectangle from Paris to Lille', + ), + )); + Remove elements from Map ~~~~~~~~~~~~~~~~~~~~~~~~ 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 a83ce8eb3b3..4895c2bf881 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 { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } 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; @@ -31,6 +31,10 @@ export default class extends AbstractMapController; }): google.maps.Circle; protected doRemoveCircle(circle: google.maps.Circle): void; + protected doCreateRectangle({ definition, }: { + definition: RectangleDefinition; + }): google.maps.Rectangle; + protected doRemoveRectangle(rectangle: google.maps.Rectangle): 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 c7878c5c28f..74f38869f28 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -13,6 +13,7 @@ class default_1 extends Controller { this.polygons = new Map(); this.polylines = new Map(); this.circles = new Map(); + this.rectangles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -23,6 +24,7 @@ class default_1 extends Controller { 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.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -32,6 +34,7 @@ class default_1 extends Controller { this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); this.circlesValue.forEach((definition) => this.createCircle({ definition })); + this.rectanglesValue.forEach((definition) => this.createRectangle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -41,6 +44,7 @@ class default_1 extends Controller { polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], circles: [...this.circles.values()], + rectangles: [...this.rectangles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -79,6 +83,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); } + rectanglesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -116,6 +126,7 @@ default_1.values = { polygons: Array, polylines: Array, circles: Array, + rectangles: Array, options: Object, }; @@ -264,6 +275,24 @@ class map_controller extends default_1 { doRemoveCircle(circle) { circle.setMap(null); } + doCreateRectangle({ definition, }) { + const { bounds, title, infoWindow, rawOptions = {} } = definition; + const rectangle = new _google.maps.Rectangle({ + ...rawOptions, + bounds: new _google.maps.LatLngBounds(bounds.southWest, bounds.northEast), + map: this.map, + }); + if (title) { + rectangle.set('title', title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: rectangle }); + } + return rectangle; + } + doRemoveRectangle(rectangle) { + rectangle.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 94cf80bf23c..a203eb53445 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -18,6 +18,7 @@ import type { Point, PolygonDefinition, PolylineDefinition, + RectangleDefinition, } from '@symfony/ux-map'; type MapOptions = Pick< @@ -57,7 +58,9 @@ export default class extends AbstractMapController< google.maps.PolylineOptions, google.maps.Polyline, google.maps.CircleOptions, - google.maps.Circle + google.maps.Circle, + google.maps.RectangleOptions, + google.maps.Rectangle > { declare providerOptionsValue: Pick< LoaderOptions, @@ -275,6 +278,34 @@ export default class extends AbstractMapController< circle.setMap(null); } + protected doCreateRectangle({ + definition, + }: { + definition: RectangleDefinition; + }): google.maps.Rectangle { + const { bounds, title, infoWindow, rawOptions = {} } = definition; + + const rectangle = new _google.maps.Rectangle({ + ...rawOptions, + bounds: new _google.maps.LatLngBounds(bounds.southWest, bounds.northEast), + map: this.map, + }); + + if (title) { + rectangle.set('title', title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: rectangle }); + } + + return rectangle; + } + + protected doRemoveRectangle(rectangle: google.maps.Rectangle): void { + rectangle.setMap(null); + } + protected doCreateInfoWindow({ definition, element, diff --git a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts index ff5d31d041a..c681aaa348f 100644 --- a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts +++ b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts @@ -36,17 +36,18 @@ describe('GoogleMapsController', () => { beforeEach(() => { container = mountDOM(` -
`); diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php index 1882da41683..d40a1e71f56 100644 --- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php +++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php @@ -23,6 +23,7 @@ use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; use Symfony\UX\Map\Test\RendererTestCase; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; @@ -111,6 +112,15 @@ public static function provideTestRenderMap(): iterable ->addCircle(new Circle(center: new Point(1.1, 2.2), radius: 1000, infoWindow: new InfoWindow(content: 'Circle'))), ]; + yield 'with rectangles 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) + ->addRectangle(new Rectangle(southWest: new Point(48.8566, 2.3522), northEast: new Point(48.8566, 2.3522), infoWindow: new InfoWindow(content: 'Rectangle'))) + ->addRectangle(new Rectangle(southWest: new Point(1.1, 2.2), northEast: new Point(3.3, 4.4), infoWindow: new InfoWindow(content: 'Rectangle'))), + ]; + 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 9559402431d..19bc46ce8e4 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 8f7cd39eced..bdbb9ae5675 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 8f7cd39eced..bdbb9ae5675 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 index 1ca243a562e..a1f6d7a4baa 100644 --- 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[{"center":{"lat":48.8566,"lng":2.3522},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null},{"center":{"lat":1.1,"lng":2.2},"radius":1000,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null}]" + data-symfony--ux-google-map--map-rectangles-value="[]" > \ 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 8f7cd39eced..bdbb9ae5675 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 bdb68d0e800..a039ca7a9a5 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 @@ -11,5 +11,6 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 cf902c81083..0a9f3db6b7c 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 7a9a93e8f7d..a313ade2448 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 7a9a93e8f7d..a313ade2448 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 b48e93d3cf1..e5943a9f1cb 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 581ad31a65c..bd3883e62b8 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 0f83755efa8..281c1adfc69 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 d615a208ea0..1d2aaf668ca 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 @@ -11,4 +11,5 @@ 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="[]" + data-symfony--ux-google-map--map-rectangles-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 ad046db849d..2ed6b41da30 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 @@ -11,4 +11,5 @@ 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="[]" + data-symfony--ux-google-map--map-rectangles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt new file mode 100644 index 00000000000..f0a24c243b4 --- /dev/null +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -0,0 +1,15 @@ + +
\ 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 09aa85802d3..7712767637b 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 @@ -11,4 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" + data-symfony--ux-google-map--map-rectangles-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 4d705693924..120df29fa3d 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 { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; +import type { CircleDefinition, Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition, RectangleDefinition } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; -import type { CircleOptions, 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, PolylineOptions as RectangleOptions } 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; @@ -48,9 +48,13 @@ export default class extends AbstractMapController; }): L.Circle; protected doRemoveCircle(circle: L.Circle): void; + protected doCreateRectangle({ definition, }: { + definition: RectangleDefinition; + }): L.Rectangle; + protected doRemoveRectangle(rectangle: L.Rectangle): void; protected doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: L.Marker | L.Polygon | L.Polyline | L.Circle; + element: L.Marker | L.Polygon | L.Polyline | L.Circle | L.Rectangle; }): 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 dff15b2ce3d..8f5b188275f 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -14,6 +14,7 @@ class default_1 extends Controller { this.polygons = new Map(); this.polylines = new Map(); this.circles = new Map(); + this.rectangles = new Map(); this.infoWindows = []; this.isConnected = false; } @@ -24,6 +25,7 @@ class default_1 extends Controller { 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.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this)); this.map = this.doCreateMap({ center: this.hasCenterValue ? this.centerValue : null, zoom: this.hasZoomValue ? this.zoomValue : null, @@ -33,6 +35,7 @@ class default_1 extends Controller { this.polygonsValue.forEach((definition) => this.createPolygon({ definition })); this.polylinesValue.forEach((definition) => this.createPolyline({ definition })); this.circlesValue.forEach((definition) => this.createCircle({ definition })); + this.rectanglesValue.forEach((definition) => this.createRectangle({ definition })); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -42,6 +45,7 @@ class default_1 extends Controller { polygons: [...this.polygons.values()], polylines: [...this.polylines.values()], circles: [...this.circles.values()], + rectangles: [...this.rectangles.values()], infoWindows: this.infoWindows, }); this.isConnected = true; @@ -80,6 +84,12 @@ class default_1 extends Controller { } this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle); } + rectanglesValueChanged() { + if (!this.isConnected) { + return; + } + this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle); + } createDrawingFactory(type, draws, factory) { const eventBefore = `${type}:before-create`; const eventAfter = `${type}:after-create`; @@ -117,6 +127,7 @@ default_1.values = { polygons: Array, polylines: Array, circles: Array, + rectangles: Array, options: Object, }; @@ -228,6 +239,23 @@ class map_controller extends default_1 { doRemoveCircle(circle) { circle.remove(); } + doCreateRectangle({ definition, }) { + const { '@id': _id, bounds, title, infoWindow, rawOptions = {} } = definition; + const rectangle = L.rectangle([ + [bounds.southWest.lat, bounds.southWest.lng], + [bounds.northEast.lat, bounds.northEast.lng], + ], { ...rawOptions }).addTo(this.map); + if (title) { + rectangle.bindPopup(title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: rectangle }); + } + return rectangle; + } + doRemoveRectangle(rectangle) { + rectangle.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 96314e6ed13..01ef436d5ee 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -7,6 +7,7 @@ import type { Point, PolygonDefinition, PolylineDefinition, + RectangleDefinition, } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; @@ -19,6 +20,7 @@ import type { PolylineOptions as PolygonOptions, PolylineOptions, PopupOptions, + PolylineOptions as RectangleOptions, } from 'leaflet'; type MapOptions = Pick & { @@ -45,7 +47,9 @@ export default class extends AbstractMapController< PolylineOptions, L.Polyline, CircleOptions, - L.Circle + L.Circle, + RectangleOptions, + L.Rectangle > { declare map: L.Map; @@ -200,12 +204,40 @@ export default class extends AbstractMapController< circle.remove(); } + protected doCreateRectangle({ + definition, + }: { definition: RectangleDefinition }): L.Rectangle { + const { '@id': _id, bounds, title, infoWindow, rawOptions = {} } = definition; + + const rectangle = L.rectangle( + [ + [bounds.southWest.lat, bounds.southWest.lng], + [bounds.northEast.lat, bounds.northEast.lng], + ], + { ...rawOptions } + ).addTo(this.map); + + if (title) { + rectangle.bindPopup(title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: rectangle }); + } + + return rectangle; + } + + protected doRemoveRectangle(rectangle: L.Rectangle): void { + rectangle.remove(); + } + protected doCreateInfoWindow({ definition, element, }: { definition: InfoWindowWithoutPositionDefinition; - element: L.Marker | L.Polygon | L.Polyline | L.Circle; + element: L.Marker | L.Polygon | L.Polyline | L.Circle | L.Rectangle; }): L.Popup { const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; diff --git a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts index 680f6d3a718..f1489889d2e 100644 --- a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts +++ b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts @@ -36,17 +36,18 @@ describe('LeafletController', () => { beforeEach(() => { container = mountDOM(` -
diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php index f1561e3488e..07490459b89 100644 --- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php +++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php @@ -22,6 +22,7 @@ use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; use Symfony\UX\Map\Test\RendererTestCase; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; @@ -106,6 +107,25 @@ public static function provideTestRenderMap(): iterable ->addCircle(new Circle(center: new Point(1.1, 2.2), radius: 500, infoWindow: new InfoWindow(content: 'Circle'), id: 'circle2')), ]; + yield 'with rectangles and infoWindows' => [ + 'renderer' => new LeafletRenderer(new StimulusHelper(null), new UxIconRenderer(null)), + 'map' => (new Map()) + ->center(new Point(48.8566, 2.3522)) + ->zoom(12) + ->addRectangle(new Rectangle( + southWest: new Point(48.8566, 2.3522), + northEast: new Point(48.8566, 2.3522), + title: 'Rectangle', + id: 'rectangle1' + )) + ->addRectangle(new Rectangle( + southWest: new Point(1.1, 2.2), + northEast: new Point(3.3, 4.4), + infoWindow: new InfoWindow(content: 'Rectangle'), + id: 'rectangle2' + )), + ]; + 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 9e43016115b..0001322a7e8 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 7e2de5e6716..6d5492e998d 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 7e2de5e6716..6d5492e998d 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 index 11da769576b..2329e462720 100644 --- 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[{"center":{"lat":48.8566,"lng":2.3522},"radius":1000000,"title":"Paris","infoWindow":null,"extra":[],"id":"circle1"},{"center":{"lat":1.1,"lng":2.2},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"circle2"}]" + data-symfony--ux-leaflet-map--map-rectangles-value="[]" > \ 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 119080dae97..55933034c81 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 @@ -11,5 +11,6 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 63832d46d80..bedb660dad4 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 ac12e7b2d75..c3cbc4e6388 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 @@ -11,4 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 1bad78f87b8..6499703a408 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 @@ -11,4 +11,5 @@ 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="[]" + data-symfony--ux-leaflet-map--map-rectangles-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 cd7fe232df0..176a7c6924f 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 @@ -11,4 +11,5 @@ 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="[]" + data-symfony--ux-leaflet-map--map-rectangles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt new file mode 100644 index 00000000000..fac21b6fbd2 --- /dev/null +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -0,0 +1,15 @@ + +
\ No newline at end of file diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php index cee3730284f..340399e8c93 100644 --- a/src/Map/src/Map.php +++ b/src/Map/src/Map.php @@ -24,12 +24,14 @@ final class Map private Polygons $polygons; private Polylines $polylines; private Circles $circles; + private Rectangles $rectangles; /** - * @param Marker[] $markers - * @param Polygon[] $polygons - * @param Polyline[] $polylines - * @param Circle[] $circles + * @param Marker[] $markers + * @param Polygon[] $polygons + * @param Polyline[] $polylines + * @param Circle[] $circles + * @param Rectangles[] $rectangles */ public function __construct( private readonly ?string $rendererName = null, @@ -41,11 +43,13 @@ public function __construct( array $polygons = [], array $polylines = [], array $circles = [], + array $rectangles = [], ) { $this->markers = new Markers($markers); $this->polygons = new Polygons($polygons); $this->polylines = new Polylines($polylines); $this->circles = new Circles($circles); + $this->rectangles = new Rectangles($rectangles); } public function getRendererName(): ?string @@ -147,6 +151,20 @@ public function removeCircle(Circle|string $circleOrId): self return $this; } + public function addRectangle(Rectangle $rectangle): self + { + $this->rectangles->add($rectangle); + + return $this; + } + + public function removeRectangle(Rectangle|string $rectangleOrId): self + { + $this->rectangles->remove($rectangleOrId); + + return $this; + } + public function toArray(): array { if (!$this->fitBoundsToMarkers) { @@ -168,6 +186,7 @@ public function toArray(): array 'polygons' => $this->polygons->toArray(), 'polylines' => $this->polylines->toArray(), 'circles' => $this->circles->toArray(), + 'rectangles' => $this->rectangles->toArray(), ]; } @@ -179,6 +198,7 @@ public function toArray(): array * polygons?: list, * polylines?: list, * circles?: list, + * rectangles?: list, * fitBoundsToMarkers?: bool, * options?: array, * } $map @@ -219,6 +239,12 @@ public static function fromArray(array $map): self } $map['circles'] = array_map(Circle::fromArray(...), $map['circles']); + $map['rectangles'] ??= []; + if (!\is_array($map['rectangles'])) { + throw new InvalidArgumentException('The "rectangles" parameter must be an array.'); + } + $map['rectangles'] = array_map(Rectangle::fromArray(...), $map['rectangles']); + return new self(...$map); } } diff --git a/src/Map/src/Rectangle.php b/src/Map/src/Rectangle.php new file mode 100644 index 00000000000..49525e0bb0c --- /dev/null +++ b/src/Map/src/Rectangle.php @@ -0,0 +1,85 @@ + + * + * 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 rectangle on a map defined by two points: the south-west and north-east corners. + * + * @author Valmont Pehaut-Pietri + */ +final class Rectangle implements Element +{ + public function __construct( + public readonly Point $southWest, + public readonly Point $northEast, + public readonly ?string $title = null, + public readonly ?InfoWindow $infoWindow = null, + public readonly array $extra = [], + public readonly ?string $id = null, + ) { + } + + /** + * Convert the rectangle to an array representation. + * + * @return array{ + * bounds: array{southWest: array{lat: float, lng: float}, northEast: array{lat: float, lng: float}}, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * id: string|null + * } + */ + public function toArray(): array + { + return [ + 'bounds' => [ + 'southWest' => $this->southWest->toArray(), + 'northEast' => $this->northEast->toArray(), + ], + 'title' => $this->title, + 'infoWindow' => $this->infoWindow?->toArray(), + 'extra' => $this->extra, + 'id' => $this->id, + ]; + } + + /** + * @param array{ + * southWest: array{lat: float, lng: float}, + * northEast: array{lat: float, lng: float}, + * title?: string|null, + * infoWindow?: array|null, + * extra?: array, + * id?: string|null + * } $rectangle + * + * @internal + */ + public static function fromArray(array $rectangle): self + { + if (!isset($rectangle['southWest'], $rectangle['northEast'])) { + throw new InvalidArgumentException('The points "southWest" and "northEast" are required.'); + } + + $rectangle['southWest'] = Point::fromArray($rectangle['southWest']); + $rectangle['northEast'] = Point::fromArray($rectangle['northEast']); + + if (isset($rectangle['infoWindow'])) { + $rectangle['infoWindow'] = InfoWindow::fromArray($rectangle['infoWindow']); + } + + return new self(...$rectangle); + } +} diff --git a/src/Map/src/Rectangles.php b/src/Map/src/Rectangles.php new file mode 100644 index 00000000000..7ef1b18ed19 --- /dev/null +++ b/src/Map/src/Rectangles.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 Rectangle collection. + * + * @author Valmont Pehaut-Pietri + * + * @internal + */ +final class Rectangles extends Elements +{ + public static function fromArray(array $elements): self + { + $elementObjects = []; + + foreach ($elements as $element) { + $elementObjects[] = Rectangle::fromArray($element); + } + + return new self(elements: $elementObjects); + } +} diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php index dad69e5172d..bc8d280384b 100644 --- a/src/Map/tests/MapTest.php +++ b/src/Map/tests/MapTest.php @@ -20,6 +20,7 @@ use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; class MapTest extends TestCase { @@ -70,6 +71,7 @@ public function testZoomAndCenterCanBeOmittedIfFitBoundsToMarkers(): void 'polygons' => [], 'polylines' => [], 'circles' => [], + 'rectangles' => [], ], $array); } @@ -91,6 +93,7 @@ public function testWithMinimumConfiguration(): void 'polygons' => [], 'polylines' => [], 'circles' => [], + 'rectangles' => [], ], $array); } @@ -190,6 +193,30 @@ public function testWithMaximumConfiguration(): void autoClose: true, ), )) + ->addRectangle(new Rectangle( + southWest: new Point(48.853, 2.3499), + northEast: new Point(48.8566, 2.3522), + title: 'Rectangle around Paris', + infoWindow: new InfoWindow( + headerContent: 'Rectangle around Paris', + content: 'A rectangle around Paris.', + position: new Point(48.8566, 2.3522), + opened: true, + autoClose: true, + ), + )) + ->addRectangle(new Rectangle( + southWest: new Point(45.75, 4.85), + northEast: new Point(45.77, 4.82), + title: 'Rectangle around Lyon', + infoWindow: new InfoWindow( + headerContent: 'Rectangle around Lyon', + content: 'A rectangle around Lyon.', + position: new Point(45.764, 4.8357), + opened: true, + autoClose: true, + ), + )) ; self::assertEquals([ @@ -342,6 +369,42 @@ public function testWithMaximumConfiguration(): void 'id' => null, ], ], + 'rectangles' => [ + [ + 'bounds' => [ + 'southWest' => ['lat' => 48.853, 'lng' => 2.3499], + 'northEast' => ['lat' => 48.8566, 'lng' => 2.3522], + ], + 'title' => 'Rectangle around Paris', + 'infoWindow' => [ + 'headerContent' => 'Rectangle around Paris', + 'content' => 'A rectangle around Paris.', + 'position' => ['lat' => 48.8566, 'lng' => 2.3522], + 'opened' => true, + 'autoClose' => true, + 'extra' => [], + ], + 'extra' => [], + 'id' => null, + ], + [ + 'bounds' => [ + 'southWest' => ['lat' => 45.75, 'lng' => 4.85], + 'northEast' => ['lat' => 45.77, 'lng' => 4.82], + ], + 'title' => 'Rectangle around Lyon', + 'infoWindow' => [ + 'headerContent' => 'Rectangle around Lyon', + 'content' => 'A rectangle around Lyon.', + 'position' => ['lat' => 45.764, 'lng' => 4.8357], + 'opened' => true, + 'autoClose' => true, + 'extra' => [], + ], + 'extra' => [], + 'id' => null, + ], + ], ], $map->toArray()); } } diff --git a/src/Map/tests/RectangleTest.php b/src/Map/tests/RectangleTest.php new file mode 100644 index 00000000000..2ad38ea1858 --- /dev/null +++ b/src/Map/tests/RectangleTest.php @@ -0,0 +1,92 @@ + + * + * 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\Exception\InvalidArgumentException; +use Symfony\UX\Map\InfoWindow; +use Symfony\UX\Map\Point; +use Symfony\UX\Map\Rectangle; + +class RectangleTest extends TestCase +{ + public function testToArray() + { + $infoWindow = new InfoWindow('Hello'); + + $southWest = new Point(1.0, 2.0); + $northEast = new Point(3.0, 4.0); + + $rectangle = new Rectangle($southWest, $northEast, 'Test Rectangle', $infoWindow, ['foo' => 'bar'], 'rect1'); + + $array = $rectangle->toArray(); + self::assertSame([ + 'bounds' => [ + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], + ], + 'title' => 'Test Rectangle', + 'infoWindow' => $infoWindow->toArray(), + 'extra' => ['foo' => 'bar'], + 'id' => 'rect1', + ], $array); + } + + public function testFromArray() + { + $data = [ + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], + 'title' => 'Test Rectangle', + 'infoWindow' => ['content' => 'Hello'], + 'extra' => ['foo' => 'bar'], + 'id' => 'rect1', + ]; + + $rectangle = Rectangle::fromArray($data); + + $array = $rectangle->toArray(); + self::assertSame([ + 'bounds' => [ + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], + ], + 'title' => 'Test Rectangle', + 'infoWindow' => [ + 'headerContent' => null, + 'content' => 'Hello', + 'position' => null, + 'opened' => false, + 'autoClose' => true, + 'extra' => $array['infoWindow']['extra'], + ], + 'extra' => ['foo' => 'bar'], + 'id' => 'rect1', + ], $array); + } + + public function testFromArrayThrowsExceptionIfSouthWestMissing() + { + $this->expectException(InvalidArgumentException::class); + Rectangle::fromArray([ + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], + ]); + } + + public function testFromArrayThrowsExceptionIfNorthEastMissing() + { + $this->expectException(InvalidArgumentException::class); + Rectangle::fromArray([ + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + ]); + } +} From 649a84c6736758e5266cdbdf7e7f8d395869705c Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sun, 22 Jun 2025 15:58:29 +0200 Subject: [PATCH 2/4] [Map] Remove "bounds" higher key from normalized `Rectangle` --- .../assets/dist/abstract_map_controller.d.ts | 6 ++--- src/Map/assets/src/abstract_map_controller.ts | 3 ++- .../Google/assets/dist/map_controller.js | 4 ++-- .../Google/assets/src/map_controller.ts | 4 ++-- ...set with rectangles and infoWindows__1.txt | 2 +- .../Leaflet/assets/dist/map_controller.js | 6 ++--- .../Leaflet/assets/src/map_controller.ts | 6 ++--- ...set with rectangles and infoWindows__1.txt | 2 +- src/Map/src/Rectangle.php | 22 +++++++++++-------- src/Map/tests/MapTest.php | 12 ++++------ src/Map/tests/RectangleTest.php | 14 +++++------- 11 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index dfe93968e41..67d5c70c9f0 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -58,10 +58,8 @@ export type CircleDefinition = WithIdentifier< }>; export type RectangleDefinition = WithIdentifier<{ infoWindow?: InfoWindowWithoutPositionDefinition; - bounds: { - northEast: Point; - southWest: Point; - }; + southWest: Point; + northEast: Point; title: string | null; rawOptions?: RectangleOptions; extra: Record; diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index 27d0138a1ef..e81e267171a 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -100,7 +100,8 @@ export type CircleDefinition = WithIdentifier< export type RectangleDefinition = WithIdentifier<{ infoWindow?: InfoWindowWithoutPositionDefinition; - bounds: { northEast: Point; southWest: Point }; + southWest: Point; + northEast: Point; title: string | null; /** * Raw options passed to the rectangle constructor, specific to the map provider (e.g.: `L.rectangle()` for Leaflet). 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 74f38869f28..0675c32da5e 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -276,10 +276,10 @@ class map_controller extends default_1 { circle.setMap(null); } doCreateRectangle({ definition, }) { - const { bounds, title, infoWindow, rawOptions = {} } = definition; + const { northEast, southWest, title, infoWindow, rawOptions = {} } = definition; const rectangle = new _google.maps.Rectangle({ ...rawOptions, - bounds: new _google.maps.LatLngBounds(bounds.southWest, bounds.northEast), + bounds: new _google.maps.LatLngBounds(southWest, northEast), map: this.map, }); if (title) { 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 a203eb53445..5e6b13b06c2 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -283,11 +283,11 @@ export default class extends AbstractMapController< }: { definition: RectangleDefinition; }): google.maps.Rectangle { - const { bounds, title, infoWindow, rawOptions = {} } = definition; + const { northEast, southWest, title, infoWindow, rawOptions = {} } = definition; const rectangle = new _google.maps.Rectangle({ ...rawOptions, - bounds: new _google.maps.LatLngBounds(bounds.southWest, bounds.northEast), + bounds: new _google.maps.LatLngBounds(southWest, northEast), map: this.map, }); diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt index f0a24c243b4..b29a5790c73 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -11,5 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" - data-symfony--ux-google-map--map-rectangles-value="[{"bounds":{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522}},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null},{"bounds":{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4}},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null}]" + data-symfony--ux-google-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null}]" > \ No newline at end of file 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 8f5b188275f..eda5299743b 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -240,10 +240,10 @@ class map_controller extends default_1 { circle.remove(); } doCreateRectangle({ definition, }) { - const { '@id': _id, bounds, title, infoWindow, rawOptions = {} } = definition; + const { '@id': _id, southWest, northEast, title, infoWindow, rawOptions = {} } = definition; const rectangle = L.rectangle([ - [bounds.southWest.lat, bounds.southWest.lng], - [bounds.northEast.lat, bounds.northEast.lng], + [southWest.lat, southWest.lng], + [northEast.lat, northEast.lng], ], { ...rawOptions }).addTo(this.map); if (title) { rectangle.bindPopup(title); 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 01ef436d5ee..277cde10c6a 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -207,12 +207,12 @@ export default class extends AbstractMapController< protected doCreateRectangle({ definition, }: { definition: RectangleDefinition }): L.Rectangle { - const { '@id': _id, bounds, title, infoWindow, rawOptions = {} } = definition; + const { '@id': _id, southWest, northEast, title, infoWindow, rawOptions = {} } = definition; const rectangle = L.rectangle( [ - [bounds.southWest.lat, bounds.southWest.lng], - [bounds.northEast.lat, bounds.northEast.lng], + [southWest.lat, southWest.lng], + [northEast.lat, northEast.lng], ], { ...rawOptions } ).addTo(this.map); diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt index fac21b6fbd2..909b535d341 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -11,5 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" - data-symfony--ux-leaflet-map--map-rectangles-value="[{"bounds":{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522}},"title":"Rectangle","infoWindow":null,"extra":[],"id":"rectangle1"},{"bounds":{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4}},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"rectangle2"}]" + data-symfony--ux-leaflet-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":"Rectangle","infoWindow":null,"extra":[],"id":"rectangle1"},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"rectangle2"}]" > \ No newline at end of file diff --git a/src/Map/src/Rectangle.php b/src/Map/src/Rectangle.php index 49525e0bb0c..981460976ef 100644 --- a/src/Map/src/Rectangle.php +++ b/src/Map/src/Rectangle.php @@ -34,7 +34,8 @@ public function __construct( * Convert the rectangle to an array representation. * * @return array{ - * bounds: array{southWest: array{lat: float, lng: float}, northEast: array{lat: float, lng: float}}, + * southWest: array{lat: float, lng: float}, + * northEast: array{lat: float, lng: float}, * title: string|null, * infoWindow: array|null, * extra: array, @@ -44,10 +45,8 @@ public function __construct( public function toArray(): array { return [ - 'bounds' => [ - 'southWest' => $this->southWest->toArray(), - 'northEast' => $this->northEast->toArray(), - ], + 'southWest' => $this->southWest->toArray(), + 'northEast' => $this->northEast->toArray(), 'title' => $this->title, 'infoWindow' => $this->infoWindow?->toArray(), 'extra' => $this->extra, @@ -57,8 +56,8 @@ public function toArray(): array /** * @param array{ - * southWest: array{lat: float, lng: float}, - * northEast: array{lat: float, lng: float}, + * southWest?: array{lat: float, lng: float}, + * northEast?: array{lat: float, lng: float}, * title?: string|null, * infoWindow?: array|null, * extra?: array, @@ -69,11 +68,16 @@ public function toArray(): array */ public static function fromArray(array $rectangle): self { - if (!isset($rectangle['southWest'], $rectangle['northEast'])) { - throw new InvalidArgumentException('The points "southWest" and "northEast" are required.'); + if (!isset($rectangle['southWest'])) { + throw new InvalidArgumentException('The "southWest" point is required to create a Rectangle.'); } $rectangle['southWest'] = Point::fromArray($rectangle['southWest']); + + if (!isset($rectangle['northEast'])) { + throw new InvalidArgumentException('The "northEast" point is required to create a Rectangle.'); + } + $rectangle['northEast'] = Point::fromArray($rectangle['northEast']); if (isset($rectangle['infoWindow'])) { diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php index bc8d280384b..06d09a24d4c 100644 --- a/src/Map/tests/MapTest.php +++ b/src/Map/tests/MapTest.php @@ -371,10 +371,8 @@ public function testWithMaximumConfiguration(): void ], 'rectangles' => [ [ - 'bounds' => [ - 'southWest' => ['lat' => 48.853, 'lng' => 2.3499], - 'northEast' => ['lat' => 48.8566, 'lng' => 2.3522], - ], + 'southWest' => ['lat' => 48.853, 'lng' => 2.3499], + 'northEast' => ['lat' => 48.8566, 'lng' => 2.3522], 'title' => 'Rectangle around Paris', 'infoWindow' => [ 'headerContent' => 'Rectangle around Paris', @@ -388,10 +386,8 @@ public function testWithMaximumConfiguration(): void 'id' => null, ], [ - 'bounds' => [ - 'southWest' => ['lat' => 45.75, 'lng' => 4.85], - 'northEast' => ['lat' => 45.77, 'lng' => 4.82], - ], + 'southWest' => ['lat' => 45.75, 'lng' => 4.85], + 'northEast' => ['lat' => 45.77, 'lng' => 4.82], 'title' => 'Rectangle around Lyon', 'infoWindow' => [ 'headerContent' => 'Rectangle around Lyon', diff --git a/src/Map/tests/RectangleTest.php b/src/Map/tests/RectangleTest.php index 2ad38ea1858..60517f8bfc6 100644 --- a/src/Map/tests/RectangleTest.php +++ b/src/Map/tests/RectangleTest.php @@ -30,10 +30,8 @@ public function testToArray() $array = $rectangle->toArray(); self::assertSame([ - 'bounds' => [ - 'southWest' => ['lat' => 1.0, 'lng' => 2.0], - 'northEast' => ['lat' => 3.0, 'lng' => 4.0], - ], + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], 'title' => 'Test Rectangle', 'infoWindow' => $infoWindow->toArray(), 'extra' => ['foo' => 'bar'], @@ -56,10 +54,8 @@ public function testFromArray() $array = $rectangle->toArray(); self::assertSame([ - 'bounds' => [ - 'southWest' => ['lat' => 1.0, 'lng' => 2.0], - 'northEast' => ['lat' => 3.0, 'lng' => 4.0], - ], + 'southWest' => ['lat' => 1.0, 'lng' => 2.0], + 'northEast' => ['lat' => 3.0, 'lng' => 4.0], 'title' => 'Test Rectangle', 'infoWindow' => [ 'headerContent' => null, @@ -77,6 +73,7 @@ public function testFromArray() public function testFromArrayThrowsExceptionIfSouthWestMissing() { $this->expectException(InvalidArgumentException::class); + Rectangle::fromArray([ 'northEast' => ['lat' => 3.0, 'lng' => 4.0], ]); @@ -85,6 +82,7 @@ public function testFromArrayThrowsExceptionIfSouthWestMissing() public function testFromArrayThrowsExceptionIfNorthEastMissing() { $this->expectException(InvalidArgumentException::class); + Rectangle::fromArray([ 'southWest' => ['lat' => 1.0, 'lng' => 2.0], ]); From 503d295c682aaf33a7841a851c316cdc039d4433 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sun, 22 Jun 2025 16:44:51 +0200 Subject: [PATCH 3/4] [Map] Fix usage of `Circle` and `Rectangle` with LiveComponent, by computing `@id` is expected --- ...ta set with circles and infoWindows__1.txt | 2 +- ...set with rectangles and infoWindows__1.txt | 2 +- ...ta set with circles and infoWindows__1.txt | 2 +- ...set with rectangles and infoWindows__1.txt | 2 +- src/Map/src/Renderer/AbstractRenderer.php | 8 ++++ src/Map/src/Test/RendererTestCase.php | 43 +++++++++++++++++++ 6 files changed, 55 insertions(+), 4 deletions(-) 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 index a1f6d7a4baa..c7a07df8f64 100644 --- 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 @@ -10,6 +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="[{"center":{"lat":48.8566,"lng":2.3522},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null},{"center":{"lat":1.1,"lng":2.2},"radius":1000,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null}]" + data-symfony--ux-google-map--map-circles-value="[{"center":{"lat":48.8566,"lng":2.3522},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"3115517621c88996"},{"center":{"lat":1.1,"lng":2.2},"radius":1000,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"71febe272ee9a265"}]" data-symfony--ux-google-map--map-rectangles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt index b29a5790c73..f29709a8f4a 100644 --- a/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt +++ b/src/Map/src/Bridge/Google/tests/__snapshots__/GoogleRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -11,5 +11,5 @@ data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" data-symfony--ux-google-map--map-circles-value="[]" - data-symfony--ux-google-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null}]" + data-symfony--ux-google-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"1b754ce32860016a"},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":null,"@id":"d98c12855ec90699"}]" > \ 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 index 2329e462720..e6bbccceadd 100644 --- 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 @@ -10,6 +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="[{"center":{"lat":48.8566,"lng":2.3522},"radius":1000000,"title":"Paris","infoWindow":null,"extra":[],"id":"circle1"},{"center":{"lat":1.1,"lng":2.2},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"circle2"}]" + data-symfony--ux-leaflet-map--map-circles-value="[{"center":{"lat":48.8566,"lng":2.3522},"radius":1000000,"title":"Paris","infoWindow":null,"extra":[],"id":"circle1","@id":"b12929e5dc57d558"},{"center":{"lat":1.1,"lng":2.2},"radius":500,"title":null,"infoWindow":{"headerContent":null,"content":"Circle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"circle2","@id":"4953b5cc1d5759f4"}]" data-symfony--ux-leaflet-map--map-rectangles-value="[]" > \ No newline at end of file diff --git a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt index 909b535d341..b85e1a42630 100644 --- a/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt +++ b/src/Map/src/Bridge/Leaflet/tests/__snapshots__/LeafletRendererTest__testRenderMap with data set with rectangles and infoWindows__1.txt @@ -11,5 +11,5 @@ data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" data-symfony--ux-leaflet-map--map-circles-value="[]" - data-symfony--ux-leaflet-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":"Rectangle","infoWindow":null,"extra":[],"id":"rectangle1"},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"rectangle2"}]" + data-symfony--ux-leaflet-map--map-rectangles-value="[{"southWest":{"lat":48.8566,"lng":2.3522},"northEast":{"lat":48.8566,"lng":2.3522},"title":"Rectangle","infoWindow":null,"extra":[],"id":"rectangle1","@id":"4cde1a021a127686"},{"southWest":{"lat":1.1,"lng":2.2},"northEast":{"lat":3.3,"lng":4.4},"title":null,"infoWindow":{"headerContent":null,"content":"Rectangle","position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"id":"rectangle2","@id":"2549f5a73ef6bee7"}]" > \ No newline at end of file diff --git a/src/Map/src/Renderer/AbstractRenderer.php b/src/Map/src/Renderer/AbstractRenderer.php index ad3ba4334b1..b56a53489b9 100644 --- a/src/Map/src/Renderer/AbstractRenderer.php +++ b/src/Map/src/Renderer/AbstractRenderer.php @@ -108,6 +108,14 @@ private function getMapAttributes(Map $map): array $attrs['polylines'][$key]['@id'] = $computeId($polyline); } + foreach ($attrs['circles'] as $key => $circle) { + $attrs['circles'][$key]['@id'] = $computeId($circle); + } + + foreach ($attrs['rectangles'] as $key => $rectangle) { + $attrs['rectangles'][$key]['@id'] = $computeId($rectangle); + } + return $attrs; } } diff --git a/src/Map/src/Test/RendererTestCase.php b/src/Map/src/Test/RendererTestCase.php index 6e8ff909fef..acb9685f2aa 100644 --- a/src/Map/src/Test/RendererTestCase.php +++ b/src/Map/src/Test/RendererTestCase.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Spatie\Snapshots\MatchesSnapshots; +use Symfony\UX\Map\Elements; use Symfony\UX\Map\Map; use Symfony\UX\Map\Renderer\RendererInterface; @@ -38,6 +39,7 @@ public function testRenderMap(RendererInterface $renderer, Map $map, array $attr $rendered = $renderer->renderMap($map, $attributes); $rendered = $this->prettify($rendered); + $this->assertElementsHaveComputedId($rendered); $this->assertMatchesSnapshot($rendered); } @@ -50,4 +52,45 @@ private function prettify(string $html): string return $html; } + + private function assertElementsHaveComputedId(string $html): void + { + // Extract the `Elements` properties from the Map class + static $elementsProperties = null; + if (null === $elementsProperties) { + $elementsProperties = []; + $reflMap = new \ReflectionClass(Map::class); + + foreach ($reflMap->getProperties() as $prop) { + if (is_a($prop->getType()->getName(), Elements::class, true)) { + $elementsProperties[] = $prop->getName(); + } + } + } + + // Parse the rendered HTML and extract `data--value` attributes + /** @var array $htmlAttributes */ + $htmlAttributes = []; + foreach ($elementsProperties as $property) { + $matchesResult = preg_match(\sprintf('/data-symfony--ux-[a-z-]+-map-%s-value="([^"]+)"/', preg_quote($property, '/')), $html, $matches); + if (false === $matchesResult) { + throw new \LogicException(\sprintf('Failed to parse the rendered HTML for property "%s".', $property)); + } elseif (0 === $matchesResult) { + throw new \LogicException(\sprintf('It looks like the property "%s" is missing from "Map::toArray()" normalization.', $property)); + } else { + $htmlAttributes[$property] = $matches[1]; + } + } + + // Check that each property has a computed "@id" attribute + foreach ($htmlAttributes as $property => $value) { + if ('[]' === $value) { + continue; + } + + if (!str_contains($value, '@id') || str_contains($value, '"@id":null')) { + throw new \LogicException(\sprintf('It looks like the "AbstractRendered::getMapAttributes()" has not been updated to compute "@id" attribute of "%s" property.', $property)); + } + } + } } From 93c31b00df0a811016d3b6f5eb0e426b9a479e0f Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Sun, 22 Jun 2025 16:53:33 +0200 Subject: [PATCH 4/4] [Map] Fix `` and `ux_icon()` (without `Map` instance) for `Circle` and `Rectangle` instances --- src/Map/src/Twig/MapRuntime.php | 22 +++++++++++++++++----- src/Map/src/Twig/UXMapComponent.php | 12 ++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php index a729e46e31c..05690e7b082 100644 --- a/src/Map/src/Twig/MapRuntime.php +++ b/src/Map/src/Twig/MapRuntime.php @@ -11,11 +11,13 @@ namespace Symfony\UX\Map\Twig; +use Symfony\UX\Map\Circle; use Symfony\UX\Map\Map; use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; use Symfony\UX\Map\Renderer\RendererInterface; use Twig\Extension\RuntimeExtensionInterface; @@ -36,19 +38,23 @@ public function __construct( * @param array $markers * @param array $polygons * @param array $polylines + * @param array $circles + * @param array $rectangles */ public function renderMap( ?Map $map = null, array $attributes = [], + ?array $center = null, + ?float $zoom = null, ?array $markers = null, ?array $polygons = null, ?array $polylines = null, - ?array $center = null, - ?float $zoom = null, + ?array $circles = null, + ?array $rectangles = null, ): string { if ($map instanceof Map) { - if (null !== $center || null !== $zoom || $markers) { - throw new \InvalidArgumentException('You cannot set "center", "markers" or "zoom" on an existing Map.'); + if (null !== $center || null !== $zoom || $markers || $polygons || $polylines || $circles || $rectangles) { + throw new \InvalidArgumentException('It is not allowed to pass both a Map object and other parameters (like "center", "zoom", "markers", etc...) to the "renderMap" method. Please use either a Map object or the individual parameters.'); } return $this->renderer->renderMap($map, $attributes); @@ -64,6 +70,12 @@ public function renderMap( foreach ($polylines ?? [] as $polyline) { $map->addPolyline(Polyline::fromArray($polyline)); } + foreach ($circles ?? [] as $circle) { + $map->addCircle(Circle::fromArray($circle)); + } + foreach ($rectangles ?? [] as $rectangle) { + $map->addRectangle(Rectangle::fromArray($rectangle)); + } if (null !== $center) { $map->center(Point::fromArray($center)); } @@ -76,7 +88,7 @@ public function renderMap( public function render(array $args = []): string { - $map = array_intersect_key($args, ['map' => 0, 'markers' => 1, 'polygons' => 2, 'polylines' => 3, 'center' => 4, 'zoom' => 5]); + $map = array_intersect_key($args, array_flip(['map', 'center', 'zoom', 'markers', 'polygons', 'polylines', 'circles', 'rectangles'])); $attributes = array_diff_key($args, $map); return $this->renderMap(...$map, attributes: $attributes); diff --git a/src/Map/src/Twig/UXMapComponent.php b/src/Map/src/Twig/UXMapComponent.php index 4c167420d52..0a20552e601 100644 --- a/src/Map/src/Twig/UXMapComponent.php +++ b/src/Map/src/Twig/UXMapComponent.php @@ -11,10 +11,12 @@ namespace Symfony\UX\Map\Twig; +use Symfony\UX\Map\Circle; use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; use Symfony\UX\Map\Polyline; +use Symfony\UX\Map\Rectangle; /** * @author Simon André @@ -41,4 +43,14 @@ final class UXMapComponent * @var Polyline[] */ public array $polylines; + + /** + * @var Circle[] + */ + public array $circles; + + /** + * @var Rectangle[] + */ + public array $rectangles; }