diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md index df7d6551c62..cdad7ae91dd 100644 --- a/src/Map/CHANGELOG.md +++ b/src/Map/CHANGELOG.md @@ -5,6 +5,7 @@ - Add method `Symfony\UX\Map\Renderer\AbstractRenderer::tapOptions()`, to allow Renderer to modify options before rendering a Map. - Add `ux_map.google_maps.default_map_id` configuration to set the Google ``Map ID`` - Add `ComponentWithMapTrait` to ease maps integration in [Live Components](https://symfony.com/bundles/ux-live-component/current/index.html) +- Add `Polyline` support ## 2.20 diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts index 382050b962a..7bfd532550e 100644 --- a/src/Map/assets/dist/abstract_map_controller.d.ts +++ b/src/Map/assets/dist/abstract_map_controller.d.ts @@ -19,6 +19,14 @@ export type PolygonDefinition = { rawOptions?: PolygonOptions; extra: Record; }; +export type PolylineDefinition = { + '@id': string; + infoWindow?: Omit, 'position'>; + points: Array; + title: string | null; + rawOptions?: PolylineOptions; + extra: Record; +}; export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -28,7 +36,7 @@ export type InfoWindowDefinition = { rawOptions?: InfoWindowOptions; extra: Record; }; -export default abstract class extends Controller { +export default abstract class extends Controller { static values: { providerOptions: ObjectConstructor; center: ObjectConstructor; @@ -36,6 +44,7 @@ export default abstract class>; polygonsValue: Array>; + polylinesValue: Array>; optionsValue: MapOptions; protected map: Map; protected markers: globalThis.Map; protected infoWindows: Array; protected polygons: globalThis.Map; + protected polylines: globalThis.Map; connect(): void; protected abstract doCreateMap({ center, zoom, options, }: { center: Point | null; @@ -58,10 +69,20 @@ export default abstract class): Marker; createPolygon(definition: PolygonDefinition): Polygon; + protected abstract removePolygon(polygon: Polygon): void; protected abstract doCreatePolygon(definition: PolygonDefinition): Polygon; + createPolyline(definition: PolylineDefinition): Polyline; + protected abstract removePolyline(polyline: Polyline): void; + protected abstract doCreatePolyline(definition: PolylineDefinition): Polyline; protected createInfoWindow({ definition, element, }: { - definition: MarkerDefinition['infoWindow'] | PolygonDefinition['infoWindow']; - element: Marker | Polygon; + definition: MarkerDefinition['infoWindow']; + element: Marker; + } | { + definition: PolygonDefinition['infoWindow']; + element: Polygon; + } | { + definition: PolylineDefinition['infoWindow']; + element: Polyline; }): InfoWindow; protected abstract doCreateInfoWindow({ definition, element, }: { definition: MarkerDefinition['infoWindow']; @@ -69,6 +90,9 @@ export default abstract class['infoWindow']; element: Polygon; + } | { + definition: PolylineDefinition['infoWindow']; + element: Polyline; }): InfoWindow; protected abstract doFitBoundsToMarkers(): void; protected abstract dispatchEvent(name: string, payload: Record): void; @@ -76,4 +100,5 @@ export default abstract class this.createMarker(marker)); this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); + this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -20,6 +22,7 @@ class default_1 extends Controller { map: this.map, markers: [...this.markers.values()], polygons: [...this.polygons.values()], + polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); } @@ -39,6 +42,14 @@ class default_1 extends Controller { this.polygons.set(definition['@id'], polygon); return polygon; } + createPolyline(definition) { + this.dispatchEvent('polyline:before-create', { definition }); + const polyline = this.doCreatePolyline(definition); + this.dispatchEvent('polyline:after-create', { polyline }); + polyline['@id'] = definition['@id']; + this.polylines.set(definition['@id'], polyline); + return polyline; + } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); @@ -71,7 +82,7 @@ class default_1 extends Controller { } this.polygons.forEach((polygon) => { if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - polygon.remove(); + this.removePolygon(polygon); this.polygons.delete(polygon['@id']); } }); @@ -81,6 +92,22 @@ class default_1 extends Controller { } }); } + polylinesValueChanged() { + if (!this.map) { + return; + } + this.polylines.forEach((polyline) => { + if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { + this.removePolyline(polyline); + this.polylines.delete(polyline['@id']); + } + }); + this.polylinesValue.forEach((polyline) => { + if (!this.polylines.has(polyline['@id'])) { + this.createPolyline(polyline); + } + }); + } } default_1.values = { providerOptions: Object, @@ -89,6 +116,7 @@ default_1.values = { fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, + polylines: Array, options: Object, }; diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts index 2d95f64a7f2..a1f9de68810 100644 --- a/src/Map/assets/src/abstract_map_controller.ts +++ b/src/Map/assets/src/abstract_map_controller.ts @@ -29,6 +29,15 @@ export type PolygonDefinition = { extra: Record; }; +export type PolylineDefinition = { + '@id': string; + infoWindow?: Omit, 'position'>; + points: Array; + title: string | null; + rawOptions?: PolylineOptions; + extra: Record; +}; + export type InfoWindowDefinition = { headerContent: string | null; content: string | null; @@ -58,6 +67,8 @@ export default abstract class< InfoWindow, PolygonOptions, Polygon, + PolylineOptions, + Polyline, > extends Controller { static values = { providerOptions: Object, @@ -66,6 +77,7 @@ export default abstract class< fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, + polylines: Array, options: Object, }; @@ -74,12 +86,14 @@ export default abstract class< declare fitBoundsToMarkersValue: boolean; declare markersValue: Array>; declare polygonsValue: Array>; + declare polylinesValue: Array>; declare optionsValue: MapOptions; protected map: Map; protected markers = new Map(); protected infoWindows: Array = []; protected polygons = new Map(); + protected polylines = new Map(); connect() { const options = this.optionsValue; @@ -92,6 +106,8 @@ export default abstract class< this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); + this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); + if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -100,6 +116,7 @@ export default abstract class< map: this.map, markers: [...this.markers.values()], polygons: [...this.polygons.values()], + polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); } @@ -142,17 +159,36 @@ export default abstract class< return polygon; } + protected abstract removePolygon(polygon: Polygon): void; + protected abstract doCreatePolygon(definition: PolygonDefinition): Polygon; + public createPolyline(definition: PolylineDefinition): Polyline { + this.dispatchEvent('polyline:before-create', { definition }); + const polyline = this.doCreatePolyline(definition); + this.dispatchEvent('polyline:after-create', { polyline }); + + polyline['@id'] = definition['@id']; + + this.polylines.set(definition['@id'], polyline); + + return polyline; + } + + protected abstract removePolyline(polyline: Polyline): void; + + protected abstract doCreatePolyline(definition: PolylineDefinition): Polyline; + protected createInfoWindow({ definition, element, - }: { - definition: - | MarkerDefinition['infoWindow'] - | PolygonDefinition['infoWindow']; - element: Marker | Polygon; - }): InfoWindow { + }: + | { definition: MarkerDefinition['infoWindow']; element: Marker } + | { definition: PolygonDefinition['infoWindow']; element: Polygon } + | { + definition: PolylineDefinition['infoWindow']; + element: Polyline; + }): InfoWindow { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); this.dispatchEvent('info-window:after-create', { infoWindow, element }); @@ -166,13 +202,11 @@ export default abstract class< definition, element, }: + | { definition: MarkerDefinition['infoWindow']; element: Marker } + | { definition: PolygonDefinition['infoWindow']; element: Polygon } | { - definition: MarkerDefinition['infoWindow']; - element: Marker; - } - | { - definition: PolygonDefinition['infoWindow']; - element: Polygon; + definition: PolylineDefinition['infoWindow']; + element: Polyline; }): InfoWindow; protected abstract doFitBoundsToMarkers(): void; @@ -213,7 +247,7 @@ export default abstract class< this.polygons.forEach((polygon) => { if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - polygon.remove(); + this.removePolygon(polygon); this.polygons.delete(polygon['@id']); } }); @@ -224,4 +258,23 @@ export default abstract class< } }); } + + public polylinesValueChanged(): void { + if (!this.map) { + return; + } + + this.polylines.forEach((polyline) => { + if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { + this.removePolyline(polyline); + this.polylines.delete(polyline['@id']); + } + }); + + this.polylinesValue.forEach((polyline) => { + if (!this.polylines.has(polyline['@id'])) { + this.createPolyline(polyline); + } + }); + } } diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts index 118241f0220..b3d2623a898 100644 --- a/src/Map/assets/test/abstract_map_controller.test.ts +++ b/src/Map/assets/test/abstract_map_controller.test.ts @@ -35,6 +35,15 @@ class MyMapController extends AbstractMapController { return polygon; } + doCreatePolyline(definition) { + const polyline = { polyline: 'polyline', title: definition.title }; + + if (definition.infoWindow) { + this.createInfoWindow({ definition: definition.infoWindow, element: polyline }); + } + return polyline; + } + doCreateInfoWindow({ definition, element }) { if (element.marker) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title }; @@ -42,6 +51,9 @@ class MyMapController extends AbstractMapController { if (element.polygon) { return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title }; } + if (element.polyline) { + return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polyline: element.title }; + } } doFitBoundsToMarkers() { @@ -70,6 +82,7 @@ describe('AbstractMapController', () => { data-map-options-value="{}" data-map-markers-value="[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":{"headerContent":"Paris","content":null,"position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"@id":"a69f13edd2e571f3"},{"position":{"lat":45.75,"lng":4.85},"title":"Lyon","infoWindow":{"headerContent":"Lyon","content":null,"position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"@id":"cb9c1a30d562694b"},{"position":{"lat":43.6047,"lng":1.4442},"title":"Toulouse","infoWindow":{"headerContent":"Toulouse","content":null,"position":null,"opened":false,"autoClose":true,"extra":[]},"extra":[],"@id":"e6b3acef1325fb52"}]" 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"}]" style="height: 600px" > `); @@ -79,7 +92,7 @@ describe('AbstractMapController', () => { clearDOM(); }); - it('connect and create map, marker, polygon and info window', async () => { + it('connect and create map, marker, polygon, polyline and info window', async () => { const div = getByTestId(container, 'map'); expect(div).not.toHaveClass('connected'); @@ -101,6 +114,9 @@ describe('AbstractMapController', () => { ['9874334e4e8caa16', { '@id': '9874334e4e8caa16', polygon: 'polygon', title: null }], ]) ); + expect(controller.polylines).toEqual( + new Map([['0fa955da866c7720', { '@id': '0fa955da866c7720', polyline: 'polyline', title: null }]]) + ); expect(controller.infoWindows).toEqual([ { headerContent: 'Paris', @@ -122,6 +138,11 @@ describe('AbstractMapController', () => { infoWindow: 'infoWindow', polygon: null, }, + { + headerContent: 'Polyline', + infoWindow: 'infoWindow', + polyline: null, + }, ]); }); }); diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst index 8274a99ad5a..0801f95f859 100644 --- a/src/Map/doc/index.rst +++ b/src/Map/doc/index.rst @@ -153,6 +153,24 @@ You can also add Polygons, which represents an area enclosed by a series of ``Po ), )); +Add Polylines +~~~~~~~~~~~~~ + +You can add Polylines, which represents a path made by a series of ``Point`` instances:: + + $myMap->addPolyline(new Polyline( + points: [ + new Point(48.8566, 2.3522), + new Point(45.7640, 4.8357), + new Point(43.2965, 5.3698), + new Point(44.8378, -0.5792), + ], + infoWindow: new InfoWindow( + content: 'A line passing through Paris, Lyon, Marseille, Bordeaux', + ), + )); + + Render a 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 110c2ff7f14..2ce6fcc0bad 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 AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; import type { LoaderOptions } from '@googlemaps/js-api-loader'; type MapOptions = Pick; -export default class extends AbstractMapController { +export default class extends AbstractMapController { providerOptionsValue: Pick; connect(): Promise; protected dispatchEvent(name: string, payload?: Record): void; @@ -14,12 +14,18 @@ export default class extends AbstractMapController): google.maps.marker.AdvancedMarkerElement; protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void; protected doCreatePolygon(definition: PolygonDefinition): google.maps.Polygon; + protected removePolygon(polygon: google.maps.Polygon): void; + protected doCreatePolyline(definition: PolylineDefinition): google.maps.Polyline; + protected removePolyline(polyline: google.maps.Polyline): void; protected doCreateInfoWindow({ definition, element, }: { definition: MarkerDefinition['infoWindow']; element: google.maps.marker.AdvancedMarkerElement; } | { definition: PolygonDefinition['infoWindow']; element: google.maps.Polygon; + } | { + definition: PolylineDefinition['infoWindow']; + element: google.maps.Polyline; }): google.maps.InfoWindow; private createTextOrElement; private closeInfoWindowsExcept; 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 4b199e56958..4307c67af49 100644 --- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js @@ -7,6 +7,7 @@ class default_1 extends Controller { this.markers = new Map(); this.infoWindows = []; this.polygons = new Map(); + this.polylines = new Map(); } connect() { const options = this.optionsValue; @@ -14,6 +15,7 @@ class default_1 extends Controller { this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); this.markersValue.forEach((marker) => this.createMarker(marker)); this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); + this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -21,6 +23,7 @@ class default_1 extends Controller { map: this.map, markers: [...this.markers.values()], polygons: [...this.polygons.values()], + polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); } @@ -40,6 +43,14 @@ class default_1 extends Controller { this.polygons.set(definition['@id'], polygon); return polygon; } + createPolyline(definition) { + this.dispatchEvent('polyline:before-create', { definition }); + const polyline = this.doCreatePolyline(definition); + this.dispatchEvent('polyline:after-create', { polyline }); + polyline['@id'] = definition['@id']; + this.polylines.set(definition['@id'], polyline); + return polyline; + } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); @@ -72,7 +83,7 @@ class default_1 extends Controller { } this.polygons.forEach((polygon) => { if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - polygon.remove(); + this.removePolygon(polygon); this.polygons.delete(polygon['@id']); } }); @@ -82,6 +93,22 @@ class default_1 extends Controller { } }); } + polylinesValueChanged() { + if (!this.map) { + return; + } + this.polylines.forEach((polyline) => { + if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { + this.removePolyline(polyline); + this.polylines.delete(polyline['@id']); + } + }); + this.polylinesValue.forEach((polyline) => { + if (!this.polylines.has(polyline['@id'])) { + this.createPolyline(polyline); + } + }); + } } default_1.values = { providerOptions: Object, @@ -90,6 +117,7 @@ default_1.values = { fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, + polylines: Array, options: Object, }; @@ -166,6 +194,27 @@ class map_controller extends default_1 { } return polygon; } + removePolygon(polygon) { + polygon.setMap(null); + } + doCreatePolyline(definition) { + const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; + const polyline = new _google.maps.Polyline({ + ...rawOptions, + path: points, + map: this.map, + }); + if (title) { + polyline.set('title', title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: polyline }); + } + return polyline; + } + removePolyline(polyline) { + polyline.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 6aa9ff8b1f7..df5e843c1d5 100644 --- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts @@ -8,7 +8,7 @@ */ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; import type { LoaderOptions } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader'; @@ -38,7 +38,9 @@ export default class extends AbstractMapController< google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, - google.maps.Polygon + google.maps.Polygon, + google.maps.PolylineOptions, + google.maps.Polyline > { declare providerOptionsValue: Pick< LoaderOptions, @@ -153,6 +155,36 @@ export default class extends AbstractMapController< return polygon; } + protected removePolygon(polygon: google.maps.Polygon) { + polygon.setMap(null); + } + + protected doCreatePolyline( + definition: PolylineDefinition + ): google.maps.Polyline { + const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; + + const polyline = new _google.maps.Polyline({ + ...rawOptions, + path: points, + map: this.map, + }); + + if (title) { + polyline.set('title', title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: polyline }); + } + + return polyline; + } + + protected removePolyline(polyline: google.maps.Polyline): void { + polyline.setMap(null); + } + protected doCreateInfoWindow({ definition, element, @@ -167,6 +199,10 @@ export default class extends AbstractMapController< | { definition: PolygonDefinition['infoWindow']; element: google.maps.Polygon; + } + | { + definition: PolylineDefinition['infoWindow']; + element: google.maps.Polyline; }): google.maps.InfoWindow { const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition; 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 76118c0511b..edee670cf64 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 @@ -46,6 +46,7 @@ describe('GoogleMapsController', () => { data-symfony--ux-google-map--map-options-value="{"mapId":null,"gestureHandling":"auto","backgroundColor":null,"disableDoubleClickZoom":false,"zoomControlOptions":{"position":22},"mapTypeControlOptions":{"mapTypeIds":[],"position":14,"style":0},"streetViewControlOptions":{"position":22},"fullscreenControlOptions":{"position":20},"@provider":"google"}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" + data-symfony--ux-google-map--map-polylines-value="[]" style="height: 600px" > `); diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php index c64045c1e68..d8695096141 100644 --- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php +++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php @@ -18,6 +18,7 @@ use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; use Symfony\UX\Map\Test\RendererTestCase; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; @@ -30,26 +31,26 @@ public function provideTestRenderMap(): iterable ->zoom(12); yield 'simple map, with minimum options' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => $map, ]; yield 'with every options' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'quarterly'), 'map' => $map, ]; yield 'with custom attributes' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => $map, 'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'], ]; yield 'with markers and infoWindows' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => (clone $map) ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris')) @@ -57,15 +58,23 @@ public function provideTestRenderMap(): iterable ]; yield 'with polygons and infoWindows' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => (clone $map) ->addPolygon(new Polygon(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)])) ->addPolygon(new Polygon(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))), ]; + yield 'with polylines and infoWindows' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), + 'map' => (clone $map) + ->addPolyline(new Polyline(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)])) + ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))), + ]; + yield 'with controls enabled' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => (clone $map) ->options(new GoogleOptions( @@ -77,7 +86,7 @@ public function provideTestRenderMap(): iterable ]; yield 'without controls enabled' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), 'map' => (clone $map) ->options(new GoogleOptions( @@ -89,18 +98,18 @@ public function provideTestRenderMap(): iterable ]; yield 'with default map id' => [ - 'expected_renderer' => '
', + 'expected_renderer' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'), 'map' => (clone $map), ]; yield 'with default map id, when passing options (except the "mapId")' => [ - 'expected_renderer' => '
', + 'expected_renderer' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'), 'map' => (clone $map) ->options(new GoogleOptions()), ]; yield 'with default map id overridden by option "mapId"' => [ - 'expected_renderer' => '
', + 'expected_renderer' => '
', 'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'), 'map' => (clone $map) ->options(new GoogleOptions(mapId: 'CustomMapId')), 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 1caf28c123e..ccca26086a2 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 { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; -import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet'; +import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolylineOptions as PolygonOptions, PolylineOptions } from 'leaflet'; type MapOptions = Pick & { tileLayer: { url: string; @@ -10,7 +10,7 @@ type MapOptions = Pick & { options: Record; }; }; -export default class extends AbstractMapController { +export default class extends AbstractMapController { connect(): void; protected dispatchEvent(name: string, payload?: Record): void; protected doCreateMap({ center, zoom, options, }: { @@ -21,12 +21,18 @@ export default class extends AbstractMapController): L.Marker; protected removeMarker(marker: L.Marker): void; protected doCreatePolygon(definition: PolygonDefinition): L.Polygon; + protected removePolygon(polygon: L.Polygon): void; + protected doCreatePolyline(definition: PolylineDefinition): L.Polyline; + protected removePolyline(polyline: L.Polyline): void; protected doCreateInfoWindow({ definition, element, }: { - definition: MarkerDefinition; + definition: MarkerDefinition['infoWindow']; element: L.Marker; } | { - definition: PolygonDefinition; + definition: PolygonDefinition['infoWindow']; element: L.Polygon; + } | { + definition: PolylineDefinition['infoWindow']; + element: L.Polyline; }): L.Popup; protected doFitBoundsToMarkers(): void; centerValueChanged(): void; 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 cdbad0f8752..088c27063b6 100644 --- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js +++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js @@ -8,6 +8,7 @@ class default_1 extends Controller { this.markers = new Map(); this.infoWindows = []; this.polygons = new Map(); + this.polylines = new Map(); } connect() { const options = this.optionsValue; @@ -15,6 +16,7 @@ class default_1 extends Controller { this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options }); this.markersValue.forEach((marker) => this.createMarker(marker)); this.polygonsValue.forEach((polygon) => this.createPolygon(polygon)); + this.polylinesValue.forEach((polyline) => this.createPolyline(polyline)); if (this.fitBoundsToMarkersValue) { this.doFitBoundsToMarkers(); } @@ -22,6 +24,7 @@ class default_1 extends Controller { map: this.map, markers: [...this.markers.values()], polygons: [...this.polygons.values()], + polylines: [...this.polylines.values()], infoWindows: this.infoWindows, }); } @@ -41,6 +44,14 @@ class default_1 extends Controller { this.polygons.set(definition['@id'], polygon); return polygon; } + createPolyline(definition) { + this.dispatchEvent('polyline:before-create', { definition }); + const polyline = this.doCreatePolyline(definition); + this.dispatchEvent('polyline:after-create', { polyline }); + polyline['@id'] = definition['@id']; + this.polylines.set(definition['@id'], polyline); + return polyline; + } createInfoWindow({ definition, element, }) { this.dispatchEvent('info-window:before-create', { definition, element }); const infoWindow = this.doCreateInfoWindow({ definition, element }); @@ -73,7 +84,7 @@ class default_1 extends Controller { } this.polygons.forEach((polygon) => { if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) { - polygon.remove(); + this.removePolygon(polygon); this.polygons.delete(polygon['@id']); } }); @@ -83,6 +94,22 @@ class default_1 extends Controller { } }); } + polylinesValueChanged() { + if (!this.map) { + return; + } + this.polylines.forEach((polyline) => { + if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) { + this.removePolyline(polyline); + this.polylines.delete(polyline['@id']); + } + }); + this.polylinesValue.forEach((polyline) => { + if (!this.polylines.has(polyline['@id'])) { + this.createPolyline(polyline); + } + }); + } } default_1.values = { providerOptions: Object, @@ -91,6 +118,7 @@ default_1.values = { fitBoundsToMarkers: Boolean, markers: Array, polygons: Array, + polylines: Array, options: Object, }; @@ -148,6 +176,23 @@ class map_controller extends default_1 { } return polygon; } + removePolygon(polygon) { + polygon.remove(); + } + doCreatePolyline(definition) { + const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; + const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); + if (title) { + polyline.bindPopup(title); + } + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: polyline }); + } + return polyline; + } + removePolyline(polyline) { + polyline.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 eb392d2178e..86cc334c6cf 100644 --- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts +++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts @@ -1,8 +1,14 @@ import AbstractMapController from '@symfony/ux-map'; -import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map'; +import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map'; import 'leaflet/dist/leaflet.min.css'; import * as L from 'leaflet'; -import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet'; +import type { + MapOptions as LeafletMapOptions, + MarkerOptions, + PopupOptions, + PolylineOptions as PolygonOptions, + PolylineOptions, +} from 'leaflet'; type MapOptions = Pick & { tileLayer: { url: string; attribution: string; options: Record }; @@ -16,7 +22,9 @@ export default class extends AbstractMapController< PopupOptions, typeof L.Popup, PolygonOptions, - typeof L.Polygon + typeof L.Polygon, + PolylineOptions, + typeof L.Polyline > { connect(): void { L.Marker.prototype.options.icon = L.divIcon({ @@ -91,12 +99,43 @@ export default class extends AbstractMapController< return polygon; } + protected removePolygon(polygon: L.Polygon) { + polygon.remove(); + } + + protected doCreatePolyline(definition: PolylineDefinition): L.Polyline { + const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition; + + const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map); + + if (title) { + polyline.bindPopup(title); + } + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, element: polyline }); + } + + return polyline; + } + + protected removePolyline(polyline: L.Polyline): void { + polyline.remove(); + } + protected doCreateInfoWindow({ definition, element, }: - | { definition: MarkerDefinition; element: L.Marker } - | { definition: PolygonDefinition; element: L.Polygon }): L.Popup { + | { + definition: MarkerDefinition['infoWindow']; + element: L.Marker; + } + | { definition: PolygonDefinition['infoWindow']; element: L.Polygon } + | { + definition: PolylineDefinition['infoWindow']; + element: L.Polyline; + }): L.Popup { 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/test/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts index c5378eaf46f..379eb603fed 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 @@ -46,6 +46,7 @@ describe('LeafletController', () => { data-symfony--ux-leaflet-map--map-options-value="{"tileLayer":{"url":"https:\\/\\/tile.openstreetmap.org\\/{z}\\/{x}\\/{y}.png","attribution":"\u00a9 <a href=\\"https:\\/\\/www.openstreetmap.org\\/copyright\\">OpenStreetMap<\\/a>","options":[]},"@provider":"leaflet"}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" + data-symfony--ux-leaflet-map--map-polylines-value="[]" style="height: 600px" > diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php index b7e1394e3d0..81d5316079f 100644 --- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php +++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php @@ -17,6 +17,7 @@ use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; use Symfony\UX\Map\Test\RendererTestCase; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; @@ -29,20 +30,20 @@ public function provideTestRenderMap(): iterable ->zoom(12); yield 'simple map' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new LeafletRenderer(new StimulusHelper(null)), 'map' => $map, ]; yield 'with custom attributes' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new LeafletRenderer(new StimulusHelper(null)), 'map' => $map, 'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'], ]; yield 'with markers and infoWindows' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new LeafletRenderer(new StimulusHelper(null)), 'map' => (clone $map) ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris')) @@ -50,11 +51,19 @@ public function provideTestRenderMap(): iterable ]; yield 'with polygons and infoWindows' => [ - 'expected_render' => '
', + 'expected_render' => '
', 'renderer' => new LeafletRenderer(new StimulusHelper(null)), 'map' => (clone $map) ->addPolygon(new Polygon(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)])) ->addPolygon(new Polygon(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))), ]; + + yield 'with polylines and infoWindows' => [ + 'expected_render' => '
', + 'renderer' => new LeafletRenderer(new StimulusHelper(null)), + 'map' => (clone $map) + ->addPolyline(new Polyline(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)])) + ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))), + ]; } } diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php index 374b7e1a8ad..529f8af8c41 100644 --- a/src/Map/src/Map.php +++ b/src/Map/src/Map.php @@ -35,6 +35,11 @@ public function __construct( * @var array */ private array $polygons = [], + + /** + * @var array + */ + private array $polylines = [], ) { } @@ -95,6 +100,13 @@ public function addPolygon(Polygon $polygon): self return $this; } + public function addPolyline(Polyline $polyline): self + { + $this->polylines[] = $polyline; + + return $this; + } + public function toArray(): array { if (!$this->fitBoundsToMarkers) { @@ -114,6 +126,7 @@ public function toArray(): array 'options' => $this->options ? MapOptionsNormalizer::normalize($this->options) : [], 'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers), 'polygons' => array_map(static fn (Polygon $polygon) => $polygon->toArray(), $this->polygons), + 'polylines' => array_map(static fn (Polyline $polyline) => $polyline->toArray(), $this->polylines), ]; } @@ -123,6 +136,7 @@ public function toArray(): array * zoom?: float, * markers?: list, * polygons?: list, + * polylines?: list, * fitBoundsToMarkers?: bool, * options?: array, * } $map @@ -157,6 +171,12 @@ public static function fromArray(array $map): self } $map['polygons'] = array_map(Polygon::fromArray(...), $map['polygons']); + $map['polylines'] ??= []; + if (!\is_array($map['polylines'])) { + throw new InvalidArgumentException('The "polylines" parameter must be an array.'); + } + $map['polylines'] = array_map(Polyline::fromArray(...), $map['polylines']); + return new self(...$map); } } diff --git a/src/Map/src/Marker.php b/src/Map/src/Marker.php index f14082f9977..54d61ebefc8 100644 --- a/src/Map/src/Marker.php +++ b/src/Map/src/Marker.php @@ -37,7 +37,7 @@ public function __construct( * position: array{lat: float, lng: float}, * title: string|null, * infoWindow: array|null, - * extra: object, + * extra: array, * } */ public function toArray(): array @@ -55,7 +55,7 @@ public function toArray(): array * position: array{lat: float, lng: float}, * title: string|null, * infoWindow: array|null, - * extra: object, + * extra: array, * } $marker * * @internal diff --git a/src/Map/src/Polygon.php b/src/Map/src/Polygon.php index 7dd4cc60ce6..d32594ad000 100644 --- a/src/Map/src/Polygon.php +++ b/src/Map/src/Polygon.php @@ -33,6 +33,13 @@ public function __construct( /** * Convert the polygon to an array representation. + * + * @return array{ + * points: array, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * } */ public function toArray(): array { @@ -49,7 +56,7 @@ public function toArray(): array * points: array, * title: string|null, * infoWindow: array|null, - * extra: object, + * extra: array, * } $polygon * * @internal diff --git a/src/Map/src/Polyline.php b/src/Map/src/Polyline.php new file mode 100644 index 00000000000..4630213637a --- /dev/null +++ b/src/Map/src/Polyline.php @@ -0,0 +1,77 @@ + + * + * 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 polyline on a map. + * + * @author [Sylvain Blondeau] + */ +final readonly class Polyline +{ + /** + * @param array $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side + */ + public function __construct( + private array $points, + private ?string $title = null, + private ?InfoWindow $infoWindow = null, + private array $extra = [], + ) { + } + + /** + * Convert the polyline to an array representation. + * + * @return array{ + * points: array, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * } + */ + public function toArray(): array + { + return [ + 'points' => array_map(fn (Point $point) => $point->toArray(), $this->points), + 'title' => $this->title, + 'infoWindow' => $this->infoWindow?->toArray(), + 'extra' => $this->extra, + ]; + } + + /** + * @param array{ + * points: array, + * title: string|null, + * infoWindow: array|null, + * extra: array, + * } $polyline + * + * @internal + */ + public static function fromArray(array $polyline): self + { + if (!isset($polyline['points'])) { + throw new InvalidArgumentException('The "points" parameter is required.'); + } + $polyline['points'] = array_map(Point::fromArray(...), $polyline['points']); + + if (isset($polyline['infoWindow'])) { + $polyline['infoWindow'] = InfoWindow::fromArray($polyline['infoWindow']); + } + + return new self(...$polyline); + } +} diff --git a/src/Map/src/Renderer/AbstractRenderer.php b/src/Map/src/Renderer/AbstractRenderer.php index b79d2e2c24b..794b08d6d98 100644 --- a/src/Map/src/Renderer/AbstractRenderer.php +++ b/src/Map/src/Renderer/AbstractRenderer.php @@ -84,7 +84,7 @@ final public function renderMap(Map $map, array $attributes = []): string private function getMapAttributes(Map $map): array { - $computeId = fn (array $array) => hash('xxh3', json_encode($array, JSON_THROW_ON_ERROR)); + $computeId = fn (array $array) => hash('xxh3', json_encode($array, \JSON_THROW_ON_ERROR)); $attrs = $map->toArray(); @@ -96,6 +96,10 @@ private function getMapAttributes(Map $map): array $attrs['polygons'][$key]['@id'] = $computeId($polygon); } + foreach ($attrs['polylines'] as $key => $polyline) { + $attrs['polylines'][$key]['@id'] = $computeId($polyline); + } + return $attrs; } } diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php index 8b9a1ca2036..a729e46e31c 100644 --- a/src/Map/src/Twig/MapRuntime.php +++ b/src/Map/src/Twig/MapRuntime.php @@ -15,6 +15,7 @@ use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; use Symfony\UX\Map\Renderer\RendererInterface; use Twig\Extension\RuntimeExtensionInterface; @@ -34,12 +35,14 @@ public function __construct( * @param array $attributes * @param array $markers * @param array $polygons + * @param array $polylines */ public function renderMap( ?Map $map = null, array $attributes = [], ?array $markers = null, ?array $polygons = null, + ?array $polylines = null, ?array $center = null, ?float $zoom = null, ): string { @@ -55,8 +58,11 @@ public function renderMap( foreach ($markers ?? [] as $marker) { $map->addMarker(Marker::fromArray($marker)); } - foreach ($polygons ?? [] as $polygons) { - $map->addPolygon(Polygon::fromArray($polygons)); + foreach ($polygons ?? [] as $polygon) { + $map->addPolygon(Polygon::fromArray($polygon)); + } + foreach ($polylines ?? [] as $polyline) { + $map->addPolyline(Polyline::fromArray($polyline)); } if (null !== $center) { $map->center(Point::fromArray($center)); @@ -70,7 +76,7 @@ public function renderMap( public function render(array $args = []): string { - $map = array_intersect_key($args, ['map' => 0, 'markers' => 0, 'polygons' => 0, 'center' => 1, 'zoom' => 2]); + $map = array_intersect_key($args, ['map' => 0, 'markers' => 1, 'polygons' => 2, 'polylines' => 3, 'center' => 4, 'zoom' => 5]); $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 39e362b34b9..4c167420d52 100644 --- a/src/Map/src/Twig/UXMapComponent.php +++ b/src/Map/src/Twig/UXMapComponent.php @@ -14,6 +14,7 @@ use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; /** * @author Simon André @@ -35,4 +36,9 @@ final class UXMapComponent * @var Polygon[] */ public array $polygons; + + /** + * @var Polyline[] + */ + public array $polylines; } diff --git a/src/Map/tests/MapFactoryTest.php b/src/Map/tests/MapFactoryTest.php index bdf737cb320..25194376f96 100644 --- a/src/Map/tests/MapFactoryTest.php +++ b/src/Map/tests/MapFactoryTest.php @@ -53,6 +53,13 @@ public function testFromArray(): void $this->assertSame($array['polygons'][0]['title'], $polygons[0]['title']); $this->assertSame($array['polygons'][0]['infoWindow']['headerContent'], $polygons[0]['infoWindow']['headerContent']); $this->assertSame($array['polygons'][0]['infoWindow']['content'], $polygons[0]['infoWindow']['content']); + + $this->assertCount(1, $polylines = $map->toArray()['polylines']); + $this->assertEquals($array['polylines'][0]['points'], $polylines[0]['points']); + $this->assertEquals($array['polylines'][0]['points'], $polylines[0]['points']); + $this->assertSame($array['polylines'][0]['title'], $polylines[0]['title']); + $this->assertSame($array['polylines'][0]['infoWindow']['headerContent'], $polylines[0]['infoWindow']['headerContent']); + $this->assertSame($array['polylines'][0]['infoWindow']['content'], $polylines[0]['infoWindow']['content']); } public function testToArrayFromArray(): void @@ -154,6 +161,30 @@ public function testFromArrayWithInvalidPolygon(): void Map::fromArray($array); } + public function testFromArrayWithInvalidPolylines(): void + { + $array = self::createMapArray(); + $array['polylines'] = 'invalid'; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "polylines" parameter must be an array.'); + Map::fromArray($array); + } + + public function testFromArrayWithInvalidPolyline(): void + { + $array = self::createMapArray(); + $array['polylines'] = [ + [ + 'invalid', + ], + ]; + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "points" parameter is required.'); + Map::fromArray($array); + } + private static function createMapArray(): array { return [ @@ -198,6 +229,29 @@ private static function createMapArray(): array ], ], ], + 'polylines' => [ + [ + 'points' => [ + [ + 'lat' => 48.858844, + 'lng' => 2.294351, + ], + [ + 'lat' => 48.853, + 'lng' => 2.3499, + ], + [ + 'lat' => 48.8566, + 'lng' => 2.3522, + ], + ], + 'title' => 'Polyline 1', + 'infoWindow' => [ + 'headerContent' => 'Polyline 1', + 'content' => 'Polyline 1', + ], + ], + ], ]; } } diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php index 7445dbc8b52..b176e0d99c1 100644 --- a/src/Map/tests/MapTest.php +++ b/src/Map/tests/MapTest.php @@ -18,6 +18,7 @@ use Symfony\UX\Map\Marker; use Symfony\UX\Map\Point; use Symfony\UX\Map\Polygon; +use Symfony\UX\Map\Polyline; class MapTest extends TestCase { @@ -66,6 +67,7 @@ public function testZoomAndCenterCanBeOmittedIfFitBoundsToMarkers(): void 'options' => $array['options'], 'markers' => [], 'polygons' => [], + 'polylines' => [], ], $array); } @@ -85,6 +87,7 @@ public function testWithMinimumConfiguration(): void 'options' => $array['options'], 'markers' => [], 'polygons' => [], + 'polylines' => [], ], $array); } @@ -136,6 +139,30 @@ public function testWithMaximumConfiguration(): void autoClose: true, ), )) + ->addPolyline(new Polyline( + points: [ + new Point(48.858844, 2.294351), + new Point(48.853, 2.3499), + new Point(48.8566, 2.3522), + ], + title: 'Polyline 1', + infoWindow: null, + )) + ->addPolyline(new Polyline( + points: [ + new Point(45.764043, 4.835659), + new Point(45.75, 4.85), + new Point(45.77, 4.82), + ], + title: 'Polyline 2', + infoWindow: new InfoWindow( + headerContent: 'Polyline 2', + content: 'A polyline around Lyon with some additional info.', + position: new Point(45.764, 4.8357), + opened: true, + autoClose: true, + ), + )) ; self::assertEquals([ @@ -217,6 +244,35 @@ public function testWithMaximumConfiguration(): void 'extra' => [], ], ], + 'polylines' => [ + [ + 'points' => [ + ['lat' => 48.858844, 'lng' => 2.294351], + ['lat' => 48.853, 'lng' => 2.3499], + ['lat' => 48.8566, 'lng' => 2.3522], + ], + 'title' => 'Polyline 1', + 'infoWindow' => null, + 'extra' => [], + ], + [ + 'points' => [ + ['lat' => 45.764043, 'lng' => 4.835659], + ['lat' => 45.75, 'lng' => 4.85], + ['lat' => 45.77, 'lng' => 4.82], + ], + 'title' => 'Polyline 2', + 'infoWindow' => [ + 'headerContent' => 'Polyline 2', + 'content' => 'A polyline around Lyon with some additional info.', + 'position' => ['lat' => 45.764, 'lng' => 4.8357], + 'opened' => true, + 'autoClose' => true, + 'extra' => [], + ], + 'extra' => [], + ], + ], ], $map->toArray()); } }