From 3598a9aecc8eee4b12a2e70688cbef448eed7b1c Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Fri, 5 Jul 2024 18:14:30 +0200 Subject: [PATCH] [Map] Second iteration, simplify configuration a lot, make Map/Markers/InfoWindow generic! --- .github/workflows/test.yaml | 2 + src/Map/.gitattributes | 1 + src/Map/.gitignore | 1 - src/Map/LICENSE | 2 +- src/Map/README.md | 2 +- .../assets/dist/google_maps_controller.d.ts | 47 +-- src/Map/assets/dist/google_maps_controller.js | 148 ++++--- src/Map/assets/dist/leaflet_controller.d.ts | 50 +-- src/Map/assets/dist/leaflet_controller.js | 105 ++--- src/Map/assets/package.json | 2 +- src/Map/assets/src/global.d.ts | 6 +- src/Map/assets/src/google_maps_controller.ts | 227 +++++------ src/Map/assets/src/leaflet_controller.ts | 157 ++++---- src/Map/assets/src/map.d.ts | 24 ++ .../test/google_maps_controller.test.ts | 4 +- .../assets/test/leaflet_controller.test.ts | 4 +- src/Map/composer.json | 15 +- src/Map/config/asset_mapper.php | 7 +- src/Map/config/services.php | 50 +-- src/Map/doc/index.rst | 379 +++++++----------- .../LeafletReplaceImagesAssetCompiler.php | 8 +- .../Resolver/LeafletPackageResolver.php | 8 +- src/Map/src/Configuration/Configuration.php | 83 ---- src/Map/src/Configuration/Map.php | 27 -- src/Map/src/Configuration/Provider.php | 27 -- .../src/DependencyInjection/Configuration.php | 144 ------- .../DependencyInjection/UXMapExtension.php | 32 +- ...lictingMapProvidersOnSamePageException.php | 26 -- src/Map/src/Exception/Exception.php | 5 +- ...ception.php => IncompleteDsnException.php} | 11 +- .../Exception/InvalidArgumentException.php | 5 +- .../Exception/ProviderNotFoundException.php | 22 - src/Map/src/Exception/RuntimeException.php | 5 +- .../Exception/UnsupportedSchemeException.php | 29 ++ src/Map/src/Factory/MapFactory.php | 49 --- src/Map/src/InfoWindow.php | 40 ++ src/Map/src/LatLng.php | 18 +- src/Map/src/Map.php | 134 +++++++ src/Map/src/MapFactory.php | 40 ++ src/Map/src/MapInterface.php | 35 -- ...yInterface.php => MapOptionsInterface.php} | 15 +- src/Map/src/{Registry => }/MapRegistry.php | 14 +- src/Map/src/MapTrait.php | 46 --- src/Map/src/Marker.php | 36 ++ .../src/Provider/AbstractProviderFactory.php | 35 ++ src/Map/src/Provider/Dsn.php | 81 ++++ .../Provider/GoogleMaps/GoogleMapsOptions.php | 143 +++++++ .../GoogleMaps/GoogleMapsProvider.php | 80 ++++ .../GoogleMaps/GoogleMapsProviderFactory.php | 50 +++ .../src/Provider/GoogleMaps/InfoWindow.php | 54 --- src/Map/src/Provider/GoogleMaps/Map.php | 203 ---------- .../src/Provider/GoogleMaps/MapFactory.php | 80 ---- src/Map/src/Provider/GoogleMaps/Marker.php | 52 --- .../{ => Option}/ControlPosition.php | 6 +- .../{ => Option}/FullscreenControlOptions.php | 8 +- .../{ => Option}/GestureHandling.php | 6 +- .../{ => Option}/MapTypeControlOptions.php | 8 +- .../{ => Option}/MapTypeControlStyle.php | 6 +- .../GoogleMaps/{ => Option}/MapTypeId.php | 6 +- .../{ => Option}/StreetViewControlOptions.php | 8 +- .../{ => Option}/ZoomControlOptions.php | 8 +- .../src/Provider/Leaflet/LeafletOptions.php | 36 ++ .../src/Provider/Leaflet/LeafletProvider.php | 44 ++ .../Leaflet/LeafletProviderFactory.php | 38 ++ src/Map/src/Provider/Leaflet/Map.php | 104 ----- src/Map/src/Provider/Leaflet/MapFactory.php | 58 --- src/Map/src/Provider/Leaflet/Marker.php | 58 --- .../Leaflet/{ => Option}/TileLayer.php | 10 +- src/Map/src/Provider/Leaflet/Popup.php | 52 --- src/Map/src/Provider/Provider.php | 50 +++ .../src/Provider/ProviderFactoryInterface.php | 22 + src/Map/src/Provider/ProviderInterface.php | 28 ++ src/Map/src/Provider/ProviderTrait.php | 23 ++ src/Map/src/Registry/MapRegistryInterface.php | 26 -- src/Map/src/Test/ProviderFactoryTestCase.php | 111 +++++ src/Map/src/Test/ProviderTestCase.php | 37 ++ src/Map/src/Twig/MapExtension.php | 5 +- src/Map/src/Twig/MapRuntime.php | 27 +- src/Map/src/UXMapBundle.php | 6 +- .../LeafletReplaceImagesAssetCompilerTest.php | 86 ++++ .../ImportMap/Fixtures/leaflet.1.9.4.js | 9 + .../Resolver/LeafletPackageResolverTest.php | 132 ++++++ src/Map/tests/ConfigurationTest.php | 65 --- src/Map/tests/Factory/MapFactoryTest.php | 112 ------ src/Map/tests/InfoWindowTest.php | 41 ++ src/Map/tests/Kernel/FrameworkAppKernel.php | 1 + src/Map/tests/Kernel/TwigAppKernel.php | 1 + src/Map/tests/LatLngTest.php | 14 +- src/Map/tests/MapFactoryTest.php | 43 ++ .../tests/{Registry => }/MapRegistryTest.php | 15 +- src/Map/tests/MapTest.php | 138 +++++++ src/Map/tests/MarkerTest.php | 55 +++ src/Map/tests/Provider/DsnTest.php | 103 +++++ src/Map/tests/Provider/DummyProvider.php | 46 +++ .../GoogleMaps/GoogleMapsOptionsTest.php | 102 +++++ .../GoogleMaps/GoogleMapsProviderTest.php | 76 ++++ .../GoogleMaps/GoogleProviderFactoryTest.php | 48 +++ .../Provider/GoogleMaps/InfoWindowTest.php | 70 ---- .../Provider/GoogleMaps/MapFactoryTest.php | 68 ---- src/Map/tests/Provider/GoogleMaps/MapTest.php | 201 ---------- .../tests/Provider/GoogleMaps/MarkerTest.php | 38 -- .../{ => Option}/ControlPositionTest.php | 6 +- .../FullscreenControlOptionsTest.php | 12 +- .../{ => Option}/GestureHandlingTest.php | 6 +- .../MapTypeControlOptionsTest.php | 16 +- .../{ => Option}/MapTypeControlStyleTest.php | 6 +- .../GoogleMaps/{ => Option}/MapTypeIdTest.php | 6 +- .../StreetViewControlOptionsTest.php | 12 +- .../{ => Option}/ZoomControlOptionsTest.php | 12 +- .../Provider/Leaflet/LeafletOptionsTest.php | 61 +++ .../Leaflet/LeafletProviderFactoryTest.php | 43 ++ .../Provider/Leaflet/LeafletProviderTest.php | 34 ++ .../tests/Provider/Leaflet/MapFactoryTest.php | 72 ---- src/Map/tests/Provider/Leaflet/MapTest.php | 146 ------- src/Map/tests/Provider/Leaflet/MarkerTest.php | 41 -- .../Provider/Leaflet/Option/TileLayerTest.php | 37 ++ src/Map/tests/Provider/Leaflet/PopupTest.php | 72 ---- src/Map/tests/Provider/ProviderTest.php | 46 +++ src/Map/tests/TwigTest.php | 83 +--- src/Map/tests/UxMapBundleTest.php | 74 ---- 120 files changed, 2886 insertions(+), 3024 deletions(-) create mode 100644 src/Map/assets/src/map.d.ts delete mode 100644 src/Map/src/Configuration/Configuration.php delete mode 100644 src/Map/src/Configuration/Map.php delete mode 100644 src/Map/src/Configuration/Provider.php delete mode 100644 src/Map/src/DependencyInjection/Configuration.php delete mode 100644 src/Map/src/Exception/ConflictingMapProvidersOnSamePageException.php rename src/Map/src/Exception/{MapNotFoundException.php => IncompleteDsnException.php} (50%) delete mode 100644 src/Map/src/Exception/ProviderNotFoundException.php create mode 100644 src/Map/src/Exception/UnsupportedSchemeException.php delete mode 100644 src/Map/src/Factory/MapFactory.php create mode 100644 src/Map/src/InfoWindow.php create mode 100644 src/Map/src/Map.php create mode 100644 src/Map/src/MapFactory.php delete mode 100644 src/Map/src/MapInterface.php rename src/Map/src/{Factory/MapFactoryInterface.php => MapOptionsInterface.php} (51%) rename src/Map/src/{Registry => }/MapRegistry.php (67%) delete mode 100644 src/Map/src/MapTrait.php create mode 100644 src/Map/src/Marker.php create mode 100644 src/Map/src/Provider/AbstractProviderFactory.php create mode 100644 src/Map/src/Provider/Dsn.php create mode 100644 src/Map/src/Provider/GoogleMaps/GoogleMapsOptions.php create mode 100644 src/Map/src/Provider/GoogleMaps/GoogleMapsProvider.php create mode 100644 src/Map/src/Provider/GoogleMaps/GoogleMapsProviderFactory.php delete mode 100644 src/Map/src/Provider/GoogleMaps/InfoWindow.php delete mode 100644 src/Map/src/Provider/GoogleMaps/Map.php delete mode 100644 src/Map/src/Provider/GoogleMaps/MapFactory.php delete mode 100644 src/Map/src/Provider/GoogleMaps/Marker.php rename src/Map/src/Provider/GoogleMaps/{ => Option}/ControlPosition.php (98%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/FullscreenControlOptions.php (83%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/GestureHandling.php (92%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/MapTypeControlOptions.php (89%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/MapTypeControlStyle.php (89%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/MapTypeId.php (89%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/StreetViewControlOptions.php (83%) rename src/Map/src/Provider/GoogleMaps/{ => Option}/ZoomControlOptions.php (83%) create mode 100644 src/Map/src/Provider/Leaflet/LeafletOptions.php create mode 100644 src/Map/src/Provider/Leaflet/LeafletProvider.php create mode 100644 src/Map/src/Provider/Leaflet/LeafletProviderFactory.php delete mode 100644 src/Map/src/Provider/Leaflet/Map.php delete mode 100644 src/Map/src/Provider/Leaflet/MapFactory.php delete mode 100644 src/Map/src/Provider/Leaflet/Marker.php rename src/Map/src/Provider/Leaflet/{ => Option}/TileLayer.php (78%) delete mode 100644 src/Map/src/Provider/Leaflet/Popup.php create mode 100644 src/Map/src/Provider/Provider.php create mode 100644 src/Map/src/Provider/ProviderFactoryInterface.php create mode 100644 src/Map/src/Provider/ProviderInterface.php create mode 100644 src/Map/src/Provider/ProviderTrait.php delete mode 100644 src/Map/src/Registry/MapRegistryInterface.php create mode 100644 src/Map/src/Test/ProviderFactoryTestCase.php create mode 100644 src/Map/src/Test/ProviderTestCase.php create mode 100644 src/Map/tests/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompilerTest.php create mode 100644 src/Map/tests/AssetMapper/ImportMap/Fixtures/leaflet.1.9.4.js create mode 100644 src/Map/tests/AssetMapper/ImportMap/Resolver/LeafletPackageResolverTest.php delete mode 100644 src/Map/tests/ConfigurationTest.php delete mode 100644 src/Map/tests/Factory/MapFactoryTest.php create mode 100644 src/Map/tests/InfoWindowTest.php create mode 100644 src/Map/tests/MapFactoryTest.php rename src/Map/tests/{Registry => }/MapRegistryTest.php (57%) create mode 100644 src/Map/tests/MapTest.php create mode 100644 src/Map/tests/MarkerTest.php create mode 100644 src/Map/tests/Provider/DsnTest.php create mode 100644 src/Map/tests/Provider/DummyProvider.php create mode 100644 src/Map/tests/Provider/GoogleMaps/GoogleMapsOptionsTest.php create mode 100644 src/Map/tests/Provider/GoogleMaps/GoogleMapsProviderTest.php create mode 100644 src/Map/tests/Provider/GoogleMaps/GoogleProviderFactoryTest.php delete mode 100644 src/Map/tests/Provider/GoogleMaps/InfoWindowTest.php delete mode 100644 src/Map/tests/Provider/GoogleMaps/MapFactoryTest.php delete mode 100644 src/Map/tests/Provider/GoogleMaps/MapTest.php delete mode 100644 src/Map/tests/Provider/GoogleMaps/MarkerTest.php rename src/Map/tests/Provider/GoogleMaps/{ => Option}/ControlPositionTest.php (94%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/FullscreenControlOptionsTest.php (67%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/GestureHandlingTest.php (82%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/MapTypeControlOptionsTest.php (67%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/MapTypeControlStyleTest.php (81%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/MapTypeIdTest.php (83%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/StreetViewControlOptionsTest.php (67%) rename src/Map/tests/Provider/GoogleMaps/{ => Option}/ZoomControlOptionsTest.php (66%) create mode 100644 src/Map/tests/Provider/Leaflet/LeafletOptionsTest.php create mode 100644 src/Map/tests/Provider/Leaflet/LeafletProviderFactoryTest.php create mode 100644 src/Map/tests/Provider/Leaflet/LeafletProviderTest.php delete mode 100644 src/Map/tests/Provider/Leaflet/MapFactoryTest.php delete mode 100644 src/Map/tests/Provider/Leaflet/MapTest.php delete mode 100644 src/Map/tests/Provider/Leaflet/MarkerTest.php create mode 100644 src/Map/tests/Provider/Leaflet/Option/TileLayerTest.php delete mode 100644 src/Map/tests/Provider/Leaflet/PopupTest.php create mode 100644 src/Map/tests/Provider/ProviderTest.php diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 96a0c126b02..402d4bca253 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -89,6 +89,8 @@ jobs: dependency-version: 'highest' component: ${{ fromJson(needs.tests-php-components.outputs.components )}} exclude: + - component: Map # does not support PHP 8.1 + php-version: '8.1' - component: Swup # has no tests - component: Turbo # has its own workflow (test-turbo.yml) - component: Typed # has no tests diff --git a/src/Map/.gitattributes b/src/Map/.gitattributes index 97734d35229..35c1f46ae5d 100644 --- a/src/Map/.gitattributes +++ b/src/Map/.gitattributes @@ -4,4 +4,5 @@ /phpunit.xml.dist export-ignore /assets/src export-ignore /assets/test export-ignore +/assets/vitest.config.js export-ignore /tests export-ignore diff --git a/src/Map/.gitignore b/src/Map/.gitignore index 30282084317..50b321e33a2 100644 --- a/src/Map/.gitignore +++ b/src/Map/.gitignore @@ -1,4 +1,3 @@ vendor composer.lock -.php_cs.cache .phpunit.result.cache diff --git a/src/Map/LICENSE b/src/Map/LICENSE index 3ed9f412ce5..e374a5c8339 100644 --- a/src/Map/LICENSE +++ b/src/Map/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023-present Fabien Potencier +Copyright (c) 2024-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Map/README.md b/src/Map/README.md index 067879fa01b..443684f5d84 100644 --- a/src/Map/README.md +++ b/src/Map/README.md @@ -3,7 +3,7 @@ **EXPERIMENTAL** This component is currently experimental and is likely to change, or even change drastically. -Symfony UX Map integrates [Symfony Translation](https://symfony.com/doc/current/translation.html) for JavaScript. +Symfony UX Map integrates interactive Maps in Symfony applications, like Leaflet or GoogleMaps. **This repository is a READ-ONLY sub-tree split**. See https://github.com/symfony/ux to create issues or submit pull requests. diff --git a/src/Map/assets/dist/google_maps_controller.d.ts b/src/Map/assets/dist/google_maps_controller.d.ts index afc320e0a86..dcdada40bd8 100644 --- a/src/Map/assets/dist/google_maps_controller.d.ts +++ b/src/Map/assets/dist/google_maps_controller.d.ts @@ -1,55 +1,22 @@ /// import { Controller } from '@hotwired/stimulus'; -type MarkerId = number; +import type { MapView } from './map'; +type GoogleMapsOptions = Pick; export default class extends Controller { static values: { view: ObjectConstructor; }; - viewValue: { - mapId: string | null; - center: null | { - lat: number; - lng: number; - }; - zoom: number; - gestureHandling: string; - backgroundColor: string; - disableDoubleClickZoom: boolean; - zoomControl: boolean; - zoomControlOptions: google.maps.ZoomControlOptions; - mapTypeControl: boolean; - mapTypeControlOptions: google.maps.MapTypeControlOptions; - streetViewControl: boolean; - streetViewControlOptions: google.maps.StreetViewControlOptions; - fullscreenControl: boolean; - fullscreenControlOptions: google.maps.FullscreenControlOptions; - markers: Array<{ - _id: MarkerId; - position: { - lat: number; - lng: number; - }; - title: string | null; - }>; - infoWindows: Array<{ - headerContent: string | null; - content: string | null; - position: { - lat: number; - lng: number; - }; - opened: boolean; - _markerId: MarkerId | null; - autoClose: boolean; - }>; - fitBoundsToMarkers: boolean; - }; + viewValue: MapView; private loader; private map; private markers; private infoWindows; initialize(): void; connect(): Promise; + private createMarkers; + private createMarker; + private createInfoWindows; + private createInfoWindow; private createTextOrElement; private closeInfoWindowsExcept; private dispatchEvent; diff --git a/src/Map/assets/dist/google_maps_controller.js b/src/Map/assets/dist/google_maps_controller.js index 09928d70394..33774d7c48d 100644 --- a/src/Map/assets/dist/google_maps_controller.js +++ b/src/Map/assets/dist/google_maps_controller.js @@ -4,106 +4,94 @@ import { Loader } from '@googlemaps/js-api-loader'; class default_1 extends Controller { constructor() { super(...arguments); - this.markers = new Map(); + this.markers = []; this.infoWindows = []; } initialize() { - var _a; - const providerConfig = (_a = window.__symfony_ux_maps.providers) === null || _a === void 0 ? void 0 : _a.google_maps; + var _a, _b; + const providerConfig = (_b = (_a = window.__symfony_ux_maps) === null || _a === void 0 ? void 0 : _a.providers) === null || _b === void 0 ? void 0 : _b['google-maps']; if (!providerConfig) { throw new Error('Google Maps provider configuration is missing, did you forget to call `{{ ux_map_script_tags() }}`?'); } - const loaderOptions = { - apiKey: providerConfig.key, - }; + const loaderOptions = providerConfig; this.dispatchEvent('init', { loaderOptions, }); this.loader = new Loader(loaderOptions); } async connect() { - const { Map: GoogleMap, InfoWindow } = await this.loader.importLibrary('maps'); - const mapOptions = { - gestureHandling: this.viewValue.gestureHandling, - backgroundColor: this.viewValue.backgroundColor, - disableDoubleClickZoom: this.viewValue.disableDoubleClickZoom, - zoomControl: this.viewValue.zoomControl, - zoomControlOptions: this.viewValue.zoomControlOptions, - mapTypeControl: this.viewValue.mapTypeControl, - mapTypeControlOptions: this.viewValue.mapTypeControlOptions, - streetViewControl: this.viewValue.streetViewControl, - streetViewControlOptions: this.viewValue.streetViewControlOptions, - fullscreenControl: this.viewValue.fullscreenControl, - fullscreenControlOptions: this.viewValue.fullscreenControlOptions, - }; - if (this.viewValue.mapId) { - mapOptions.mapId = this.viewValue.mapId; - } - if (this.viewValue.center) { - mapOptions.center = this.viewValue.center; - } - if (this.viewValue.zoom) { - mapOptions.zoom = this.viewValue.zoom; - } - this.dispatchEvent('pre-connect', { - mapOptions, + const { Map: GoogleMap } = await this.loader.importLibrary('maps'); + const { center, zoom, fitBoundsToMarkers, options, markers, infoWindows } = this.viewValue; + this.dispatchEvent('pre-connect', { options }); + this.map = new GoogleMap(this.element, Object.assign(Object.assign({}, options), { center, + zoom })); + this.createMarkers(markers, fitBoundsToMarkers); + this.createInfoWindows(infoWindows); + this.dispatchEvent('connect', { + map: this.map, + markers: this.markers, + infoWindows: this.infoWindows, }); - this.map = new GoogleMap(this.element, mapOptions); - if (this.viewValue.markers) { - const { AdvancedMarkerElement } = await this.loader.importLibrary('marker'); - this.viewValue.markers.forEach((markerConfiguration) => { - const marker = new AdvancedMarkerElement({ - position: markerConfiguration.position, - title: markerConfiguration.title, - map: this.map, - }); - this.markers.set(markerConfiguration._id, marker); + } + createMarkers(markers, fitBoundsToMarkers) { + markers.forEach((definition) => this.createMarker(definition)); + if (this.markers.length > 0 && fitBoundsToMarkers) { + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + bounds.extend(marker.position); }); - if (this.viewValue.fitBoundsToMarkers) { - const bounds = new google.maps.LatLngBounds(); - this.markers.forEach((marker) => { - if (!marker.position) { - return; - } - bounds.extend(marker.position); - }); - this.map.fitBounds(bounds); - } + this.map.fitBounds(bounds); + } + } + async createMarker(definition) { + const { AdvancedMarkerElement } = await this.loader.importLibrary('marker'); + const options = { + position: definition.position, + title: definition.title, + }; + this.dispatchEvent('marker:before-create', { options }); + const marker = new AdvancedMarkerElement(Object.assign(Object.assign({}, options), { map: this.map })); + if (definition.infoWindow) { + this.createInfoWindow(definition.infoWindow, marker); } - this.viewValue.infoWindows.forEach((infoWindowConfiguration) => { - const marker = infoWindowConfiguration._markerId - ? this.markers.get(infoWindowConfiguration._markerId) - : undefined; - const infoWindow = new InfoWindow({ - headerContent: this.createTextOrElement(infoWindowConfiguration.headerContent), - content: this.createTextOrElement(infoWindowConfiguration.content), - position: infoWindowConfiguration.position, + this.dispatchEvent('marker:after-create', { marker }); + this.markers.push(marker); + } + createInfoWindows(infoWindows) { + infoWindows.forEach((definition) => this.createInfoWindow(definition)); + } + async createInfoWindow(definition, marker) { + const { InfoWindow } = await this.loader.importLibrary('maps'); + const options = { + headerContent: this.createTextOrElement(definition.headerContent), + content: this.createTextOrElement(definition.content), + position: definition.position, + }; + this.dispatchEvent('info-window:before-create', { options }); + const infoWindow = new InfoWindow(options); + this.infoWindows.push(infoWindow); + if (definition.opened) { + infoWindow.open({ + map: this.map, + shouldFocus: false, + anchor: marker, }); - this.infoWindows.push(infoWindow); - if (infoWindowConfiguration.opened) { + } + if (marker) { + marker.addListener('click', () => { + if (definition.autoClose) { + this.closeInfoWindowsExcept(infoWindow); + } infoWindow.open({ map: this.map, - shouldFocus: false, anchor: marker, }); - } - if (marker) { - marker.addListener('click', () => { - if (infoWindowConfiguration.autoClose) { - this.closeInfoWindowsExcept(infoWindow); - } - infoWindow.open({ - map: this.map, - anchor: marker, - }); - }); - } - }); - this.dispatchEvent('connect', { - map: this.map, - markers: this.markers, - infoWindows: this.infoWindows, - }); + }); + } + this.dispatchEvent('info-window:after-create', { infoWindow }); } createTextOrElement(content) { if (!content) { diff --git a/src/Map/assets/dist/leaflet_controller.d.ts b/src/Map/assets/dist/leaflet_controller.d.ts index edad1bcafac..e4717cb27f0 100644 --- a/src/Map/assets/dist/leaflet_controller.d.ts +++ b/src/Map/assets/dist/leaflet_controller.d.ts @@ -1,45 +1,29 @@ import { Controller } from '@hotwired/stimulus'; import 'leaflet/dist/leaflet.min.css'; -import type { MarkerOptions } from 'leaflet'; -type MarkerId = number; +import type { MapOptions } from 'leaflet'; +import type { MapView } from './map'; +type LeafletOptions = Pick; +type AdditionalOptions = { + tileLayer: { + url: string; + attribution: string; + options: Record; + }; +}; export default class extends Controller { static values: { view: ObjectConstructor; }; - viewValue: { - center: null | { - lat: number; - lng: number; - }; - zoom: number | null; - tileLayer: { - url: string; - attribution: string; - } & Record; - fitBoundsToMarkers: boolean; - markers: Array<{ - _id: MarkerId; - position: { - lat: number; - lng: number; - }; - } & MarkerOptions>; - popups: Array<{ - _markerId: MarkerId | null; - content: string; - position: { - lat: number; - lng: number; - }; - opened: boolean; - autoClose: boolean; - }>; - }; + viewValue: MapView; private map; private markers; - private popups; + private infoWindows; connect(): void; - private setupTileLayer; + private createTileLayer; + private createMarkers; + private createMarker; + private createInfoWindows; + private createInfoWindow; private dispatchEvent; } export {}; diff --git a/src/Map/assets/dist/leaflet_controller.js b/src/Map/assets/dist/leaflet_controller.js index ef3ec84faee..7237a585970 100644 --- a/src/Map/assets/dist/leaflet_controller.js +++ b/src/Map/assets/dist/leaflet_controller.js @@ -32,63 +32,72 @@ function __rest(s, e) { class default_1 extends Controller { constructor() { super(...arguments); - this.markers = new Map(); - this.popups = []; + this.markers = []; + this.infoWindows = []; } connect() { - const mapOptions = { - center: this.viewValue.center || undefined, - zoom: this.viewValue.zoom || undefined, - }; - this.dispatchEvent('pre-connect', { - mapOptions, - }); - this.map = L.map(this.element, mapOptions); - this.setupTileLayer(); - this.viewValue.markers.forEach((markerConfiguration) => { - const { _id, position } = markerConfiguration, options = __rest(markerConfiguration, ["_id", "position"]); - const marker = L.marker(position, options).addTo(this.map); - this.markers.set(_id, marker); - }); - this.viewValue.popups.forEach((popupConfiguration) => { - let popup; - if (popupConfiguration._markerId) { - const marker = this.markers.get(popupConfiguration._markerId); - if (!marker) { - return; - } - marker.bindPopup(popupConfiguration.content, { - autoClose: popupConfiguration.autoClose, - }); - popup = marker.getPopup(); - } - else { - popup = L.popup({ - content: popupConfiguration.content, - autoClose: popupConfiguration.autoClose, - }); - popup.setLatLng(popupConfiguration.position); - } - if (popupConfiguration.opened) { - popup.openOn(this.map); - } - this.popups.push(popup); + const { center, zoom, fitBoundsToMarkers, options, markers, infoWindows } = this.viewValue; + this.dispatchEvent('pre-connect', { options }); + const _a = this.viewValue.options, { tileLayer } = _a, mapOptions = __rest(_a, ["tileLayer"]); + this.map = L.map(this.element, Object.assign(Object.assign({}, mapOptions), { center, + zoom })); + this.createTileLayer(tileLayer); + this.createMarkers(markers, fitBoundsToMarkers); + this.createInfoWindows(infoWindows); + this.dispatchEvent('connect', { + map: this.map, + markers: this.markers, + infoWindows: this.infoWindows, }); - if (this.viewValue.fitBoundsToMarkers) { + } + createTileLayer(definition) { + const { url, attribution, options } = definition; + L.tileLayer(url, Object.assign({ attribution }, options)).addTo(this.map); + } + createMarkers(markers, fitBoundsToMarkers) { + markers.forEach((definition) => this.createMarker(definition)); + if (fitBoundsToMarkers && this.markers.length > 0) { this.map.fitBounds(Array.from(this.markers.values()).map((marker) => { const position = marker.getLatLng(); return [position.lat, position.lng]; })); } - this.dispatchEvent('connect', { - map: this.map, - markers: this.markers, - popups: this.popups, - }); } - setupTileLayer() { - const _a = this.viewValue.tileLayer, { url, attribution } = _a, options = __rest(_a, ["url", "attribution"]); - L.tileLayer(url, Object.assign({ attribution }, options)).addTo(this.map); + createMarker(definition) { + const { position } = definition, options = __rest(definition, ["position"]); + this.dispatchEvent('marker:before-create', { options }); + const marker = L.marker(position, options).addTo(this.map); + if (definition.infoWindow) { + this.createInfoWindow(definition.infoWindow, marker); + } + this.dispatchEvent('marker:after-create', { marker }); + this.markers.push(marker); + } + createInfoWindows(infoWindows) { + infoWindows.forEach((definition) => this.createInfoWindow(definition)); + } + createInfoWindow(definition, marker) { + let infoWindow; + const options = Object.assign({}, definition); + this.dispatchEvent('info-window:before-create', { options }); + const { headerContent, content, position } = options, otherOptions = __rest(options, ["headerContent", "content", "position"]); + if (marker) { + marker.bindPopup(headerContent + '
' + content, otherOptions); + if (definition.opened) { + marker.openPopup(); + } + infoWindow = marker.getPopup(); + } + else { + infoWindow = L.popup(otherOptions) + .setContent(headerContent + '
' + content) + .setLatLng(position); + if (definition.opened) { + infoWindow.openOn(this.map); + } + } + this.infoWindows.push(infoWindow); + this.dispatchEvent('info-window:after-create', { infoWindow }); } dispatchEvent(name, payload) { this.dispatch(name, { detail: payload, prefix: 'leaflet' }); diff --git a/src/Map/assets/package.json b/src/Map/assets/package.json index 977349804b0..14f939319eb 100644 --- a/src/Map/assets/package.json +++ b/src/Map/assets/package.json @@ -1,6 +1,6 @@ { "name": "@symfony/ux-map", - "description": "Symfony Map for JavaScript", + "description": "Integrates interactive maps in your Symfony applications", "license": "MIT", "version": "1.0.0", "symfony": { diff --git a/src/Map/assets/src/global.d.ts b/src/Map/assets/src/global.d.ts index ce8d7d79b19..2a0838fbe90 100644 --- a/src/Map/assets/src/global.d.ts +++ b/src/Map/assets/src/global.d.ts @@ -1,10 +1,10 @@ +import type { LoaderOptions } from '@googlemaps/js-api-loader'; + declare global { interface Window { __symfony_ux_maps?: { providers?: { - google_maps?: { - key: string; - }; + 'google-maps'?: LoaderOptions; leaflet?: Record; }; }; diff --git a/src/Map/assets/src/google_maps_controller.ts b/src/Map/assets/src/google_maps_controller.ts index b29c01d5ba1..7c11c4b7c0b 100644 --- a/src/Map/assets/src/google_maps_controller.ts +++ b/src/Map/assets/src/google_maps_controller.ts @@ -8,63 +8,46 @@ */ import { Controller } from '@hotwired/stimulus'; -import type { LoaderOptions } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader'; - -type MarkerId = number; +import type { InfoWindowDefinition, MapView, MarkerDefinition } from './map'; + +type GoogleMapsOptions = Pick< + google.maps.MapOptions, + | 'mapId' + | 'gestureHandling' + | 'backgroundColor' + | 'disableDoubleClickZoom' + | 'zoomControl' + | 'zoomControlOptions' + | 'mapTypeControl' + | 'mapTypeControlOptions' + | 'streetViewControl' + | 'streetViewControlOptions' + | 'fullscreenControl' + | 'fullscreenControlOptions' +>; export default class extends Controller { static values = { view: Object, }; - declare viewValue: { - mapId: string | null; - center: null | { lat: number; lng: number }; - zoom: number; - gestureHandling: string; - backgroundColor: string; - disableDoubleClickZoom: boolean; - zoomControl: boolean; - zoomControlOptions: google.maps.ZoomControlOptions; - mapTypeControl: boolean; - mapTypeControlOptions: google.maps.MapTypeControlOptions; - streetViewControl: boolean; - streetViewControlOptions: google.maps.StreetViewControlOptions; - fullscreenControl: boolean; - fullscreenControlOptions: google.maps.FullscreenControlOptions; - markers: Array<{ - _id: MarkerId; - position: { lat: number; lng: number }; - title: string | null; - }>; - infoWindows: Array<{ - headerContent: string | null; - content: string | null; - position: { lat: number; lng: number }; - opened: boolean; - _markerId: MarkerId | null; - autoClose: boolean; - }>; - fitBoundsToMarkers: boolean; - }; + declare viewValue: MapView; private loader: Loader; private map: google.maps.Map; - private markers = new Map(); + private markers: Array = []; private infoWindows: Array = []; initialize() { - const providerConfig = window.__symfony_ux_maps.providers?.google_maps; + const providerConfig = window.__symfony_ux_maps?.providers?.['google-maps']; if (!providerConfig) { throw new Error( 'Google Maps provider configuration is missing, did you forget to call `{{ ux_map_script_tags() }}`?' ); } - const loaderOptions: LoaderOptions = { - apiKey: providerConfig.key, - }; + const loaderOptions = providerConfig; this.dispatchEvent('init', { loaderOptions, @@ -74,106 +57,115 @@ export default class extends Controller { } async connect() { - const { Map: GoogleMap, InfoWindow } = await this.loader.importLibrary('maps'); - - const mapOptions: google.maps.MapOptions = { - gestureHandling: this.viewValue.gestureHandling, - backgroundColor: this.viewValue.backgroundColor, - disableDoubleClickZoom: this.viewValue.disableDoubleClickZoom, - zoomControl: this.viewValue.zoomControl, - zoomControlOptions: this.viewValue.zoomControlOptions, - mapTypeControl: this.viewValue.mapTypeControl, - mapTypeControlOptions: this.viewValue.mapTypeControlOptions, - streetViewControl: this.viewValue.streetViewControl, - streetViewControlOptions: this.viewValue.streetViewControlOptions, - fullscreenControl: this.viewValue.fullscreenControl, - fullscreenControlOptions: this.viewValue.fullscreenControlOptions, - }; + const { Map: GoogleMap } = await this.loader.importLibrary('maps'); + const { center, zoom, fitBoundsToMarkers, options, markers, infoWindows } = this.viewValue; - if (this.viewValue.mapId) { - mapOptions.mapId = this.viewValue.mapId; - } + this.dispatchEvent('pre-connect', { options }); - if (this.viewValue.center) { - mapOptions.center = this.viewValue.center; - } + this.map = new GoogleMap(this.element, { + ...options, + center, + zoom, + }); + this.createMarkers(markers, fitBoundsToMarkers); + this.createInfoWindows(infoWindows); + + this.dispatchEvent('connect', { + map: this.map, + markers: this.markers, + infoWindows: this.infoWindows, + }); + } + + private createMarkers(markers: Array, fitBoundsToMarkers: boolean) { + markers.forEach((definition) => this.createMarker(definition)); - if (this.viewValue.zoom) { - mapOptions.zoom = this.viewValue.zoom; + if (this.markers.length > 0 && fitBoundsToMarkers) { + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + + bounds.extend(marker.position); + }); + this.map.fitBounds(bounds); } + } - this.dispatchEvent('pre-connect', { - mapOptions, + private async createMarker(definition: MarkerDefinition) { + // Load the marker library on demand. Doing it twice won't make another HTTP request. + const { AdvancedMarkerElement } = await this.loader.importLibrary('marker'); + + const options = { + position: definition.position, + title: definition.title, + }; + this.dispatchEvent('marker:before-create', { options }); + + const marker = new AdvancedMarkerElement({ + ...options, + map: this.map, }); - this.map = new GoogleMap(this.element, mapOptions); + if (definition.infoWindow) { + this.createInfoWindow(definition.infoWindow, marker); + } - if (this.viewValue.markers) { - const { AdvancedMarkerElement } = await this.loader.importLibrary('marker'); + this.dispatchEvent('marker:after-create', { marker }); - this.viewValue.markers.forEach((markerConfiguration) => { - const marker = new AdvancedMarkerElement({ - position: markerConfiguration.position, - title: markerConfiguration.title, - map: this.map, - }); + this.markers.push(marker); + } - this.markers.set(markerConfiguration._id, marker); - }); + private createInfoWindows(infoWindows: Array) { + infoWindows.forEach((definition) => this.createInfoWindow(definition)); + } - if (this.viewValue.fitBoundsToMarkers) { - const bounds = new google.maps.LatLngBounds(); - this.markers.forEach((marker) => { - if (!marker.position) { - return; - } + private async createInfoWindow(definition: InfoWindowDefinition): Promise; + private async createInfoWindow( + definition: MarkerDefinition['infoWindow'], + marker: google.maps.marker.AdvancedMarkerElement + ): Promise; + private async createInfoWindow( + definition: InfoWindowDefinition, + marker?: google.maps.marker.AdvancedMarkerElement + ): Promise { + // Load the marker library on demand. Doing it twice won't make another HTTP request. + const { InfoWindow } = await this.loader.importLibrary('maps'); + const options = { + headerContent: this.createTextOrElement(definition.headerContent), + content: this.createTextOrElement(definition.content), + position: definition.position, + }; - bounds.extend(marker.position); - }); - this.map.fitBounds(bounds); - } - } + this.dispatchEvent('info-window:before-create', { options }); + + const infoWindow = new InfoWindow(options); - this.viewValue.infoWindows.forEach((infoWindowConfiguration) => { - const marker = infoWindowConfiguration._markerId - ? this.markers.get(infoWindowConfiguration._markerId) - : undefined; + this.infoWindows.push(infoWindow); - const infoWindow = new InfoWindow({ - headerContent: this.createTextOrElement(infoWindowConfiguration.headerContent), - content: this.createTextOrElement(infoWindowConfiguration.content), - position: infoWindowConfiguration.position, + if (definition.opened) { + infoWindow.open({ + map: this.map, + shouldFocus: false, + anchor: marker, }); + } - this.infoWindows.push(infoWindow); + if (marker) { + marker.addListener('click', () => { + if (definition.autoClose) { + this.closeInfoWindowsExcept(infoWindow); + } - if (infoWindowConfiguration.opened) { infoWindow.open({ map: this.map, - shouldFocus: false, anchor: marker, }); - } - - if (marker) { - marker.addListener('click', () => { - if (infoWindowConfiguration.autoClose) { - this.closeInfoWindowsExcept(infoWindow); - } - - infoWindow.open({ - map: this.map, - anchor: marker, - }); - }); - } - }); + }); + } - this.dispatchEvent('connect', { - map: this.map, - markers: this.markers, - infoWindows: this.infoWindows, - }); + this.dispatchEvent('info-window:after-create', { infoWindow }); } private createTextOrElement(content: string | null): string | HTMLElement | null { @@ -181,7 +173,8 @@ export default class extends Controller { return null; } - if (content.includes('<') /* we assume it's HTML if it includes "<" */) { + // we assume it's HTML if it includes "<" + if (content.includes('<')) { const div = document.createElement('div'); div.innerHTML = content; return div; diff --git a/src/Map/assets/src/leaflet_controller.ts b/src/Map/assets/src/leaflet_controller.ts index 24cd699f0bf..8bbb4c2db42 100644 --- a/src/Map/assets/src/leaflet_controller.ts +++ b/src/Map/assets/src/leaflet_controller.ts @@ -11,111 +11,122 @@ import { Controller } from '@hotwired/stimulus'; import 'leaflet/dist/leaflet.min.css'; -import type { Map as LeafletMap, MapOptions, Marker, MarkerOptions, Popup } from 'leaflet'; +import type { Map as LeafletMap, MapOptions, Marker, Popup } from 'leaflet'; import L from 'leaflet'; +import type { InfoWindowDefinition, MapView, MarkerDefinition } from './map'; -type MarkerId = number; +type LeafletOptions = Pick; +type AdditionalOptions = { + tileLayer: { url: string; attribution: string; options: Record }; +}; export default class extends Controller { static values = { view: Object, }; - declare viewValue: { - center: null | { lat: number; lng: number }; - zoom: number | null; - tileLayer: { url: string; attribution: string } & Record; - fitBoundsToMarkers: boolean; - markers: Array< - { - _id: MarkerId; - position: { lat: number; lng: number }; - } & MarkerOptions - >; - popups: Array<{ - _markerId: MarkerId | null; - content: string; - position: { lat: number; lng: number }; - opened: boolean; - autoClose: boolean; - }>; - }; + declare viewValue: MapView; private map: LeafletMap; - private markers = new Map(); - private popups: Array = []; + private markers: Array = []; + private infoWindows: Array = []; connect() { - const mapOptions: MapOptions = { - center: this.viewValue.center || undefined, - zoom: this.viewValue.zoom || undefined, - }; - - this.dispatchEvent('pre-connect', { - mapOptions, - }); + const { center, zoom, fitBoundsToMarkers, options, markers, infoWindows } = this.viewValue; - this.map = L.map(this.element, mapOptions); + this.dispatchEvent('pre-connect', { options }); - this.setupTileLayer(); + const { tileLayer, ...mapOptions } = this.viewValue.options; - this.viewValue.markers.forEach((markerConfiguration) => { - const { _id, position, ...options } = markerConfiguration; - const marker = L.marker(position, options).addTo(this.map); + this.map = L.map(this.element, { + ...mapOptions, + center, + zoom, + }); + this.createTileLayer(tileLayer); + this.createMarkers(markers, fitBoundsToMarkers); + this.createInfoWindows(infoWindows); - this.markers.set(_id, marker); + this.dispatchEvent('connect', { + map: this.map, + markers: this.markers, + infoWindows: this.infoWindows, }); + } - this.viewValue.popups.forEach((popupConfiguration) => { - let popup: Popup; - if (popupConfiguration._markerId) { - const marker = this.markers.get(popupConfiguration._markerId); - if (!marker) { - return; - } - marker.bindPopup(popupConfiguration.content, { - autoClose: popupConfiguration.autoClose, - }); - popup = marker.getPopup()!; - } else { - popup = L.popup({ - content: popupConfiguration.content, - autoClose: popupConfiguration.autoClose, - }); - - popup.setLatLng(popupConfiguration.position); - } + private createTileLayer(definition: AdditionalOptions['tileLayer']) { + const { url, attribution, options } = definition; - if (popupConfiguration.opened) { - popup.openOn(this.map); - } + L.tileLayer(url, { + attribution, + ...options, + }).addTo(this.map); + } - this.popups.push(popup); - }); + private createMarkers(markers: Array, fitBoundsToMarkers: boolean) { + markers.forEach((definition) => this.createMarker(definition)); - if (this.viewValue.fitBoundsToMarkers) { + if (fitBoundsToMarkers && this.markers.length > 0) { this.map.fitBounds( Array.from(this.markers.values()).map((marker) => { const position = marker.getLatLng(); + return [position.lat, position.lng]; }) ); } + } - this.dispatchEvent('connect', { - map: this.map, - markers: this.markers, - popups: this.popups, - }); + private createMarker(definition: MarkerDefinition) { + const { position, ...options } = definition; + + this.dispatchEvent('marker:before-create', { options }); + + const marker = L.marker(position, options).addTo(this.map); + + if (definition.infoWindow) { + this.createInfoWindow(definition.infoWindow, marker); + } + + this.dispatchEvent('marker:after-create', { marker }); + + this.markers.push(marker); } - private setupTileLayer() { - const { url, attribution, ...options } = this.viewValue.tileLayer; + private createInfoWindows(infoWindows: Array) { + infoWindows.forEach((definition) => this.createInfoWindow(definition)); + } - L.tileLayer(url, { - attribution, - ...options, - }).addTo(this.map); + private createInfoWindow(definition: InfoWindowDefinition): void; + private createInfoWindow(definition: MarkerDefinition['infoWindow'], marker: Marker): void; + private createInfoWindow(definition: InfoWindowDefinition, marker?: Marker): void { + let infoWindow: Popup; + const options = { ...definition }; + + this.dispatchEvent('info-window:before-create', { options }); + + const { headerContent, content, position, ...otherOptions } = options; + + if (marker) { + marker.bindPopup(headerContent + '
' + content, otherOptions); + if (definition.opened) { + marker.openPopup(); + } + + infoWindow = marker.getPopup()!; + } else { + infoWindow = L.popup(otherOptions) + .setContent(headerContent + '
' + content) + .setLatLng(position); + + if (definition.opened) { + infoWindow.openOn(this.map); + } + } + + this.infoWindows.push(infoWindow); + + this.dispatchEvent('info-window:after-create', { infoWindow }); } private dispatchEvent(name: string, payload: any) { diff --git a/src/Map/assets/src/map.d.ts b/src/Map/assets/src/map.d.ts new file mode 100644 index 00000000000..98df8cddcad --- /dev/null +++ b/src/Map/assets/src/map.d.ts @@ -0,0 +1,24 @@ +type LatLng = { lat: number; lng: number }; + +export type MapView = { + center: LatLng; + zoom: number; + fitBoundsToMarkers: boolean; + options: Options; + markers: Array; + infoWindows: Array; +}; + +export type MarkerDefinition = { + position: LatLng; + title: string | null; + infoWindow?: Omit; +}; + +export type InfoWindowDefinition = { + headerContent: string | null; + content: string | null; + position: LatLng; + opened: boolean; + autoClose: boolean; +}; diff --git a/src/Map/assets/test/google_maps_controller.test.ts b/src/Map/assets/test/google_maps_controller.test.ts index 527b6af5007..efe6b9ef1ee 100644 --- a/src/Map/assets/test/google_maps_controller.test.ts +++ b/src/Map/assets/test/google_maps_controller.test.ts @@ -39,7 +39,7 @@ describe('GoogleMapsController', () => {
`); }); @@ -51,7 +51,7 @@ describe('GoogleMapsController', () => { it('connect' , async () => { window.__symfony_ux_maps = { providers: { - google_maps: { + ['google-maps']: { key: '', }, }, diff --git a/src/Map/assets/test/leaflet_controller.test.ts b/src/Map/assets/test/leaflet_controller.test.ts index 7562397a6c7..3812a995f57 100644 --- a/src/Map/assets/test/leaflet_controller.test.ts +++ b/src/Map/assets/test/leaflet_controller.test.ts @@ -36,10 +36,10 @@ describe('LeafletController', () => { beforeEach(() => { container = mountDOM(` -
`); }); diff --git a/src/Map/composer.json b/src/Map/composer.json index a782ae1c98c..ace65bf6ec3 100644 --- a/src/Map/composer.json +++ b/src/Map/composer.json @@ -3,7 +3,10 @@ "type": "symfony-bundle", "description": "Easily embed interactive maps in your Symfony application", "keywords": [ - "symfony-ux" + "symfony-ux", + "map", + "markers", + "maps" ], "homepage": "https://symfony.com", "license": "MIT", @@ -28,15 +31,15 @@ } }, "require": { - "php": ">=8.1", + "php": ">=8.3", "symfony/stimulus-bundle": "^2.18.12" }, "require-dev": { "symfony/asset-mapper": "^6.4|^7.0", - "symfony/framework-bundle": "^5.4|^6.0|^7.0", - "symfony/phpunit-bridge": "^5.2|^6.0|^7.0", - "symfony/twig-bundle": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/twig-bundle": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "extra": { "thanks": { diff --git a/src/Map/config/asset_mapper.php b/src/Map/config/asset_mapper.php index 233ca11e4b5..7ccdee78973 100644 --- a/src/Map/config/asset_mapper.php +++ b/src/Map/config/asset_mapper.php @@ -11,13 +11,12 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -/* - * @author Hugo Alliaume - */ - use Symfony\UX\Map\AssetMapper\ImportMap\Compiler\LeafletReplaceImagesAssetCompiler; use Symfony\UX\Map\AssetMapper\ImportMap\Resolver\LeafletPackageResolver; +/* + * @author Hugo Alliaume + */ return static function (ContainerConfigurator $container): void { $container->services() ->set('ux_map.asset_mapper.leaflet_replace_images_compiler', LeafletReplaceImagesAssetCompiler::class) diff --git a/src/Map/config/services.php b/src/Map/config/services.php index e2a64bddf96..a6a5ad3f6c7 100644 --- a/src/Map/config/services.php +++ b/src/Map/config/services.php @@ -11,42 +11,39 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; -/* - * @author Hugo Alliaume - */ - -use Symfony\UX\Map\Configuration\Configuration; -use Symfony\UX\Map\Factory\MapFactory; -use Symfony\UX\Map\Factory\MapFactoryInterface; -use Symfony\UX\Map\Registry\MapRegistry; -use Symfony\UX\Map\Registry\MapRegistryInterface; +use Symfony\UX\Map\MapFactory; +use Symfony\UX\Map\MapRegistry; +use Symfony\UX\Map\Provider\GoogleMaps\GoogleMapsProviderFactory; +use Symfony\UX\Map\Provider\Leaflet\LeafletProviderFactory; +use Symfony\UX\Map\Provider\Provider; +use Symfony\UX\Map\Provider\ProviderInterface; use Symfony\UX\Map\Twig\MapExtension; use Symfony\UX\Map\Twig\MapRuntime; +/* + * @author Hugo Alliaume + */ return static function (ContainerConfigurator $container): void { $container->services() - ->set('ux_map.configuration', Configuration::class) - ->args([ - param('ux_map.config.providers'), - param('ux_map.config.maps'), - ]) - ->set('ux_map.map_factory', MapFactory::class) ->args([ - tagged_locator('ux_map.map_factory', 'name'), - service('ux_map.configuration'), + service('ux_map.default_provider'), service('ux_map.map_registry'), ]) - ->alias(MapFactoryInterface::class, 'ux_map.map_factory') + ->alias(MapFactory::class, 'ux_map.map_factory') - ->set('ux_map.google_maps.map_factory', \Symfony\UX\Map\Provider\GoogleMaps\MapFactory::class) - ->tag('ux_map.map_factory', ['name' => 'google_maps']) + ->set('ux_map.default_provider', ProviderInterface::class) + ->factory([service('ux_map.provider'), 'fromString']) + ->args([ + abstract_arg('provider configuration'), + ]) - ->set('ux_map.leaflet.map_factory', \Symfony\UX\Map\Provider\Leaflet\MapFactory::class) - ->tag('ux_map.map_factory', ['name' => 'leaflet']) + ->set('ux_map.provider', Provider::class) + ->args([ + tagged_iterator('ux_map.provider_factory', indexAttribute: 'name'), + ]) ->set('ux_map.map_registry', MapRegistry::class) - ->alias(MapRegistryInterface::class, 'ux_map.map_registry') ->set('ux_map.twig_extension', MapExtension::class) ->tag('twig.extension') @@ -55,8 +52,13 @@ ->args([ service('stimulus.helper'), service('ux_map.map_registry'), - service('ux_map.configuration'), ]) ->tag('twig.runtime') + + ->set('ux_map.google.provider_factory', GoogleMapsProviderFactory::class) + ->tag('ux_map.provider_factory', ['name' => 'google']) + + ->set('ux_map.leaflet.provider_factory', LeafletProviderFactory::class) + ->tag('ux_map.provider_factory', ['name' => 'leaflet']) ; }; diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst index 53a9c9ea9fb..0d361ba5907 100644 --- a/src/Map/doc/index.rst +++ b/src/Map/doc/index.rst @@ -41,206 +41,109 @@ After installing the bundle, ensure the line ``{{ ux_map_script_tags() }}`` is p {{ ux_map_script_tags() }} {% endblock %} -Usage ------ - Configuration -~~~~~~~~~~~~~ +------------- -Configuration is done in your ``config/packages/ux_map.yaml`` file, where you can define the providers and maps you want to use. - -Providers are the services that will be used to render the maps. -They can be configured with options that are specific to the provider, like the API key for Google Maps: +Configuration is done in your ``config/packages/ux_map.yaml`` file: .. code-block:: yaml + # config/packages/ux_map.yaml ux_map: - providers: - google_maps: - provider: google_maps - options: - key: '%env(GOOGLE_MAPS_API_KEY)%' - - leaflet: - provider: leaflet - -Maps are the actual maps that will be rendered. -They are configured with the provider they will use, and can have options that are specific to the map, -like the center and zoom level: - -.. code-block:: yaml - - ux_map: - maps: - google_maps_map_1: - provider: google_maps - options: - center: [48.8566, 2.3522] - zoom: 12 - - leaflet_map: - provider: leaflet - -.. note:: - - Even if it is possible to render several maps with different providers, - it will not be possible to render two maps with two providers of the same type - but with a different configuration, since they will conflict. - - -Google Maps -~~~~~~~~~~~ - -To use Google Maps on your application, you need to enable the Google Maps controller in your ``assets/controllers.json``: - -.. code-block:: json - - { - "controllers": { - "@symfony/ux-map": { - "google-maps": { - "enabled": true, - "fetch": "lazy" - }, - "leaflet": { - "enabled": false, - "fetch": "lazy" - } - }, - }, - "entrypoints": [] - } + provider: '%env(UX_MAP_DSN)%' +The ``UX_MAP_DSN`` environment variable should contain the provider DSN to use, e.g. ``google-maps://`` or ``leaflet://``. +See :ref:`Map providers` for more information on the available providers. -Then, you need to configure a new provider and a new map, in your ``config/packages/ux_map.yaml``: +Usage +----- -.. code-block:: yaml +Creating and rendering +~~~~~~~~~~~~~~~~~~~~~~ - ux_map: - providers: - google_maps: - provider: google_maps - options: - key: '%env(GOOGLE_MAPS_API_KEY)%' - - maps: - # With the default options - google_maps_map_1: - provider: google_maps - - # With all supported options - google_maps_map_2: - provider: google_maps - options: - mapId: 'DEMO_MAP_ID' - center: [48.8566, 2.3522] - zoom: 12 - gestureHandling: auto - backgroundColor: '#f8f9fa' - enableDoubleClickZoom: true - zoomControl: true - mapTypeControl: true - streetViewControl: true - fullscreenControl: true - fitBoundsToMarkers: true - -Then, you must create the Map instance in your PHP code (e.g. in a controller):: +To create a map, you must use the ``MapFactory`` service. +This service allows you to create a new ``Map`` instance by using the default configured provider:: namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; - use Symfony\UX\Map\Factory\MapFactoryInterface; + use Symfony\UX\Map\InfoWindow; use Symfony\UX\Map\LatLng; - use Symfony\UX\Map\Provider\GoogleMaps; + use Symfony\UX\Map\MapFactory; + use Symfony\UX\Map\Marker; final class ContactController extends AbstractController { #[Route('/contact')] - public function __invoke(MapFactoryInterface $mapFactory): Response + public function __invoke(MapFactory $mapFactory): Response { - // 1. The map is created with the factory, you must pass the map name you defined in the configuration (here 'google_maps_map_1'), - // you can also pass the map options as a second argument. - - /** @var GoogleMaps\Map $map */ - $map = $mapFactory->createMap('google_maps_map_1'); - - // 2. The map can be programmatically configured with a fluent API, you can change the center, zoom, configure controls, etc... - $map - ->setMapId("2b2d73ba4b8c7b41") - ->setCenter(new LatLng(46.903354, 1.888334)) - ->setZoom(6) - ->enableFitBoundsToMarkers() - ->enableStreetViewControl(false) - ->enableMapTypeControl(false) - ->setFullscreenControlOptions(new GoogleMaps\FullscreenControlOptions( - position: GoogleMaps\ControlPosition::BLOCK_START_INLINE_START, - )) - ->setZoomControlOptions(new GoogleMaps\ZoomControlOptions( - position: GoogleMaps\ControlPosition::BLOCK_START_INLINE_END, - )); - - // 3. You can add also add markers - $map - ->addMarker($paris = new GoogleMaps\Marker(position: new LatLng(48.8566, 2.3522), title: 'Paris')) - ->addMarker($lyon = new GoogleMaps\Marker(position: new LatLng(45.7640, 4.8357), title: 'Lyon')) - ->addMarker(new GoogleMaps\Marker(position: new LatLng(43.2965, 5.3698), title: 'Marseille')); - - // 4. You can also add info windows to the markers or to a position - $map - ->addInfoWindow(new GoogleMaps\InfoWindow( - headerContent: 'Paris', - content: "Capital of France, is a major European city and a world center for art, fashion, gastronomy and culture.", - marker: $paris, // Attach the info window to the marker, when the marker is clicked, the info window will open - opened: true, // Open the info window by default - )) - ->addInfoWindow(new GoogleMaps\InfoWindow( - headerContent: 'Lyon', - content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.', - marker: $lyon + // 1. Create a new map instance + $myMap = $mapFactory->createMap(); + + // 2. The map can be programmatically configured with a fluent API, you can change the center, zoom, and other options specific to the provider + $myMap + ->center(new LatLng(46.903354, 1.888334)) + ->zoom(6) + ->fitBoundsToMarkers() + ; + + // 3. You can also add markers + $myMap + ->addMarker(new Marker( + position: new LatLng(48.8566, 2.3522), + title: 'Paris' )) - ->addInfoWindow(new GoogleMaps\InfoWindow( - headerContent: 'Strasbourg', - content: "The French town of Alsace is home to the European Parliament and the Council of Europe.", - position: new LatLng(48.5846, 7.7507), // Attach the info window to a position, not to a marker + ->addMarker(new Marker( + position: new LatLng(45.7640, 4.8357), + title: 'Lyon', + // With an info window + infoWindow: new InfoWindow( + headerContent: 'Lyon', + content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.' + ) )); - ; - - // 4. Finally, you must inject the map in your template to render it + + // 4. and then, you must inject the map in your template to render it return $this->render('contact/index.html.twig', [ - 'map' => $map, + 'my_map' => $myMap, ]); } } -Finally, you can render the map in your Twig template: +To render a map in your Twig template, use the ``render_map`` Twig function, e.g.: .. code-block:: twig - {{ render_map(map) }} + {{ render_map(my_map) }} {# or with custom attributes #} - {{ render_map(map, { 'data-controller': 'my-map', style: 'height: 300px' }) }} + {{ render_map(my_map, { 'data-controller': 'my-map', style: 'height: 300px' }) }} -If everything went well, you should see a map with markers and info windows in your page. +Map providers +------------- Leaflet ~~~~~~~ -To use Google Maps on your application, you need to enable the Leaflet controller in your ``assets/controllers.json``: +You can use `Leaflet`_ as the default provider by configuring the ``UX_MAP_DSN`` environment variable like this: + +.. code-block:: env + + # .env + UX_MAP_DSN=leaflet://default -.. code-block:: json +Then, enable the Leaflet`_ Stimulus controller in your ``assets/controllers.json``: + +.. code-block:: diff { "controllers": { "@symfony/ux-map": { - "google-maps": { - "enabled": false, - "fetch": "lazy" - }, "leaflet": { - "enabled": true, + - "enabled": false, + + "enabled": true, "fetch": "lazy" } }, @@ -248,103 +151,87 @@ To use Google Maps on your application, you need to enable the Leaflet controlle "entrypoints": [] } +You can configure the map with specific Leaflet options by passing a ``LeafletOptions`` instance to the ``options`` method:: + + use Symfony\UX\Map\Provider\Leaflet\LeafletOptions; + use Symfony\UX\Map\Provider\Leaflet\Option as LeafletOption; + + $map + ->options(new LeafletOptions( + tileLayer: new LeafletOption\TileLayer( + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + attribution: '© OpenStreetMap contributors', + options: [ + 'maxZoom' => 19, + ] + ), + )); + +Google Maps +~~~~~~~~~~~ -Then, you need to configure a new provider and a new map, in your ``config/packages/ux_map.yaml``: +You can use `Google Maps`_ as the default provider by configuring the ``UX_MAP_DSN`` environment variable like this: -.. code-block:: yaml +.. code-block:: env - ux_map: - providers: - leaflet: - provider: leaflet - - maps: - # With the default options - leaflet_map_1: - provider: google_maps - - # With all supported options - leaflet_map_2: - provider: google_maps - options: - center: [48.8566, 2.3522] - zoom: 12 - tileLayer: - url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png' - attribution: '© OpenStreetMap contributors' - options: - maxZoom: 19 - fitBoundsToMarkers: true - -Then, you must create the Map instance in your PHP code (e.g. in a controller):: - - namespace App\Controller; - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Attribute\Route; - use Symfony\UX\Map\Factory\MapFactoryInterface; - use Symfony\UX\Map\LatLng; - use Symfony\UX\Map\Provider\Leaflet; - - final class ContactController extends AbstractController - { - #[Route('/contact')] - public function __invoke(MapFactoryInterface $mapFactory): Response - { - // 1. The map is created with the factory, you must pass the map name you defined in the configuration (here 'leaflet_map_1'), - // you can also pass the map options as a second argument. - - /** @var Leaflet\Map $map */ - $map = $mapFactory->createMap('leaflet_map_1'); - - // 2. The map can be programmatically configured with a fluent API, you can change the center, zoom, configure controls, etc... - $map - ->setCenter(new LatLng(46.903354, 1.888334)) - ->setZoom(6) - ->enableFitBoundsToMarkers(); - - // 3. You can add also add markers - $map - ->addMarker($paris = new Leaflet\Marker(position: new LatLng(48.8566, 2.3522), title: 'Paris')) - ->addMarker($lyon = new Leaflet\Marker(position: new LatLng(45.7640, 4.8357), title: 'Lyon')) - ->addMarker(new Leaflet\Marker(position: new LatLng(43.2965, 5.3698), title: 'Marseille')); - - // 4. You can also add popups to the markers or to a position - $map - ->addInfoWindow(new Leaflet\Popup( - content: "Paris, capital of France, is a major European city and a world center for art, fashion, gastronomy and culture.", - marker: $paris, // Attach the info window to the marker, when the marker is clicked, the info window will open - opened: true, // Open the info window by default - )) - ->addInfoWindow(new Leaflet\Popup( - content: 'Lyon, French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.', - marker: $lyon - )) - ->addInfoWindow(new Leaflet\Popup( - content: "Strasbourg, French town of Alsace is home to the European Parliament and the Council of Europe.", - position: new LatLng(48.5846, 7.7507), // Attach the info window to a position, not to a marker - )); - ; - - // 4. Finally, you must inject the map in your template to render it - return $this->render('contact/index.html.twig', [ - 'map' => $map, - ]); - } - } + # .env + UX_MAP_DSN=google-maps://GOOGLE_MAPS_API_KEY@default + UX_MAP_DSN=google-maps://GOOGLE_MAPS_API_KEY@default?version=weekly + UX_MAP_DSN=google-maps://GOOGLE_MAPS_API_KEY@default?language=fr®ion=FR -Finally, you can render the map in your Twig template: +The provider has a number of options: -.. code-block:: twig +====================== ====================================== =================================== + Option Description Default +====================== ====================================== =================================== +``id`` The id of the script tag __googleMapsScriptId +``language`` Force language, see The user's preferred language + `list of supported languages`_ specified in the browser +``region`` Unicode region subtag identifiers + compatible with `ISO 3166-1`_ +``nonce`` Use a cryptographic nonce attribute +``retries`` The number of script load retries 3 +``url`` Custom url to load the Google Maps API https://maps.googleapis.com/maps/api/js + script +``version`` The release channels or version weekly + numbers +====================== ====================================== =================================== - {{ render_map(map) }} - - {# or with custom attributes #} - {{ render_map(map, { 'data-controller': 'my-map', style: 'height: 300px' }) }} +Then, enable the `Google Maps`_ Stimulus controller in your ``assets/controllers.json``: -If everything went well, you should see a map with markers and popups in your page. +.. code-block:: diff + + { + "controllers": { + "@symfony/ux-map": { + "google-maps": { + - "enabled": false, + + "enabled": true, + "fetch": "lazy" + } + }, + }, + "entrypoints": [] + } +You can configure the map with specific Google Maps options by passing a ``GoogleOptions`` instance to the ``options``:: + + use Symfony\UX\Map\Provider\GoogleMaps\GoogleMapsOptions; + use Symfony\UX\Map\Provider\GoogleMaps\Option as GoogleMapsOption; + + $map + ->options(new GoogleMapsOptions( + mapId: '2b2d73ba4b8c7b41', // Enable markers and Google Maps Cloud Styles (https://developers.google.com/maps/documentation/cloud-customization) + zoomControlOptions: new GoogleMapsOption\ZoomControlOptions( + position: GoogleMapsOption\ControlPosition::BLOCK_START_INLINE_END, + ), + mapTypeControl: false, + streetViewControl: false, + fullscreenControlOptions: new GoogleMapsOption\FullscreenControlOptions( + position: GoogleMapsOption\ControlPosition::BLOCK_START_INLINE_START, + ), + )); + .. _using-with-asset-mapper: Using with AssetMapper @@ -380,3 +267,7 @@ https://symfony.com/doc/current/contributing/code/bc.html .. _`the Symfony UX initiative`: https://symfony.com/ux .. _StimulusBundle configured in your app: https://symfony.com/bundles/StimulusBundle/current/index.html +.. _`Leaflet`: https://leafletjs.com +.. _`Google Maps`: https://developers.google.com/maps/documentation/javascript/overview +.. _`ISO 3166-1`: https://en.wikipedia.org/wiki/ISO_3166-1 +.. _`list of supported languages`: https://developers.google.com/maps/faq#languagesupport diff --git a/src/Map/src/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompiler.php b/src/Map/src/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompiler.php index cc2a979bccd..f7823dfef4b 100644 --- a/src/Map/src/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompiler.php +++ b/src/Map/src/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompiler.php @@ -1,7 +1,5 @@ */ final class LeafletReplaceImagesAssetCompiler implements AssetCompilerInterface { @@ -46,7 +48,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac try { $resolvedSourcePath = Path::join(\dirname($asset->sourcePath), 'dist', 'images', $matches['asset']); } catch (RuntimeException $e) { - $this->logger?->warning(sprintf('Error processing import in "%s": ', $asset->sourcePath).$e->getMessage()); + $this->logger?->warning(\sprintf('Error processing import in "%s": ', $asset->sourcePath).$e->getMessage()); return $matches[0]; } diff --git a/src/Map/src/AssetMapper/ImportMap/Resolver/LeafletPackageResolver.php b/src/Map/src/AssetMapper/ImportMap/Resolver/LeafletPackageResolver.php index 3df8c1344d2..4ecb4ba85b9 100644 --- a/src/Map/src/AssetMapper/ImportMap/Resolver/LeafletPackageResolver.php +++ b/src/Map/src/AssetMapper/ImportMap/Resolver/LeafletPackageResolver.php @@ -1,7 +1,5 @@ */ class LeafletPackageResolver implements PackageResolverInterface { @@ -56,7 +58,7 @@ public function downloadPackages(array $importMapEntries, ?callable $progressCal $distPath = Path::join('dist', 'images', $leafletAsset); $responses[] = $this->httpClient->request( 'GET', - sprintf('https://cdn.jsdelivr.net/npm/leaflet@%s/%s', $importMapEntries['leaflet']->version, $distPath), + \sprintf('https://cdn.jsdelivr.net/npm/leaflet@%s/%s', $importMapEntries['leaflet']->version, $distPath), ['user_data' => ['dist_path' => $distPath]] ); } diff --git a/src/Map/src/Configuration/Configuration.php b/src/Map/src/Configuration/Configuration.php deleted file mode 100644 index fc084271609..00000000000 --- a/src/Map/src/Configuration/Configuration.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Configuration; - -use Symfony\UX\Map\Exception\ConflictingMapProvidersOnSamePageException; -use Symfony\UX\Map\Exception\MapNotFoundException; -use Symfony\UX\Map\Exception\ProviderNotFoundException; - -final class Configuration -{ - /** @var array */ - private readonly array $maps; - - /** - * @param array> $providersConfig - * @param array> $mapsConfig - */ - public function __construct( - array $providersConfig, - array $mapsConfig, - ) { - $providers = []; - foreach ($providersConfig as $providerName => $providerConfig) { - $providers[$providerName] = new Provider( - $providerName, - $providerConfig['provider'], - $providerConfig['options'] ?? [], - ); - } - - $maps = []; - foreach ($mapsConfig as $mapName => $mapConfig) { - $maps[$mapName] = new Map( - $mapName, - $mapConfig['options'] ?? [], - $providers[$mapConfig['provider']] ?? throw new ProviderNotFoundException($mapConfig['provider']), - ); - } - $this->maps = $maps; - } - - public function getMap(string $mapName): Map - { - return $this->maps[$mapName] ?? throw new MapNotFoundException($mapName); - } - - /** - * @param array $mapNames - * - * @throws ConflictingMapProvidersOnSamePageException if providers conflict with each other - */ - public function validateSimultaneousMapsUsage(array $mapNames): void - { - $usedProviders = []; - - foreach ($mapNames as $mapName) { - $map = $this->getMap($mapName); - - if (!\in_array($map->provider, $usedProviders, true)) { - $usedProviders[] = $map->provider; - } - } - - foreach ($usedProviders as $provider) { - $similarProviders = array_filter($usedProviders, fn (Provider $usedProvider) => $provider->provider === $usedProvider->provider && $provider !== $usedProvider); - - if ($similarProviders) { - throw new ConflictingMapProvidersOnSamePageException($provider->name, array_map(fn (Provider $similarProvider) => $similarProvider->name, $similarProviders)); - } - } - } -} diff --git a/src/Map/src/Configuration/Map.php b/src/Map/src/Configuration/Map.php deleted file mode 100644 index bc40625303a..00000000000 --- a/src/Map/src/Configuration/Map.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Configuration; - -final class Map -{ - /** - * @param array $options - */ - public function __construct( - public readonly string $name, - public readonly array $options, - public readonly Provider $provider, - ) { - } -} diff --git a/src/Map/src/Configuration/Provider.php b/src/Map/src/Configuration/Provider.php deleted file mode 100644 index 097032cee92..00000000000 --- a/src/Map/src/Configuration/Provider.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Configuration; - -final class Provider -{ - /** - * @param array $options - */ - public function __construct( - public readonly string $name, - public readonly string $provider, - public readonly array $options, - ) { - } -} diff --git a/src/Map/src/DependencyInjection/Configuration.php b/src/Map/src/DependencyInjection/Configuration.php deleted file mode 100644 index f304b51f6dd..00000000000 --- a/src/Map/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,144 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\DependencyInjection; - -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; -use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; - -/** - * @author Hugo Alliaume - * - * @experimental - */ -class Configuration implements ConfigurationInterface -{ - private const PROVIDERS = [ - 'google_maps' => [ - 'available_options' => [ - 'mapId', - 'center', - 'zoom', - 'gestureHandling', - 'backgroundColor', - 'enableDoubleClickZoom', - 'zoomControl', - 'mapTypeControl', - 'streetViewControl', - 'fullscreenControl', - 'fitBoundsToMarkers', - ], - ], - 'leaflet' => [ - 'available_options' => [ - 'center', - 'zoom', - 'tileLayer', - 'fitBoundsToMarkers', - ], - ], - ]; - - public function getConfigTreeBuilder(): TreeBuilder - { - $treeBuilder = new TreeBuilder('ux_map'); - $rootNode = $treeBuilder->getRootNode(); - $rootNode - ->children() - ->arrayNode('providers') - ->normalizeKeys(false) - ->useAttributeAsKey('name') - ->defaultValue([]) - ->arrayPrototype() - ->children() - ->scalarNode('provider') - ->isRequired() - ->validate() - ->ifNotInArray(array_keys(self::PROVIDERS)) - ->thenInvalid('The provider %s is not supported.') - ->end() - ->end() - ->arrayNode('options') - ->normalizeKeys(false) - ->defaultValue([]) - ->prototype('variable')->end() - ->end() - ->end() - ->validate() - ->ifTrue(function ($v) { return 'google_maps' === $v['provider'] && !isset($v['options']['key']); }) - ->thenInvalid('The "key" option is required for the "google_maps" provider.') - ->end() - ->end() - ->end() - - ->arrayNode('maps') - ->normalizeKeys(false) - ->useAttributeAsKey('name') - ->defaultValue([]) - ->arrayPrototype() - ->children() - ->scalarNode('provider')->isRequired()->end() - ->arrayNode('options') - ->normalizeKeys(false) - ->defaultValue([]) - ->prototype('variable') - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - - ->beforeNormalization() - ->always(function ($v) { - // Validate that the provider exists - foreach ($v['maps'] ?? [] as $mapName => $map) { - if (!isset($v['providers'][$map['provider']])) { - throw new InvalidArgumentException(sprintf('The provider "%s" for the map "%s" is not found, has it been correctly registered?', $map['provider'], $mapName)); - } - } - - foreach ($v['maps'] ?? [] as $map) { - $this->validateMapOptions($map, $v['providers'][$map['provider']]); - } - - return $v; - }) - ->end() - ; - - return $treeBuilder; - } - - private function validateMapOptions(array $map, array $provider): void - { - $availableOptions = self::PROVIDERS[$provider['provider']]['available_options'] ?? []; - $userOptions = array_keys($map['options'] ?? []); - $invalidOptions = array_diff($userOptions, $availableOptions); - - foreach ($invalidOptions as $invalidOption) { - $alternatives = []; - foreach ($availableOptions as $availableOption) { - $lev = levenshtein($invalidOption, $availableOption); - if ($lev <= \strlen($invalidOption) / 3 || str_contains($availableOption, $invalidOption)) { - $alternatives[] = $availableOption; - } - } - - if ($alternatives) { - throw new InvalidArgumentException(sprintf('The option "%s" is not supported for the provider "%s". Did you mean "%s"?', $invalidOption, $provider['provider'], implode('", "', $alternatives))); - } else { - throw new InvalidArgumentException(sprintf('The option "%s" is not supported for the provider "%s". Known options are "%s".', $invalidOption, $provider['provider'], implode('", "', $availableOptions))); - } - } - } -} diff --git a/src/Map/src/DependencyInjection/UXMapExtension.php b/src/Map/src/DependencyInjection/UXMapExtension.php index b88b8951c91..42502f87e02 100644 --- a/src/Map/src/DependencyInjection/UXMapExtension.php +++ b/src/Map/src/DependencyInjection/UXMapExtension.php @@ -12,6 +12,8 @@ namespace Symfony\UX\Map\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -20,17 +22,12 @@ /** * @author Hugo Alliaume - * - * @internal - * - * @experimental */ -class UXMapExtension extends Extension implements PrependExtensionInterface +class UXMapExtension extends Extension implements ConfigurationInterface, PrependExtensionInterface { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); + $config = $this->processConfiguration($this, $configs); $loader = (new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/../config'))); $loader->load('services.php'); @@ -39,11 +36,24 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('asset_mapper.php'); } - $container->setParameter('ux_map.config.providers', $config['providers']); - $container->setParameter('ux_map.config.maps', $config['maps']); + $container->getDefinition('ux_map.default_provider') + ->setArgument(0, $config['provider']); + } + + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('ux_map'); + $rootNode = $treeBuilder->getRootNode(); + $rootNode + ->children() + ->scalarNode('provider')->isRequired()->end() + ->end() + ; + + return $treeBuilder; } - public function prepend(ContainerBuilder $container) + public function prepend(ContainerBuilder $container): void { if (!$this->isAssetMapperAvailable($container)) { return; diff --git a/src/Map/src/Exception/ConflictingMapProvidersOnSamePageException.php b/src/Map/src/Exception/ConflictingMapProvidersOnSamePageException.php deleted file mode 100644 index ed3a53d0ced..00000000000 --- a/src/Map/src/Exception/ConflictingMapProvidersOnSamePageException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Exception; - -class ConflictingMapProvidersOnSamePageException extends RuntimeException implements Exception -{ - public function __construct(string $providerName, array $similarProvidersName) - { - parent::__construct(sprintf( - 'You cannot use the "%s" map provider on the same page as the following map providers: "%s", as their configuration will conflicts with each-other.', - $providerName, - implode('", "', $similarProvidersName) - )); - } -} diff --git a/src/Map/src/Exception/Exception.php b/src/Map/src/Exception/Exception.php index ac3ca93a93d..82e977ac9ea 100644 --- a/src/Map/src/Exception/Exception.php +++ b/src/Map/src/Exception/Exception.php @@ -1,7 +1,5 @@ + */ interface Exception extends \Throwable { } diff --git a/src/Map/src/Exception/MapNotFoundException.php b/src/Map/src/Exception/IncompleteDsnException.php similarity index 50% rename from src/Map/src/Exception/MapNotFoundException.php rename to src/Map/src/Exception/IncompleteDsnException.php index ed3a54ec406..a12d01a1a27 100644 --- a/src/Map/src/Exception/MapNotFoundException.php +++ b/src/Map/src/Exception/IncompleteDsnException.php @@ -1,7 +1,5 @@ + */ +final class IncompleteDsnException extends InvalidArgumentException { - public function __construct(string $name) - { - parent::__construct(sprintf('Map "%s" is not found, has it been correctly configured?', $name)); - } } diff --git a/src/Map/src/Exception/InvalidArgumentException.php b/src/Map/src/Exception/InvalidArgumentException.php index 5529fa5f467..aa280857e7b 100644 --- a/src/Map/src/Exception/InvalidArgumentException.php +++ b/src/Map/src/Exception/InvalidArgumentException.php @@ -1,7 +1,5 @@ + */ class InvalidArgumentException extends \InvalidArgumentException implements Exception { } diff --git a/src/Map/src/Exception/ProviderNotFoundException.php b/src/Map/src/Exception/ProviderNotFoundException.php deleted file mode 100644 index f00e94f3440..00000000000 --- a/src/Map/src/Exception/ProviderNotFoundException.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Exception; - -class ProviderNotFoundException extends \InvalidArgumentException implements Exception -{ - public function __construct(string $name) - { - parent::__construct(sprintf('Provider "%s" is not found, has it been correctly configured?', $name)); - } -} diff --git a/src/Map/src/Exception/RuntimeException.php b/src/Map/src/Exception/RuntimeException.php index 69f94e14f06..ec2b5ef8b14 100644 --- a/src/Map/src/Exception/RuntimeException.php +++ b/src/Map/src/Exception/RuntimeException.php @@ -1,7 +1,5 @@ + */ class RuntimeException extends \RuntimeException implements Exception { } diff --git a/src/Map/src/Exception/UnsupportedSchemeException.php b/src/Map/src/Exception/UnsupportedSchemeException.php new file mode 100644 index 00000000000..5df69483a2b --- /dev/null +++ b/src/Map/src/Exception/UnsupportedSchemeException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Exception; + +use Symfony\UX\Map\Provider\Dsn; + +/** + * @author Hugo Alliaume + */ +class UnsupportedSchemeException extends InvalidArgumentException +{ + public function __construct(Dsn $dsn, ?\Throwable $previous = null) + { + parent::__construct( + \sprintf('The provider "%s" is not supported.', $dsn->getScheme()), + 0, + $previous + ); + } +} diff --git a/src/Map/src/Factory/MapFactory.php b/src/Map/src/Factory/MapFactory.php deleted file mode 100644 index ac045b395e7..00000000000 --- a/src/Map/src/Factory/MapFactory.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Factory; - -use Psr\Container\ContainerInterface; -use Symfony\UX\Map\Configuration\Configuration; -use Symfony\UX\Map\Exception\ProviderNotFoundException; -use Symfony\UX\Map\MapInterface; -use Symfony\UX\Map\Registry\MapRegistryInterface; - -/** - * Creates a map based on the configuration, and registers it in the map registry. - */ -final class MapFactory implements MapFactoryInterface -{ - public function __construct( - private ContainerInterface $mapFactories, - private Configuration $configuration, - private MapRegistryInterface $mapRegistry, - ) { - } - - public function createMap(string $name, array $options = []): MapInterface - { - $mapConfig = $this->configuration->getMap($name); - - if (!$this->mapFactories->has($mapConfig->provider->provider)) { - throw new ProviderNotFoundException($mapConfig->provider->provider); - } - - $mapFactory = $this->mapFactories->get($mapConfig->provider->provider); - $map = $mapFactory->createMap($name, $options + $mapConfig->options); - - $this->mapRegistry->register($map); - - return $map; - } -} diff --git a/src/Map/src/InfoWindow.php b/src/Map/src/InfoWindow.php new file mode 100644 index 00000000000..1ea1b4cddd8 --- /dev/null +++ b/src/Map/src/InfoWindow.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map; + +/** + * Represents an information window that can be displayed on a map. + * + * @author Hugo Alliaume + */ +final readonly class InfoWindow +{ + public function __construct( + private ?string $headerContent = null, + private ?string $content = null, + private ?LatLng $position = null, + private bool $opened = false, + private bool $autoClose = true, + ) { + } + + public function toArray(): array + { + return [ + 'headerContent' => $this->headerContent, + 'content' => $this->content, + 'position' => $this->position?->toArray(), + 'opened' => $this->opened, + 'autoClose' => $this->autoClose, + ]; + } +} diff --git a/src/Map/src/LatLng.php b/src/Map/src/LatLng.php index e47b17b42d4..573b1c1b757 100644 --- a/src/Map/src/LatLng.php +++ b/src/Map/src/LatLng.php @@ -1,7 +1,5 @@ */ -final class LatLng +final readonly class LatLng { public function __construct( - public readonly float $latitude, - public readonly float $longitude, + public float $latitude, + public float $longitude, ) { if ($latitude < -90 || $latitude > 90) { - throw new InvalidArgumentException(sprintf('Latitude must be between -90 and 90 degrees, "%s" given.', $latitude)); + throw new InvalidArgumentException(\sprintf('Latitude must be between -90 and 90 degrees, "%s" given.', $latitude)); } if ($longitude < -180 || $longitude > 180) { - throw new InvalidArgumentException(sprintf('Longitude must be between -180 and 180 degrees, "%s" given.', $longitude)); + throw new InvalidArgumentException(\sprintf('Longitude must be between -180 and 180 degrees, "%s" given.', $longitude)); } } /** - * @return array{latitude: float, longitude: float} + * @return array{lat: float, lng: float} */ - public function createView(): array + public function toArray(): array { return [ 'lat' => $this->latitude, diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php new file mode 100644 index 00000000000..9063900352d --- /dev/null +++ b/src/Map/src/Map.php @@ -0,0 +1,134 @@ + + * + * 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; +use Symfony\UX\Map\Provider\ProviderInterface; + +/** + * Represents a map. + * + * @author Hugo Alliaume + */ +final class Map +{ + /** + * @var array + */ + private $attributes = []; + + public function __construct( + private readonly ProviderInterface $provider, + private MapOptionsInterface $options, + private ?LatLng $center = null, + private ?float $zoom = null, + private bool $fitBoundsToMarkers = false, + /** + * @var array + */ + private array $markers = [], + /** + * @var array + */ + private array $infoWindows = [], + ) { + } + + public function center(LatLng $center): self + { + $this->center = $center; + + return $this; + } + + public function zoom(float $zoom): self + { + $this->zoom = $zoom; + + return $this; + } + + public function fitBoundsToMarkers(bool $enable = true): self + { + $this->fitBoundsToMarkers = $enable; + + return $this; + } + + public function getProvider(): ProviderInterface + { + return $this->provider; + } + + public function options(MapOptionsInterface $options): self + { + $this->options = $options; + + return $this; + } + + public function addMarker(Marker $marker): self + { + $this->markers[] = $marker; + + return $this; + } + + public function addInfoWindow(InfoWindow $infoWindow): self + { + $this->infoWindows[] = $infoWindow; + + return $this; + } + + public function setAttributes(array $attributes): self + { + $this->attributes = $attributes; + + return $this; + } + + public function getMainDataController(): string + { + return $this->provider->getStimulusDataController(); + } + + public function getDataController(): ?string + { + return $this->attributes['data-controller'] ?? null; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + public function toArray(): array + { + if (null === $this->center) { + throw new InvalidArgumentException('The center of the map must be set.'); + } + + if (null === $this->zoom) { + throw new InvalidArgumentException('The zoom of the map must be set.'); + } + + return [ + 'center' => $this->center->toArray(), + 'zoom' => $this->zoom, + 'fitBoundsToMarkers' => $this->fitBoundsToMarkers, + 'options' => $this->options->toArray(), + 'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers), + 'infoWindows' => array_map(static fn (InfoWindow $infoWindow) => $infoWindow->toArray(), $this->infoWindows), + ]; + } +} diff --git a/src/Map/src/MapFactory.php b/src/Map/src/MapFactory.php new file mode 100644 index 00000000000..039bdc3251c --- /dev/null +++ b/src/Map/src/MapFactory.php @@ -0,0 +1,40 @@ + + * + * 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\Provider\ProviderInterface; + +/** + * Creates a map based on the configuration, and registers it in the map registry. + * + * @internal + * + * @author Hugo Alliaume + */ +final readonly class MapFactory +{ + public function __construct( + private ProviderInterface $defaultProvider, + private MapRegistry $mapRegistry, + ) { + } + + public function createMap(?ProviderInterface $provider = null): Map + { + $provider ??= $this->defaultProvider; + $map = new Map($provider, $provider::getDefaultMapOptions()); + + $this->mapRegistry->register($map); + + return $map; + } +} diff --git a/src/Map/src/MapInterface.php b/src/Map/src/MapInterface.php deleted file mode 100644 index a9ac1450243..00000000000 --- a/src/Map/src/MapInterface.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map; - -interface MapInterface -{ - public static function getMainDataController(): string; - - public function getName(): string; - - public function setAttributes(array $attributes): self; - - public function getDataController(): ?string; - - /** - * @return array - */ - public function getAttributes(): array; - - /** - * @return array - */ - public function createView(): array; -} diff --git a/src/Map/src/Factory/MapFactoryInterface.php b/src/Map/src/MapOptionsInterface.php similarity index 51% rename from src/Map/src/Factory/MapFactoryInterface.php rename to src/Map/src/MapOptionsInterface.php index e452cf52b9b..de7b1e20211 100644 --- a/src/Map/src/Factory/MapFactoryInterface.php +++ b/src/Map/src/MapOptionsInterface.php @@ -1,7 +1,5 @@ + */ +interface MapOptionsInterface { /** - * @param array $options + * @return array */ - public function createMap(string $name, array $options = []): MapInterface; + public function toArray(): array; } diff --git a/src/Map/src/Registry/MapRegistry.php b/src/Map/src/MapRegistry.php similarity index 67% rename from src/Map/src/Registry/MapRegistry.php rename to src/Map/src/MapRegistry.php index e1385feae0b..d7fad4d4f89 100644 --- a/src/Map/src/Registry/MapRegistry.php +++ b/src/Map/src/MapRegistry.php @@ -1,7 +1,5 @@ + */ +final class MapRegistry implements ResetInterface { /** - * @var array + * @var array */ private array $maps = []; - public function register(MapInterface $map): void + public function register(Map $map): void { $this->maps[] = $map; } diff --git a/src/Map/src/MapTrait.php b/src/Map/src/MapTrait.php deleted file mode 100644 index e1c80a3b4ca..00000000000 --- a/src/Map/src/MapTrait.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map; - -trait MapTrait -{ - private string $name; - - /** - * @var array - */ - private $attributes = []; - - public function getName(): string - { - return $this->name; - } - - public function setAttributes(array $attributes): self - { - $this->attributes = $attributes; - - return $this; - } - - public function getDataController(): ?string - { - return $this->attributes['data-controller'] ?? null; - } - - public function getAttributes(): array - { - return $this->attributes; - } -} diff --git a/src/Map/src/Marker.php b/src/Map/src/Marker.php new file mode 100644 index 00000000000..549a5f4b8cc --- /dev/null +++ b/src/Map/src/Marker.php @@ -0,0 +1,36 @@ + + * + * 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 marker on a map. + * + * @author Hugo Alliaume + */ +final readonly class Marker +{ + public function __construct( + private LatLng $position, + private ?string $title = null, + private ?InfoWindow $infoWindow = null, + ) { + } + + public function toArray(): array + { + return [ + 'position' => $this->position->toArray(), + 'title' => $this->title, + 'infoWindow' => $this->infoWindow?->toArray(), + ]; + } +} diff --git a/src/Map/src/Provider/AbstractProviderFactory.php b/src/Map/src/Provider/AbstractProviderFactory.php new file mode 100644 index 00000000000..8bcf980ec72 --- /dev/null +++ b/src/Map/src/Provider/AbstractProviderFactory.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +use Symfony\UX\Map\Exception\IncompleteDsnException; + +/** + * @author Hugo Alliaume + */ +abstract class AbstractProviderFactory +{ + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); + } + + protected function getUser(Dsn $dsn): string + { + return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.'); + } + + /** + * @return string[] + */ + abstract protected function getSupportedSchemes(): array; +} diff --git a/src/Map/src/Provider/Dsn.php b/src/Map/src/Provider/Dsn.php new file mode 100644 index 00000000000..41e8a65815b --- /dev/null +++ b/src/Map/src/Provider/Dsn.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +use Symfony\UX\Map\Exception\InvalidArgumentException; + +/** + * @author Hugo Alliaume + */ +final readonly class Dsn +{ + private string $scheme; + private string $host; + private ?string $user; + private array $options; + private string $originalDsn; + + public function __construct(#[\SensitiveParameter] string $dsn) + { + $this->originalDsn = $dsn; + + if (false === $params = parse_url($dsn)) { + throw new InvalidArgumentException('The map provider DSN is invalid.'); + } + + if (!isset($params['scheme'])) { + throw new InvalidArgumentException('The map provider DSN must contain a scheme.'); + } + $this->scheme = $params['scheme']; + + if (!isset($params['host'])) { + throw new InvalidArgumentException('The map provider DSN must contain a host (use "default" by default).'); + } + $this->host = $params['host']; + + $this->user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; + + $options = []; + parse_str($params['query'] ?? '', $options); + $this->options = $options; + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getOption(string $key, mixed $default = null): mixed + { + return $this->options[$key] ?? $default; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getOriginalDsn(): string + { + return $this->originalDsn; + } +} diff --git a/src/Map/src/Provider/GoogleMaps/GoogleMapsOptions.php b/src/Map/src/Provider/GoogleMaps/GoogleMapsOptions.php new file mode 100644 index 00000000000..9972f145746 --- /dev/null +++ b/src/Map/src/Provider/GoogleMaps/GoogleMapsOptions.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\GoogleMaps; + +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Provider\GoogleMaps\Option\FullscreenControlOptions; +use Symfony\UX\Map\Provider\GoogleMaps\Option\GestureHandling; +use Symfony\UX\Map\Provider\GoogleMaps\Option\MapTypeControlOptions; +use Symfony\UX\Map\Provider\GoogleMaps\Option\StreetViewControlOptions; +use Symfony\UX\Map\Provider\GoogleMaps\Option\ZoomControlOptions; + +/** + * @author Hugo Alliaume + */ +final class GoogleMapsOptions implements MapOptionsInterface +{ + public function __construct( + private ?string $mapId = null, + private GestureHandling $gestureHandling = GestureHandling::Auto, + private ?string $backgroundColor = null, + private bool $disableDoubleClickZoom = false, + private bool $zoomControl = true, + private ZoomControlOptions $zoomControlOptions = new ZoomControlOptions(), + private bool $mapTypeControl = true, + private MapTypeControlOptions $mapTypeControlOptions = new MapTypeControlOptions(), + private bool $streetViewControl = true, + private StreetViewControlOptions $streetViewControlOptions = new StreetViewControlOptions(), + private bool $fullscreenControl = true, + private FullscreenControlOptions $fullscreenControlOptions = new FullscreenControlOptions(), + ) { + } + + public function mapId(?string $mapId): self + { + $this->mapId = $mapId; + + return $this; + } + + public function gestureHandling(GestureHandling $gestureHandling): self + { + $this->gestureHandling = $gestureHandling; + + return $this; + } + + public function backgroundColor(?string $backgroundColor): self + { + $this->backgroundColor = $backgroundColor; + + return $this; + } + + public function doubleClickZoom(bool $enable = true): self + { + $this->disableDoubleClickZoom = !$enable; + + return $this; + } + + public function zoomControl(bool $enable = true): self + { + $this->zoomControl = $enable; + + return $this; + } + + public function zoomControlOptions(ZoomControlOptions $zoomControlOptions): self + { + $this->zoomControlOptions = $zoomControlOptions; + + return $this; + } + + public function mapTypeControl(bool $enable = true): self + { + $this->mapTypeControl = $enable; + + return $this; + } + + public function mapTypeControlOptions(MapTypeControlOptions $mapTypeControlOptions): self + { + $this->mapTypeControlOptions = $mapTypeControlOptions; + + return $this; + } + + public function streetViewControl(bool $enable = true): self + { + $this->streetViewControl = $enable; + + return $this; + } + + public function streetViewControlOptions(StreetViewControlOptions $streetViewControlOptions): self + { + $this->streetViewControlOptions = $streetViewControlOptions; + + return $this; + } + + public function fullscreenControl(bool $enable = true): self + { + $this->fullscreenControl = $enable; + + return $this; + } + + public function fullscreenControlOptions(FullscreenControlOptions $fullscreenControlOptions): self + { + $this->fullscreenControlOptions = $fullscreenControlOptions; + + return $this; + } + + public function toArray(): array + { + return [ + 'mapId' => $this->mapId, + 'gestureHandling' => $this->gestureHandling->value, + 'backgroundColor' => $this->backgroundColor, + 'disableDoubleClickZoom' => $this->disableDoubleClickZoom, + 'zoomControl' => $this->zoomControl, + 'zoomControlOptions' => $this->zoomControlOptions->toArray(), + 'mapTypeControl' => $this->mapTypeControl, + 'mapTypeControlOptions' => $this->mapTypeControlOptions->toArray(), + 'streetViewControl' => $this->streetViewControl, + 'streetViewControlOptions' => $this->streetViewControlOptions->toArray(), + 'fullscreenControl' => $this->fullscreenControl, + 'fullscreenControlOptions' => $this->fullscreenControlOptions->toArray(), + ]; + } +} diff --git a/src/Map/src/Provider/GoogleMaps/GoogleMapsProvider.php b/src/Map/src/Provider/GoogleMaps/GoogleMapsProvider.php new file mode 100644 index 00000000000..a01c696932d --- /dev/null +++ b/src/Map/src/Provider/GoogleMaps/GoogleMapsProvider.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\GoogleMaps; + +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Provider\ProviderInterface; +use Symfony\UX\Map\Provider\ProviderTrait; + +/** + * @author Hugo Alliaume + */ +final readonly class GoogleMapsProvider implements ProviderInterface +{ + use ProviderTrait; + + public static function getDefaultMapOptions(): MapOptionsInterface + { + return new GoogleMapsOptions(); + } + + /** + * Parameters are based from https://googlemaps.github.io/js-api-loader/interfaces/LoaderOptions.html documentation. + */ + public function __construct( + #[\SensitiveParameter] + private string $apiKey, + private ?string $id = null, + private ?string $language = null, + private ?string $region = null, + private ?string $nonce = null, + private ?int $retries = null, + private ?string $url = null, + private ?string $version = null, + ) { + } + + public function getName(): string + { + return 'google-maps'; + } + + public function getOptions(): array + { + return array_filter([ + 'id' => $this->id, + 'language' => $this->language, + 'region' => $this->region, + 'nonce' => $this->nonce, + 'retries' => $this->retries, + 'url' => $this->url, + 'version' => $this->version, + ]) + ['apiKey' => $this->apiKey]; + } + + public function __toString(): string + { + return \sprintf( + 'google-maps://%s@default/?%s', + str_repeat('*', \strlen($this->apiKey)), + http_build_query(array_filter([ + 'id' => $this->id, + 'language' => $this->language, + 'region' => $this->region, + 'nonce' => $this->nonce, + 'retries' => $this->retries, + 'url' => $this->url, + 'version' => $this->version, + ])) + ); + } +} diff --git a/src/Map/src/Provider/GoogleMaps/GoogleMapsProviderFactory.php b/src/Map/src/Provider/GoogleMaps/GoogleMapsProviderFactory.php new file mode 100644 index 00000000000..ec00c824db6 --- /dev/null +++ b/src/Map/src/Provider/GoogleMaps/GoogleMapsProviderFactory.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\GoogleMaps; + +use Symfony\UX\Map\Exception\InvalidArgumentException; +use Symfony\UX\Map\Exception\UnsupportedSchemeException; +use Symfony\UX\Map\Provider\AbstractProviderFactory; +use Symfony\UX\Map\Provider\Dsn; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Provider\ProviderInterface; + +/** + * @author Hugo Alliaume + */ +final class GoogleMapsProviderFactory extends AbstractProviderFactory implements ProviderFactoryInterface +{ + public function create(Dsn $dsn): ProviderInterface + { + if (!$this->supports($dsn)) { + throw new UnsupportedSchemeException($dsn); + } + + $apiKey = $dsn->getUser() ?: throw new InvalidArgumentException('The Google Maps provider requires an API key as the user part of the DSN.'); + + return new GoogleMapsProvider( + $apiKey, + id: $dsn->getOption('id'), + language: $dsn->getOption('language'), + region: $dsn->getOption('region'), + nonce: $dsn->getOption('nonce'), + retries: $dsn->getOption('retries'), + url: $dsn->getOption('url'), + version: $dsn->getOption('version', 'weekly'), + ); + } + + protected function getSupportedSchemes(): array + { + return ['google-maps']; + } +} diff --git a/src/Map/src/Provider/GoogleMaps/InfoWindow.php b/src/Map/src/Provider/GoogleMaps/InfoWindow.php deleted file mode 100644 index 6c398e232d0..00000000000 --- a/src/Map/src/Provider/GoogleMaps/InfoWindow.php +++ /dev/null @@ -1,54 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\GoogleMaps; - -use Symfony\UX\Map\Exception\InvalidArgumentException; -use Symfony\UX\Map\LatLng; - -/** - * Represents an info window on a Google Maps map. - * - * @see https://developers.google.com/maps/documentation/javascript/reference/info-window - */ -final class InfoWindow -{ - private readonly LatLng $position; - - public function __construct( - private readonly ?string $headerContent = null, - private readonly ?string $content = null, - ?LatLng $position = null, - private readonly ?Marker $marker = null, - private readonly bool $opened = false, - private readonly bool $autoClose = true, - ) { - if (null === $position && null === $marker) { - throw new InvalidArgumentException(sprintf('An "%s" must be associated with a position or a marker.', __CLASS__)); - } - - $this->position = $this->marker?->getPosition() ?? $position; - } - - public function createView(): array - { - return [ - 'headerContent' => $this->headerContent, - 'content' => $this->content, - 'position' => $this->position->createView(), - 'opened' => $this->opened, - 'autoClose' => $this->autoClose, - '_markerId' => $this->marker?->getId(), - ]; - } -} diff --git a/src/Map/src/Provider/GoogleMaps/Map.php b/src/Map/src/Provider/GoogleMaps/Map.php deleted file mode 100644 index 2a8b387c202..00000000000 --- a/src/Map/src/Provider/GoogleMaps/Map.php +++ /dev/null @@ -1,203 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\GoogleMaps; - -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\MapInterface; -use Symfony\UX\Map\MapTrait; - -/** - * Represents a Google Maps map. - * - * @see https://developers.google.com/maps/documentation/javascript/reference/map - */ -final class Map implements MapInterface -{ - use MapTrait; - - public static function getMainDataController(): string - { - return '@symfony/ux-map/google_maps'; - } - - /** - * @param array $markers - * @param array $infoWindows - */ - public function __construct( - private string $name, - // Google Maps specific options - private ?string $mapId = null, - private ?LatLng $center = null, - private ?float $zoom = null, - private GestureHandling $gestureHandling = GestureHandling::Auto, - private ?string $backgroundColor = null, - private bool $disableDoubleClickZoom = false, - private bool $zoomControl = true, - private ZoomControlOptions $zoomControlOptions = new ZoomControlOptions(), - private bool $mapTypeControl = true, - private MapTypeControlOptions $mapTypeControlOptions = new MapTypeControlOptions(), - private bool $streetViewControl = true, - private StreetViewControlOptions $streetViewControlOptions = new StreetViewControlOptions(), - private bool $fullscreenControl = true, - private FullscreenControlOptions $fullscreenControlOptions = new FullscreenControlOptions(), - // Custom options - private bool $fitBoundsToMarkers = false, - private array $markers = [], - private array $infoWindows = [], - ) { - } - - public function setMapId(string $mapId): self - { - $this->mapId = $mapId; - - return $this; - } - - public function setCenter(LatLng $center): self - { - $this->center = $center; - - return $this; - } - - public function setZoom(float $zoom): self - { - $this->zoom = $zoom; - - return $this; - } - - public function setGestureHandling(GestureHandling $gestureHandling): self - { - $this->gestureHandling = $gestureHandling; - - return $this; - } - - public function setBackgroundColor(string $backgroundColor): self - { - $this->backgroundColor = $backgroundColor; - - return $this; - } - - public function enableDoubleClickZoom(bool $enable = true): self - { - $this->disableDoubleClickZoom = !$enable; - - return $this; - } - - public function enableZoomControl(bool $enable = true): self - { - $this->zoomControl = $enable; - - return $this; - } - - public function setZoomControlOptions(ZoomControlOptions $zoomControlOptions): self - { - $this->zoomControlOptions = $zoomControlOptions; - - return $this; - } - - public function enableMapTypeControl(bool $enable = true): self - { - $this->mapTypeControl = $enable; - - return $this; - } - - public function setMapTypeControlOptions(MapTypeControlOptions $mapTypeControlOptions): self - { - $this->mapTypeControlOptions = $mapTypeControlOptions; - - return $this; - } - - public function enableStreetViewControl(bool $enable = true): self - { - $this->streetViewControl = $enable; - - return $this; - } - - public function setStreetViewControlOptions(StreetViewControlOptions $streetViewControlOptions): self - { - $this->streetViewControlOptions = $streetViewControlOptions; - - return $this; - } - - public function enableFullscreenControl(bool $enable = true): self - { - $this->fullscreenControl = $enable; - - return $this; - } - - public function setFullscreenControlOptions(FullscreenControlOptions $fullscreenControlOptions): self - { - $this->fullscreenControlOptions = $fullscreenControlOptions; - - return $this; - } - - public function enableFitBoundsToMarkers(bool $enable = true): self - { - $this->fitBoundsToMarkers = $enable; - - return $this; - } - - public function addMarker(Marker $marker): self - { - $this->markers[] = $marker; - - return $this; - } - - public function addInfoWindow(InfoWindow $infoWindow): self - { - $this->infoWindows[] = $infoWindow; - - return $this; - } - - public function createView(): array - { - return [ - 'mapId' => $this->mapId, - 'center' => $this->center?->createView(), - 'zoom' => $this->zoom, - 'gestureHandling' => $this->gestureHandling->value, - 'backgroundColor' => $this->backgroundColor, - 'disableDoubleClickZoom' => $this->disableDoubleClickZoom, - 'zoomControl' => $this->zoomControl, - 'zoomControlOptions' => $this->zoomControlOptions->createView(), - 'mapTypeControl' => $this->mapTypeControl, - 'mapTypeControlOptions' => $this->mapTypeControlOptions->createView(), - 'streetViewControl' => $this->streetViewControl, - 'streetViewControlOptions' => $this->streetViewControlOptions->createView(), - 'fullscreenControl' => $this->fullscreenControl, - 'fullscreenControlOptions' => $this->fullscreenControlOptions->createView(), - 'fitBoundsToMarkers' => $this->fitBoundsToMarkers, - 'markers' => array_map(fn (Marker $marker) => $marker->createView(), $this->markers), - 'infoWindows' => array_map(fn (InfoWindow $infoWindow) => $infoWindow->createView(), $this->infoWindows), - ]; - } -} diff --git a/src/Map/src/Provider/GoogleMaps/MapFactory.php b/src/Map/src/Provider/GoogleMaps/MapFactory.php deleted file mode 100644 index 376cc8c090e..00000000000 --- a/src/Map/src/Provider/GoogleMaps/MapFactory.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\GoogleMaps; - -use Symfony\UX\Map\Factory\MapFactoryInterface; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\MapInterface; - -/** - * Create a Google Maps map, with options. - */ -final class MapFactory implements MapFactoryInterface -{ - /** - * @param array{ - * mapId?: string, - * center?: array{float, float}, - * zoom?: numeric, - * gestureHandling?: bool, - * backgroundColor?: string, - * enableDoubleClickZoom?: bool, - * zoomControl?: bool, - * mapTypeControl?: bool, - * streetViewControl?: bool, - * fullscreenControl?: bool, - * fitBoundsToMarkers?: bool, - * } $options - */ - public function createMap(string $name, array $options = []): MapInterface - { - $map = new Map($name); - - if ($options['mapId'] ?? null) { - $map->setMapId($options['mapId']); - } - if ($options['center'] ?? null) { - $map->setCenter(new LatLng($options['center'][0], $options['center'][1])); - } - if ($options['zoom'] ?? null) { - $map->setZoom($options['zoom']); - } - if ($options['gestureHandling'] ?? null) { - $map->setGestureHandling(GestureHandling::from($options['gestureHandling'])); - } - if ($options['backgroundColor'] ?? null) { - $map->setBackgroundColor($options['backgroundColor']); - } - if ($options['enableDoubleClickZoom'] ?? null) { - $map->enableDoubleClickZoom($options['enableDoubleClickZoom']); - } - if ($options['zoomControl'] ?? null) { - $map->enableZoomControl($options['zoomControl']); - } - if ($options['mapTypeControl'] ?? null) { - $map->enableMapTypeControl($options['mapTypeControl']); - } - if ($options['streetViewControl'] ?? null) { - $map->enableStreetViewControl($options['streetViewControl']); - } - if ($options['fullscreenControl'] ?? null) { - $map->enableFullscreenControl($options['fullscreenControl']); - } - if ($options['fitBoundsToMarkers'] ?? null) { - $map->enableFitBoundsToMarkers($options['fitBoundsToMarkers']); - } - - return $map; - } -} diff --git a/src/Map/src/Provider/GoogleMaps/Marker.php b/src/Map/src/Provider/GoogleMaps/Marker.php deleted file mode 100644 index a8792a49545..00000000000 --- a/src/Map/src/Provider/GoogleMaps/Marker.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\GoogleMaps; - -use Symfony\UX\Map\LatLng; - -/** - * Represents a marker on a Google Maps map. - * - * @see https://developers.google.com/maps/documentation/javascript/reference/advanced-markers - */ -final class Marker -{ - private int $id; - - public function __construct( - private readonly LatLng $position, - private readonly ?string $title = null, - ) { - $this->id = spl_object_id($this); - } - - public function getId(): int - { - return $this->id; - } - - public function getPosition(): LatLng - { - return $this->position; - } - - public function createView(): array - { - return [ - '_id' => $this->id, - 'position' => $this->position->createView(), - 'title' => $this->title, - ]; - } -} diff --git a/src/Map/src/Provider/GoogleMaps/ControlPosition.php b/src/Map/src/Provider/GoogleMaps/Option/ControlPosition.php similarity index 98% rename from src/Map/src/Provider/GoogleMaps/ControlPosition.php rename to src/Map/src/Provider/GoogleMaps/Option/ControlPosition.php index 5904a005efa..5b536eb8eaf 100644 --- a/src/Map/src/Provider/GoogleMaps/ControlPosition.php +++ b/src/Map/src/Provider/GoogleMaps/Option/ControlPosition.php @@ -1,7 +1,5 @@ */ enum ControlPosition: int { diff --git a/src/Map/src/Provider/GoogleMaps/FullscreenControlOptions.php b/src/Map/src/Provider/GoogleMaps/Option/FullscreenControlOptions.php similarity index 83% rename from src/Map/src/Provider/GoogleMaps/FullscreenControlOptions.php rename to src/Map/src/Provider/GoogleMaps/Option/FullscreenControlOptions.php index e740e0319c9..8a8f8d1c3f1 100644 --- a/src/Map/src/Provider/GoogleMaps/FullscreenControlOptions.php +++ b/src/Map/src/Provider/GoogleMaps/Option/FullscreenControlOptions.php @@ -1,7 +1,5 @@ */ final class FullscreenControlOptions { @@ -25,7 +25,7 @@ public function __construct( ) { } - public function createView(): array + public function toArray(): array { return [ 'position' => $this->position->value, diff --git a/src/Map/src/Provider/GoogleMaps/GestureHandling.php b/src/Map/src/Provider/GoogleMaps/Option/GestureHandling.php similarity index 92% rename from src/Map/src/Provider/GoogleMaps/GestureHandling.php rename to src/Map/src/Provider/GoogleMaps/Option/GestureHandling.php index 177b72a2d40..9dcb00b48bd 100644 --- a/src/Map/src/Provider/GoogleMaps/GestureHandling.php +++ b/src/Map/src/Provider/GoogleMaps/Option/GestureHandling.php @@ -1,7 +1,5 @@ */ enum GestureHandling: string { diff --git a/src/Map/src/Provider/GoogleMaps/MapTypeControlOptions.php b/src/Map/src/Provider/GoogleMaps/Option/MapTypeControlOptions.php similarity index 89% rename from src/Map/src/Provider/GoogleMaps/MapTypeControlOptions.php rename to src/Map/src/Provider/GoogleMaps/Option/MapTypeControlOptions.php index e3a2063b6da..2c4036ef49e 100644 --- a/src/Map/src/Provider/GoogleMaps/MapTypeControlOptions.php +++ b/src/Map/src/Provider/GoogleMaps/Option/MapTypeControlOptions.php @@ -1,7 +1,5 @@ */ final class MapTypeControlOptions { @@ -30,7 +30,7 @@ public function __construct( ) { } - public function createView(): array + public function toArray(): array { return [ 'mapTypeIds' => array_map( diff --git a/src/Map/src/Provider/GoogleMaps/MapTypeControlStyle.php b/src/Map/src/Provider/GoogleMaps/Option/MapTypeControlStyle.php similarity index 89% rename from src/Map/src/Provider/GoogleMaps/MapTypeControlStyle.php rename to src/Map/src/Provider/GoogleMaps/Option/MapTypeControlStyle.php index 11fb810de3f..f9f4fc6b067 100644 --- a/src/Map/src/Provider/GoogleMaps/MapTypeControlStyle.php +++ b/src/Map/src/Provider/GoogleMaps/Option/MapTypeControlStyle.php @@ -1,7 +1,5 @@ */ enum MapTypeControlStyle: int { diff --git a/src/Map/src/Provider/GoogleMaps/MapTypeId.php b/src/Map/src/Provider/GoogleMaps/Option/MapTypeId.php similarity index 89% rename from src/Map/src/Provider/GoogleMaps/MapTypeId.php rename to src/Map/src/Provider/GoogleMaps/Option/MapTypeId.php index 7ec592b9fba..25b8d01486c 100644 --- a/src/Map/src/Provider/GoogleMaps/MapTypeId.php +++ b/src/Map/src/Provider/GoogleMaps/Option/MapTypeId.php @@ -1,7 +1,5 @@ */ enum MapTypeId: string { diff --git a/src/Map/src/Provider/GoogleMaps/StreetViewControlOptions.php b/src/Map/src/Provider/GoogleMaps/Option/StreetViewControlOptions.php similarity index 83% rename from src/Map/src/Provider/GoogleMaps/StreetViewControlOptions.php rename to src/Map/src/Provider/GoogleMaps/Option/StreetViewControlOptions.php index 71ec54ee1ed..7583744846c 100644 --- a/src/Map/src/Provider/GoogleMaps/StreetViewControlOptions.php +++ b/src/Map/src/Provider/GoogleMaps/Option/StreetViewControlOptions.php @@ -1,7 +1,5 @@ */ final class StreetViewControlOptions { @@ -25,7 +25,7 @@ public function __construct( ) { } - public function createView(): array + public function toArray(): array { return [ 'position' => $this->position->value, diff --git a/src/Map/src/Provider/GoogleMaps/ZoomControlOptions.php b/src/Map/src/Provider/GoogleMaps/Option/ZoomControlOptions.php similarity index 83% rename from src/Map/src/Provider/GoogleMaps/ZoomControlOptions.php rename to src/Map/src/Provider/GoogleMaps/Option/ZoomControlOptions.php index 0f79634ab1c..ffa2f4b88ec 100644 --- a/src/Map/src/Provider/GoogleMaps/ZoomControlOptions.php +++ b/src/Map/src/Provider/GoogleMaps/Option/ZoomControlOptions.php @@ -1,7 +1,5 @@ */ final class ZoomControlOptions { @@ -25,7 +25,7 @@ public function __construct( ) { } - public function createView(): array + public function toArray(): array { return [ 'position' => $this->position->value, diff --git a/src/Map/src/Provider/Leaflet/LeafletOptions.php b/src/Map/src/Provider/Leaflet/LeafletOptions.php new file mode 100644 index 00000000000..cf51420a00b --- /dev/null +++ b/src/Map/src/Provider/Leaflet/LeafletOptions.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\Leaflet; + +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Provider\Leaflet\Option\TileLayer; + +/** + * @author Hugo Alliaume + */ +final readonly class LeafletOptions implements MapOptionsInterface +{ + public function __construct( + private ?TileLayer $tileLayer = new TileLayer( + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + attribution: '© OpenStreetMap', + ), + ) { + } + + public function toArray(): array + { + return [ + 'tileLayer' => $this->tileLayer->toArray(), + ]; + } +} diff --git a/src/Map/src/Provider/Leaflet/LeafletProvider.php b/src/Map/src/Provider/Leaflet/LeafletProvider.php new file mode 100644 index 00000000000..3e69656c69e --- /dev/null +++ b/src/Map/src/Provider/Leaflet/LeafletProvider.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\Leaflet; + +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Provider\ProviderInterface; +use Symfony\UX\Map\Provider\ProviderTrait; + +/** + * @author Hugo Alliaume + */ +final readonly class LeafletProvider implements ProviderInterface +{ + use ProviderTrait; + + public static function getDefaultMapOptions(): MapOptionsInterface + { + return new LeafletOptions(); + } + + public function getName(): string + { + return 'leaflet'; + } + + public function getOptions(): array + { + return []; + } + + public function __toString() + { + return 'leaflet://default'; + } +} diff --git a/src/Map/src/Provider/Leaflet/LeafletProviderFactory.php b/src/Map/src/Provider/Leaflet/LeafletProviderFactory.php new file mode 100644 index 00000000000..b4cdfd83ac4 --- /dev/null +++ b/src/Map/src/Provider/Leaflet/LeafletProviderFactory.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider\Leaflet; + +use Symfony\UX\Map\Exception\UnsupportedSchemeException; +use Symfony\UX\Map\Provider\AbstractProviderFactory; +use Symfony\UX\Map\Provider\Dsn; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Provider\ProviderInterface; + +/** + * @author Hugo Alliaume + */ +final class LeafletProviderFactory extends AbstractProviderFactory implements ProviderFactoryInterface +{ + public function create(Dsn $dsn): ProviderInterface + { + if (!$this->supports($dsn)) { + throw new UnsupportedSchemeException($dsn); + } + + return new LeafletProvider(); + } + + protected function getSupportedSchemes(): array + { + return ['leaflet']; + } +} diff --git a/src/Map/src/Provider/Leaflet/Map.php b/src/Map/src/Provider/Leaflet/Map.php deleted file mode 100644 index 23b9a69e99b..00000000000 --- a/src/Map/src/Provider/Leaflet/Map.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\Leaflet; - -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\MapInterface; -use Symfony\UX\Map\MapTrait; - -/** - * Represents a Leaflet map. - * - * @see https://leafletjs.com/reference.html#map - */ -final class Map implements MapInterface -{ - use MapTrait; - - public static function getMainDataController(): string - { - return '@symfony/ux-map/leaflet'; - } - - /** - * @param array $markers - * @param array $popups - */ - public function __construct( - private string $name, - // Leaflet options - private TileLayer $tileLayer, - private ?LatLng $center = null, - private ?float $zoom = null, - // Custom options - private bool $fitBoundsToMarkers = false, - private array $markers = [], - private array $popups = [], - ) { - } - - public function setCenter(?LatLng $center): self - { - $this->center = $center; - - return $this; - } - - public function setZoom(?float $zoom): self - { - $this->zoom = $zoom; - - return $this; - } - - public function setTileLayer(TileLayer $tileLayer): self - { - $this->tileLayer = $tileLayer; - - return $this; - } - - public function enableFitBoundsToMarkers(bool $enable = true): self - { - $this->fitBoundsToMarkers = $enable; - - return $this; - } - - public function addMarker(Marker $marker): self - { - $this->markers[] = $marker; - - return $this; - } - - public function addPopup(Popup $popup): self - { - $this->popups[] = $popup; - - return $this; - } - - public function createView(): array - { - return [ - 'center' => $this->center?->createView(), - 'zoom' => $this->zoom, - 'tileLayer' => $this->tileLayer?->createView(), - 'fitBoundsToMarkers' => $this->fitBoundsToMarkers, - 'markers' => array_map(fn (Marker $marker) => $marker->createView(), $this->markers), - 'popups' => array_map(fn (Popup $popup) => $popup->createView(), $this->popups), - ]; - } -} diff --git a/src/Map/src/Provider/Leaflet/MapFactory.php b/src/Map/src/Provider/Leaflet/MapFactory.php deleted file mode 100644 index 1508342095a..00000000000 --- a/src/Map/src/Provider/Leaflet/MapFactory.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\Leaflet; - -use Symfony\UX\Map\Factory\MapFactoryInterface; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\MapInterface; - -/** - * Creates a Leaflet map, with options. - */ -final class MapFactory implements MapFactoryInterface -{ - /** - * @param array{ - * center?: array{float, float}, - * zoom?: numeric, - * tileLayer?: array{ url?: string, attribution?: string, options?: array }, - * fitBoundsToMarkers?: bool, - * } $options - */ - public function createMap(string $name, array $options = []): MapInterface - { - $tileLayer = array_merge([ - 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap', - 'options' => [], - ], $options['tileLayer'] ?? []); - - $map = new Map( - $name, - tileLayer: new TileLayer($tileLayer['url'], $tileLayer['attribution'], $tileLayer['options']) - ); - - if ($options['center'] ?? null) { - $map->setCenter(new LatLng($options['center'][0], $options['center'][1])); - } - if ($options['zoom'] ?? null) { - $map->setZoom($options['zoom']); - } - if ($options['fitBoundsToMarkers'] ?? null) { - $map->enableFitBoundsToMarkers($options['fitBoundsToMarkers']); - } - - return $map; - } -} diff --git a/src/Map/src/Provider/Leaflet/Marker.php b/src/Map/src/Provider/Leaflet/Marker.php deleted file mode 100644 index 08363fd857b..00000000000 --- a/src/Map/src/Provider/Leaflet/Marker.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\Leaflet; - -use Symfony\UX\Map\LatLng; - -/** - * Represents a marker on a Leaflet map. - * - * @see https://leafletjs.com/reference.html#marker - */ -final class Marker -{ - private int $id; - - public function __construct( - private readonly LatLng $position, - private readonly string $title = '', - private readonly bool $riseOnHover = false, - private readonly int $riseOffset = 250, - private readonly bool $draggable = false, - ) { - $this->id = spl_object_id($this); - } - - public function getId(): int - { - return $this->id; - } - - public function getPosition(): LatLng - { - return $this->position; - } - - public function createView(): array - { - return [ - '_id' => $this->id, - 'position' => $this->position->createView(), - 'title' => $this->title, - 'riseOnHover' => $this->riseOnHover, - 'riseOffset' => $this->riseOffset, - 'draggable' => $this->draggable, - ]; - } -} diff --git a/src/Map/src/Provider/Leaflet/TileLayer.php b/src/Map/src/Provider/Leaflet/Option/TileLayer.php similarity index 78% rename from src/Map/src/Provider/Leaflet/TileLayer.php rename to src/Map/src/Provider/Leaflet/Option/TileLayer.php index 4f6b16c34b4..9930a7786e9 100644 --- a/src/Map/src/Provider/Leaflet/TileLayer.php +++ b/src/Map/src/Provider/Leaflet/Option/TileLayer.php @@ -1,7 +1,5 @@ */ final class TileLayer { @@ -27,12 +27,12 @@ public function __construct( ) { } - public function createView(): array + public function toArray(): array { return [ - ...$this->options, 'url' => $this->url, 'attribution' => $this->attribution, + 'options' => $this->options, ]; } } diff --git a/src/Map/src/Provider/Leaflet/Popup.php b/src/Map/src/Provider/Leaflet/Popup.php deleted file mode 100644 index 892ecbbf989..00000000000 --- a/src/Map/src/Provider/Leaflet/Popup.php +++ /dev/null @@ -1,52 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Provider\Leaflet; - -use Symfony\UX\Map\Exception\InvalidArgumentException; -use Symfony\UX\Map\LatLng; - -/** - * Represents a popup on a Leaflet map. - * - * @see https://leafletjs.com/reference.html#popup - */ -final class Popup -{ - private readonly LatLng $position; - - public function __construct( - private readonly string $content, - ?LatLng $position = null, - private readonly ?Marker $marker = null, - private readonly bool $opened = false, - private readonly bool $autoClose = true, - ) { - if (null === $position && null === $marker) { - throw new InvalidArgumentException(sprintf('An "%s" must be associated with a position or a marker.', __CLASS__)); - } - - $this->position = $this->marker?->getPosition() ?? $position; - } - - public function createView(): array - { - return [ - '_markerId' => $this->marker?->getId(), - 'content' => $this->content, - 'position' => $this->position->createView(), - 'opened' => $this->opened, - 'autoClose' => $this->autoClose, - ]; - } -} diff --git a/src/Map/src/Provider/Provider.php b/src/Map/src/Provider/Provider.php new file mode 100644 index 00000000000..b8966bbbcb4 --- /dev/null +++ b/src/Map/src/Provider/Provider.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +use Symfony\UX\Map\Exception\UnsupportedSchemeException; + +/** + * @author Hugo Alliaume + */ +final readonly class Provider +{ + /** + * @var array + */ + private array $factories; + + public function __construct(iterable $factoriesIterator) + { + $factories = []; + foreach ($factoriesIterator as $name => $factory) { + $factories[$name] = $factory; + } + $this->factories = $factories; + } + + public function fromString(#[\SensitiveParameter] string $dsn): ProviderInterface + { + return $this->fromDsnObject(new Dsn($dsn)); + } + + public function fromDsnObject(Dsn $dsn): ProviderInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return $factory->create($dsn); + } + } + + throw new UnsupportedSchemeException($dsn); + } +} diff --git a/src/Map/src/Provider/ProviderFactoryInterface.php b/src/Map/src/Provider/ProviderFactoryInterface.php new file mode 100644 index 00000000000..6f1a270f9df --- /dev/null +++ b/src/Map/src/Provider/ProviderFactoryInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +/** + * @author Hugo Alliaume + */ +interface ProviderFactoryInterface +{ + public function create(Dsn $dsn): ProviderInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/src/Map/src/Provider/ProviderInterface.php b/src/Map/src/Provider/ProviderInterface.php new file mode 100644 index 00000000000..9849d0f6a17 --- /dev/null +++ b/src/Map/src/Provider/ProviderInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +use Symfony\UX\Map\MapOptionsInterface; + +/** + * @author Hugo Alliaume + */ +interface ProviderInterface extends \Stringable +{ + public static function getDefaultMapOptions(): MapOptionsInterface; + + public function getName(): string; + + public function getOptions(): array; + + public function getStimulusDataController(): string; +} diff --git a/src/Map/src/Provider/ProviderTrait.php b/src/Map/src/Provider/ProviderTrait.php new file mode 100644 index 00000000000..28141d54d17 --- /dev/null +++ b/src/Map/src/Provider/ProviderTrait.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Provider; + +/** + * @author Hugo Alliaume + */ +trait ProviderTrait +{ + public function getStimulusDataController(): string + { + return '@symfony/ux-map/'.$this->getName(); + } +} diff --git a/src/Map/src/Registry/MapRegistryInterface.php b/src/Map/src/Registry/MapRegistryInterface.php deleted file mode 100644 index 547e66eea53..00000000000 --- a/src/Map/src/Registry/MapRegistryInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Registry; - -use Symfony\UX\Map\MapInterface; - -interface MapRegistryInterface -{ - public function register(MapInterface $map): void; - - /** - * @return iterable - */ - public function all(): array; -} diff --git a/src/Map/src/Test/ProviderFactoryTestCase.php b/src/Map/src/Test/ProviderFactoryTestCase.php new file mode 100644 index 00000000000..3b516e501d7 --- /dev/null +++ b/src/Map/src/Test/ProviderFactoryTestCase.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Exception\IncompleteDsnException; +use Symfony\UX\Map\Exception\UnsupportedSchemeException; +use Symfony\UX\Map\Provider\Dsn; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Provider\ProviderInterface; + +/** + * A test case to ease testing a provider factory. + * + * @author Oskar Stark + * @author Hugo Alliaume + */ +abstract class ProviderFactoryTestCase extends TestCase +{ + abstract public function createFactory(): ProviderFactoryInterface; + + /** + * @return iterable + */ + abstract public static function supportsProvider(): iterable; + + /** + * @return iterable + */ + abstract public static function createProvider(): iterable; + + /** + * @return iterable + */ + public static function unsupportedSchemeProvider(): iterable + { + return []; + } + + /** + * @return iterable + */ + public static function incompleteDsnProvider(): iterable + { + return []; + } + + /** + * @dataProvider supportsProvider + */ + public function testSupports(bool $expected, string $dsn) + { + $factory = $this->createFactory(); + + $this->assertSame($expected, $factory->supports(new Dsn($dsn))); + } + + /** + * @dataProvider createProvider + */ + public function testCreate(string $expected, string $dsn) + { + $factory = $this->createFactory(); + $provider = $factory->create(new Dsn($dsn)); + + $this->assertSame($expected, (string) $provider); + } + + /** + * @dataProvider unsupportedSchemeProvider + */ + public function testUnsupportedSchemeException(string $dsn, ?string $message = null) + { + $factory = $this->createFactory(); + + $dsn = new Dsn($dsn); + + $this->expectException(UnsupportedSchemeException::class); + if (null !== $message) { + $this->expectExceptionMessage($message); + } + + $factory->create($dsn); + } + + /** + * @dataProvider incompleteDsnProvider + */ + public function testIncompleteDsnException(string $dsn, ?string $message = null) + { + $factory = $this->createFactory(); + + $dsn = new Dsn($dsn); + + $this->expectException(IncompleteDsnException::class); + if (null !== $message) { + $this->expectExceptionMessage($message); + } + + $factory->create($dsn); + } +} diff --git a/src/Map/src/Test/ProviderTestCase.php b/src/Map/src/Test/ProviderTestCase.php new file mode 100644 index 00000000000..992a2607cc5 --- /dev/null +++ b/src/Map/src/Test/ProviderTestCase.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Test; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Provider\ProviderInterface; + +/** + * A test case to ease testing a provider. + */ +abstract class ProviderTestCase extends TestCase +{ + abstract public function testGetName(): void; + + /** + * @return iterable + */ + abstract public function provideTestOptions(); + + /** + * @dataProvider provideTestOptions + */ + public function testOptions(ProviderInterface $provider, string $expectedToString, array $expectedGetOptions): void + { + self::assertSame($expectedToString, (string) $provider); + self::assertSame($expectedGetOptions, $provider->getOptions()); + } +} diff --git a/src/Map/src/Twig/MapExtension.php b/src/Map/src/Twig/MapExtension.php index 0cb30eaad66..fe992078826 100644 --- a/src/Map/src/Twig/MapExtension.php +++ b/src/Map/src/Twig/MapExtension.php @@ -1,7 +1,5 @@ + */ final class MapExtension extends AbstractExtension { public function getFunctions(): iterable diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php index 4f2d23b72ea..cff9c553ccd 100644 --- a/src/Map/src/Twig/MapRuntime.php +++ b/src/Map/src/Twig/MapRuntime.php @@ -1,7 +1,5 @@ + */ final class MapRuntime implements RuntimeExtensionInterface { public function __construct( private readonly StimulusHelper $stimulus, - private readonly MapRegistryInterface $mapRegistry, - private readonly Configuration $configuration, + private readonly MapRegistry $mapRegistry, ) { } @@ -34,22 +33,18 @@ public function renderScriptTags(): string return ''; } - $this->configuration->validateSimultaneousMapsUsage(array_map(fn (MapInterface $map) => $map->getName(), $maps)); - $scriptTags = []; $jsConfig = ['providers' => []]; foreach ($maps as $map) { - $mapConfig = $this->configuration->getMap($map->getName()); - - $jsConfig['providers'][$mapConfig->provider->provider] = (object) $mapConfig->provider->options; + $jsConfig['providers'][$map->getProvider()->getName()] = (object) $map->getProvider()->getOptions(); } - $scriptTags[] = sprintf('', json_encode($jsConfig, flags: \JSON_THROW_ON_ERROR)); + $scriptTags[] = \sprintf('', json_encode($jsConfig, flags: \JSON_THROW_ON_ERROR)); return implode("\n", $scriptTags); } - public function renderMap(MapInterface $map, array $attributes = []): string + public function renderMap(Map $map, array $attributes = []): string { $map->setAttributes($attributes + $map->getAttributes()); @@ -57,7 +52,7 @@ public function renderMap(MapInterface $map, array $attributes = []): string if ($map->getDataController()) { $controllers[$map->getDataController()] = []; } - $controllers[$map::getMainDataController()] = ['view' => $map->createView()]; + $controllers[$map->getMainDataController()] = ['view' => $map->toArray()]; $stimulusAttributes = $this->stimulus->createStimulusAttributes(); foreach ($controllers as $name => $controllerValues) { @@ -76,6 +71,6 @@ public function renderMap(MapInterface $map, array $attributes = []): string } } - return sprintf('
', $stimulusAttributes); + return \sprintf('
', $stimulusAttributes); } } diff --git a/src/Map/src/UXMapBundle.php b/src/Map/src/UXMapBundle.php index 263a89165ff..27014e62953 100644 --- a/src/Map/src/UXMapBundle.php +++ b/src/Map/src/UXMapBundle.php @@ -15,12 +15,8 @@ /** * @author Hugo Alliaume - * - * @final - * - * @experimental */ -class UXMapBundle extends Bundle +final class UXMapBundle extends Bundle { public function getPath(): string { diff --git a/src/Map/tests/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompilerTest.php b/src/Map/tests/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompilerTest.php new file mode 100644 index 00000000000..00ba5c85d62 --- /dev/null +++ b/src/Map/tests/AssetMapper/ImportMap/Compiler/LeafletReplaceImagesAssetCompilerTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\AssetMapper\ImportMap\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\UX\Map\AssetMapper\ImportMap\Compiler\LeafletReplaceImagesAssetCompiler; + +class LeafletReplaceImagesAssetCompilerTest extends TestCase +{ + /** + * @dataProvider provideCompileTests + */ + public function testCompile(string $input, string $expectedInputContains, string $expectedOutputContains, array $expectedDependencies) + { + $assetMapper = $this->createMock(AssetMapperInterface::class); + $assetMapper->expects($this->any()) + ->method('getAssetFromSourcePath') + ->willReturnCallback(function ($path) { + return match ($path) { + '/project/assets/vendor/leaflet/dist/images/layers.png' => new MappedAsset( + logicalPath: 'vendor/leaflet/dist/images/layers.png', + publicPathWithoutDigest: '/assets/vendor/leaflet/dist/images/layers.png', + publicPath: '/assets/vendor/leaflet/dist/images/layers-abcd123.png', + ), + '/project/assets/vendor/leaflet/dist/images/layers-2x.png' => new MappedAsset( + logicalPath: 'vendor/leaflet/dist/images/layers-2x.png', + publicPathWithoutDigest: '/assets/vendor/leaflet/dist/images/layers-2x.png', + publicPath: '/assets/vendor/leaflet/dist/images/layers-2x-abcd123.png', + ), + '/project/assets/vendor/leaflet/dist/images/marker-icon.png' => new MappedAsset( + logicalPath: 'vendor/leaflet/dist/images/marker-icon.png', + publicPathWithoutDigest: '/assets/vendor/leaflet/dist/images/marker-icon.png', + publicPath: '/assets/vendor/leaflet/dist/images/marker-icon-abcd123.png', + ), + '/project/assets/vendor/leaflet/dist/images/marker-icon-2x.png' => new MappedAsset( + logicalPath: 'vendor/leaflet/dist/images/marker-icon-2x.png', + publicPathWithoutDigest: '/assets/vendor/leaflet/dist/images/marker-icon-2x.png', + publicPath: '/assets/vendor/leaflet/dist/images/marker-icon-2x-abcd123.png', + ), + '/project/assets/vendor/leaflet/dist/images/marker-shadow.png' => new MappedAsset( + logicalPath: 'vendor/leaflet/dist/images/marker-shadow.png', + publicPathWithoutDigest: '/assets/vendor/leaflet/dist/images/marker-shadow.png', + publicPath: '/assets/vendor/leaflet/dist/images/marker-shadow-abcd123.png', + ), + default => null, + }; + }); + + self::assertStringContainsString($expectedInputContains, $input, 'The input should contain the expected string'); + + $compiler = new LeafletReplaceImagesAssetCompiler(); + $asset = new MappedAsset('vendor/leaflet/leaflet.index.js', '/project/assets/vendor/leaflet/leaflet.index.js', '/assets/vendor/leaflet/leaflet.index.js'); + + $output = $compiler->compile($input, $asset, $assetMapper); + self::assertStringNotContainsString($expectedInputContains, $output, 'The output should not contain the expected string'); + self::assertStringContainsString($expectedOutputContains, $output, 'The output should contain the expected string'); + + $assetDependencyLogicalPaths = array_map(fn (MappedAsset $dependency) => $dependency->logicalPath, $asset->getDependencies()); + self::assertSame($expectedDependencies, $assetDependencyLogicalPaths); + } + + public static function provideCompileTests(): iterable + { + yield 'leaflet 1.9.4' => [ + 'input' => file_get_contents(__DIR__.'/../Fixtures/leaflet.1.9.4.js'), + 'expectedInputContains' => 'iconUrl:"marker-icon.png",iconRetinaUrl:"marker-icon-2x.png",shadowUrl:"marker-shadow.png"', + 'expectedOutputContains' => 'iconUrl:"/assets/vendor/leaflet/dist/images/marker-icon-abcd123.png",iconRetinaUrl:"/assets/vendor/leaflet/dist/images/marker-icon-2x-abcd123.png",shadowUrl:"/assets/vendor/leaflet/dist/images/marker-shadow-abcd123.png"', + 'expectedDependencies' => [ + 'vendor/leaflet/dist/images/marker-icon.png', + 'vendor/leaflet/dist/images/marker-icon-2x.png', + 'vendor/leaflet/dist/images/marker-shadow.png', + ], + ]; + } +} diff --git a/src/Map/tests/AssetMapper/ImportMap/Fixtures/leaflet.1.9.4.js b/src/Map/tests/AssetMapper/ImportMap/Fixtures/leaflet.1.9.4.js new file mode 100644 index 00000000000..f386e8c3ff6 --- /dev/null +++ b/src/Map/tests/AssetMapper/ImportMap/Fixtures/leaflet.1.9.4.js @@ -0,0 +1,9 @@ +/** + * https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js + */ +/* @preserve + * Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1 + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\AssetMapper\ImportMap\Resolver; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; +use Symfony\Component\AssetMapper\ImportMap\ImportMapType; +use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; +use Symfony\UX\Map\AssetMapper\ImportMap\Resolver\LeafletPackageResolver; + +class LeafletPackageResolverTest extends TestCase +{ + public function testResolverShouldNotDoAnythingIfLeafletIsNotRequired(): void + { + $innerPackageResolver = $this->createMock(PackageResolverInterface::class); + $innerPackageResolver->expects(self::once()) + ->method('downloadPackages') + ->with(self::anything()) + ->willReturn([ + 'bootstrap' => [ + 'content' => 'bootstrap content...', + 'dependencies' => ['@popperjs/core'], + 'extraFiles' => [], + ], + ]); + + $leafletPackageResolver = new LeafletPackageResolver( + $innerPackageResolver, + new MockHttpClient() + ); + + $downloadedPackages = $leafletPackageResolver->downloadPackages([ + 'bootstrap' => ImportMapEntry::createRemote( + importName: 'bootstrap', + importMapType: ImportMapType::JS, + path: '/project/assets/vendor/bootstrap/bootstrap.index.js', + version: '5.3.3', + packageModuleSpecifier: 'bootstrap', + isEntrypoint: false + ), + ]); + + self::assertSame([ + 'bootstrap' => [ + 'content' => 'bootstrap content...', + 'dependencies' => ['@popperjs/core'], + 'extraFiles' => [], + ], + ], $downloadedPackages); + } + + /** + * @dataProvider provideResolverShouldResolveLeafletExtraDependencies + */ + public function testResolverShouldResolveLeafletExtraDependencies( + array $importMapEntries, + array $innerPackageResolverDownloadedPackages, + callable $httpClientResponseFactory, + array $expectedDownloadedPackages, + ): void { + $innerPackageResolver = $this->createMock(PackageResolverInterface::class); + $innerPackageResolver->expects(self::once()) + ->method('downloadPackages') + ->with(self::anything()) + ->willReturn($innerPackageResolverDownloadedPackages); + + $leafletPackageResolver = new LeafletPackageResolver( + $innerPackageResolver, + new MockHttpClient($httpClientResponseFactory) + ); + + $downloadedPackages = $leafletPackageResolver->downloadPackages($importMapEntries); + + self::assertSame($expectedDownloadedPackages, $downloadedPackages); + } + + /** + * @return iterable + */ + public function provideResolverShouldResolveLeafletExtraDependencies(): iterable + { + yield 'leaflet 1.9.4' => [ + 'import_map_entries' => [ + 'leaflet' => ImportMapEntry::createRemote( + importName: 'leaflet', + importMapType: ImportMapType::JS, + path: '/project/assets/vendor/leaflet/leaflet.index.js', + version: '1.9.4', + packageModuleSpecifier: 'leaflet', + isEntrypoint: false + ), + ], + 'inner_package_resolver_downloaded_packages' => [ + 'leaflet' => [ + 'content' => file_get_contents(__DIR__.'/../Fixtures/leaflet.1.9.4.js'), + 'dependencies' => [], + 'extraFiles' => [], + ], + ], + 'http_client_response_factory' => function (string $method, string $url, array $options) { + return match ($url) { + 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/images/marker-icon.png' => new MockResponse('marker-icon.png content...'), + 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/images/marker-icon-2x.png' => new MockResponse('marker-icon-2x.png content...'), + 'https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/images/marker-shadow.png' => new MockResponse('marker-shadow.png content...'), + default => throw new \RuntimeException("Unexpected URL: $url"), + }; + }, + 'expected_downloaded_packages' => [ + 'leaflet' => [ + 'content' => file_get_contents(__DIR__.'/../Fixtures/leaflet.1.9.4.js'), + 'dependencies' => [], + 'extraFiles' => [ + 'dist/images/marker-icon.png' => 'marker-icon.png content...', + 'dist/images/marker-icon-2x.png' => 'marker-icon-2x.png content...', + 'dist/images/marker-shadow.png' => 'marker-shadow.png content...', + ], + ], + ], + ]; + } +} diff --git a/src/Map/tests/ConfigurationTest.php b/src/Map/tests/ConfigurationTest.php deleted file mode 100644 index ffb81c09e83..00000000000 --- a/src/Map/tests/ConfigurationTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * 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\Configuration\Configuration; -use Symfony\UX\Map\Exception\MapNotFoundException; - -final class ConfigurationTest extends TestCase -{ - private Configuration $configuration; - - protected function setUp(): void - { - $this->configuration = new Configuration( - [ - 'google_maps_provider' => [ - 'provider' => 'google_maps', - 'options' => [ - 'key' => 'my_key', - ], - ], - ], - [ - 'google_maps_map_1' => [ - 'provider' => 'google_maps_provider', - 'options' => [ - 'map_id' => 'DEMO_MAP_ID', - ], - ], - ] - ); - } - - public function testGetMap(): void - { - $config = $this->configuration->getMap('google_maps_map_1'); - - self::assertSame('google_maps_map_1', $config->name); - self::assertEquals(['map_id' => 'DEMO_MAP_ID'], $config->options); - - self::assertSame('google_maps', $config->provider->provider); - self::assertSame('google_maps_provider', $config->provider->name); - self::assertEquals(['key' => 'my_key'], $config->provider->options); - } - - public function testGetMapConfigWithUnknownMap(): void - { - $this->expectException(MapNotFoundException::class); - $this->expectExceptionMessage('Map "unknown_map" is not found, has it been correctly configured?'); - - $this->configuration->getMap('unknown_map'); - } -} diff --git a/src/Map/tests/Factory/MapFactoryTest.php b/src/Map/tests/Factory/MapFactoryTest.php deleted file mode 100644 index 75ac65e74e5..00000000000 --- a/src/Map/tests/Factory/MapFactoryTest.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Factory; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\DependencyInjection\ServiceLocator; -use Symfony\UX\Map\Configuration\Configuration; -use Symfony\UX\Map\Exception\MapNotFoundException; -use Symfony\UX\Map\Factory\MapFactory; -use Symfony\UX\Map\Factory\MapFactoryInterface; -use Symfony\UX\Map\MapInterface; -use Symfony\UX\Map\MapTrait; -use Symfony\UX\Map\Registry\MapRegistry; - -final class MapFactoryTest extends TestCase -{ - private MapFactoryInterface $fakeFactory; - private MapFactory $mapFactory; - private MapRegistry $mapRegistry; - - protected function setUp(): void - { - $this->fakeFactory = new class() implements MapFactoryInterface { - public string $passedName = ''; - public array $passedOptions = []; - - public function createMap(string $name, array $options = []): MapInterface - { - $this->passedName = $name; - $this->passedOptions = $options; - - return new class() implements MapInterface { - use MapTrait; - - public static function getMainDataController(): string - { - throw new \BadMethodCallException('Not implemented'); - } - - public function createView(): array - { - throw new \BadMethodCallException('Not implemented'); - } - }; - } - }; - - $this->mapFactory = new MapFactory( - new ServiceLocator([ - 'leaflet' => fn () => $this->fakeFactory, - 'google_maps' => fn () => $this->fakeFactory, - ]), - new Configuration([ - 'leaflet' => ['provider' => 'leaflet'], - 'google_maps' => ['provider' => 'google_maps'], - ], [ - 'map_1' => [ - 'provider' => 'leaflet', - ], - 'map_2' => [ - 'provider' => 'google_maps', - ], - 'map_3' => [ - 'provider' => 'google_maps', - 'options' => [ - 'mapId' => 'abcdefgh1234567890', - ], - ], - ]), - $this->mapRegistry = new MapRegistry(), - ); - - self::assertEmpty($this->mapRegistry->all()); - } - - public function testCreateMap(): void - { - $map = $this->mapFactory->createMap('map_1'); - - self::assertSame('map_1', $this->fakeFactory->passedName); - self::assertSame([], $this->fakeFactory->passedOptions); - self::assertContains($map, $this->mapRegistry->all()); - } - - public function testCreateMapWithCustomOptions(): void - { - $map = $this->mapFactory->createMap('map_3', ['zoom' => 10]); - - self::assertSame('map_3', $this->fakeFactory->passedName); - self::assertSame(['zoom' => 10, 'mapId' => 'abcdefgh1234567890'], $this->fakeFactory->passedOptions); - self::assertContains($map, $this->mapRegistry->all()); - } - - public function testCreateMapWithInvalidMap(): void - { - $this->expectException(MapNotFoundException::class); - $this->expectExceptionMessage('Map "foo_bar" is not found, has it been correctly configured?'); - - $this->mapFactory->createMap('foo_bar'); - } -} diff --git a/src/Map/tests/InfoWindowTest.php b/src/Map/tests/InfoWindowTest.php new file mode 100644 index 00000000000..9ba9a41076c --- /dev/null +++ b/src/Map/tests/InfoWindowTest.php @@ -0,0 +1,41 @@ + + * + * 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\InfoWindow; +use Symfony\UX\Map\LatLng; + +class InfoWindowTest extends TestCase +{ + public function testToArray(): void + { + $infoWindow = new InfoWindow( + headerContent: 'Paris', + content: 'Capitale de la France, est une grande ville européenne et un centre mondial de l\'art, de la mode, de la gastronomie et de la culture.', + position: new LatLng(48.8566, 2.3522), + opened: true, + autoClose: false, + ); + + $this->assertSame([ + 'headerContent' => 'Paris', + 'content' => 'Capitale de la France, est une grande ville européenne et un centre mondial de l\'art, de la mode, de la gastronomie et de la culture.', + 'position' => [ + 'lat' => 48.8566, + 'lng' => 2.3522, + ], + 'opened' => true, + 'autoClose' => false, + ], $infoWindow->toArray()); + } +} diff --git a/src/Map/tests/Kernel/FrameworkAppKernel.php b/src/Map/tests/Kernel/FrameworkAppKernel.php index 241b0aa5ddc..cf0f33cc5f8 100644 --- a/src/Map/tests/Kernel/FrameworkAppKernel.php +++ b/src/Map/tests/Kernel/FrameworkAppKernel.php @@ -36,6 +36,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) { $loader->load(function (ContainerBuilder $container) { $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]); + $container->loadFromExtension('ux_map', ['provider' => 'leaflet://default']); }); } } diff --git a/src/Map/tests/Kernel/TwigAppKernel.php b/src/Map/tests/Kernel/TwigAppKernel.php index efcebb6c750..e1ca404aeff 100644 --- a/src/Map/tests/Kernel/TwigAppKernel.php +++ b/src/Map/tests/Kernel/TwigAppKernel.php @@ -38,6 +38,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) $loader->load(function (ContainerBuilder $container) { $container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]); $container->loadFromExtension('twig', ['default_path' => __DIR__.'/templates', 'strict_variables' => true, 'exception_controller' => null]); + $container->loadFromExtension('ux_map', ['provider' => 'leaflet://default']); $container->setAlias('test.ux_map.map_factory', 'ux_map.map_factory')->setPublic(true); }); diff --git a/src/Map/tests/LatLngTest.php b/src/Map/tests/LatLngTest.php index 6c4e94f89df..819ad17d8b7 100644 --- a/src/Map/tests/LatLngTest.php +++ b/src/Map/tests/LatLngTest.php @@ -1,7 +1,5 @@ latitude); - self::assertSame(2.3522, $latLng->longitude); - } - - public function testCreateView(): void + public function testToArray(): void { $latLng = new LatLng(48.8566, 2.3533); - self::assertSame(['lat' => 48.8566, 'lng' => 2.3533], $latLng->createView()); + self::assertSame(['lat' => 48.8566, 'lng' => 2.3533], $latLng->toArray()); } } diff --git a/src/Map/tests/MapFactoryTest.php b/src/Map/tests/MapFactoryTest.php new file mode 100644 index 00000000000..706aded5137 --- /dev/null +++ b/src/Map/tests/MapFactoryTest.php @@ -0,0 +1,43 @@ + + * + * 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\MapFactory; +use Symfony\UX\Map\MapRegistry; +use Symfony\UX\Map\Provider\ProviderInterface; +use Symfony\UX\Map\Tests\Provider\DummyProvider; + +final class MapFactoryTest extends TestCase +{ + private MapFactory $mapFactory; + private ProviderInterface $defaultProvider; + private MapRegistry $mapRegistry; + + protected function setUp(): void + { + $this->mapFactory = new MapFactory( + $this->defaultProvider = new DummyProvider(), + $this->mapRegistry = new MapRegistry(), + ); + + self::assertEmpty($this->mapRegistry->all()); + } + + public function testCreateMap(): void + { + $map = $this->mapFactory->createMap(); + + self::assertSame($this->defaultProvider, $map->getProvider()); + self::assertContains($map, $this->mapRegistry->all()); + } +} diff --git a/src/Map/tests/Registry/MapRegistryTest.php b/src/Map/tests/MapRegistryTest.php similarity index 57% rename from src/Map/tests/Registry/MapRegistryTest.php rename to src/Map/tests/MapRegistryTest.php index 589515fdcdf..03df79c6a96 100644 --- a/src/Map/tests/Registry/MapRegistryTest.php +++ b/src/Map/tests/MapRegistryTest.php @@ -1,7 +1,5 @@ all()); - $mapRegistry->register($map1 = new GoogleMaps\Map('my_map')); - self::assertContains($map1, $mapRegistry->all()); + $mapRegistry->register($map1 = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions())); + self::assertSame([$map1], $mapRegistry->all()); - $mapRegistry->register($map2 = new GoogleMaps\Map('my_map')); - self::assertContains($map2, $mapRegistry->all()); + $mapRegistry->register($map2 = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions())); + self::assertSame([$map1, $map2], $mapRegistry->all()); $mapRegistry->reset(); self::assertEmpty($mapRegistry->all()); diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php new file mode 100644 index 00000000000..3b1daec0fd0 --- /dev/null +++ b/src/Map/tests/MapTest.php @@ -0,0 +1,138 @@ + + * + * 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\LatLng; +use Symfony\UX\Map\Map; +use Symfony\UX\Map\Marker; +use Symfony\UX\Map\Tests\Provider\DummyProvider; + +class MapTest extends TestCase +{ + public function testCenterValidation(): void + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('The center of the map must be set.'); + + $map = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions()); + $map->toArray(); + } + + public function testZoomValidation(): void + { + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('The zoom of the map must be set.'); + + $map = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions()); + $map->center(new LatLng(48.8566, 2.3522)); + $map->toArray(); + } + + public function testWithMinimumConfiguration(): void + { + $map = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions()); + $map + ->center(new LatLng(48.8566, 2.3522)) + ->zoom(6); + + self::assertSame([ + 'center' => ['lat' => 48.8566, 'lng' => 2.3522], + 'zoom' => 6.0, + 'fitBoundsToMarkers' => false, + 'options' => [], + 'markers' => [], + 'infoWindows' => [], + ], $map->toArray()); + } + + public function testWithMaximumConfiguration(): void + { + $map = new Map(new DummyProvider(), DummyProvider::getDefaultMapOptions()); + $map + ->center(new LatLng(48.8566, 2.3522)) + ->zoom(6) + ->fitBoundsToMarkers() + ->options(DummyProvider::getDefaultMapOptions()) + ->addMarker(new Marker( + position: new LatLng(48.8566, 2.3522), + title: 'Paris', + infoWindow: new InfoWindow(headerContent: 'Paris', content: 'Paris !', position: new LatLng(48.8566, 2.3522), opened: true) + )) + ->addMarker(new Marker( + position: new LatLng(45.764, 4.8357), + title: 'Lyon', + infoWindow: new InfoWindow(headerContent: 'Lyon', content: 'Lyon !', position: new LatLng(45.764, 4.8357), opened: true) + )) + ->addMarker(new Marker( + position: new LatLng(43.2965, 5.3698), + title: 'Marseille', + infoWindow: new InfoWindow(headerContent: 'Marseille', content: 'Marseille !', position: new LatLng(43.2965, 5.3698), opened: true) + )) + ->addMarker(new Marker( + position: new LatLng(43.7102, 7.262), + title: 'Nice', + infoWindow: new InfoWindow(headerContent: 'Nice', content: 'Nice !', position: new LatLng(43.7102, 7.262), opened: true) + )) + ->addMarker(new Marker( + position: new LatLng(47.2184, -1.5536), + title: 'Nantes', + infoWindow: new InfoWindow(headerContent: 'Nantes', content: 'Nantes !', position: new LatLng(47.2184, -1.5536), opened: true) + )) + ->addInfoWindow(new InfoWindow(headerContent: 'Strasbourg', content: 'Strasbourg !', position: new LatLng(48.5734, 7.7521), opened: true)); + + self::assertSame([ + 'center' => ['lat' => 48.8566, 'lng' => 2.3522], + 'zoom' => 6.0, + 'fitBoundsToMarkers' => true, + 'options' => [], + 'markers' => [ + [ + 'position' => ['lat' => 48.8566, 'lng' => 2.3522], + 'title' => 'Paris', + 'infoWindow' => ['headerContent' => 'Paris', 'content' => 'Paris !', 'position' => ['lat' => 48.8566, 'lng' => 2.3522], 'opened' => true, 'autoClose' => true], + ], + [ + 'position' => ['lat' => 45.764, 'lng' => 4.8357], + 'title' => 'Lyon', + 'infoWindow' => ['headerContent' => 'Lyon', 'content' => 'Lyon !', 'position' => ['lat' => 45.764, 'lng' => 4.8357], 'opened' => true, 'autoClose' => true], + ], + [ + 'position' => ['lat' => 43.2965, 'lng' => 5.3698], + 'title' => 'Marseille', + 'infoWindow' => ['headerContent' => 'Marseille', 'content' => 'Marseille !', 'position' => ['lat' => 43.2965, 'lng' => 5.3698], 'opened' => true, 'autoClose' => true], + ], + [ + 'position' => ['lat' => 43.7102, 'lng' => 7.262], + 'title' => 'Nice', + 'infoWindow' => ['headerContent' => 'Nice', 'content' => 'Nice !', 'position' => ['lat' => 43.7102, 'lng' => 7.262], 'opened' => true, 'autoClose' => true], + ], + [ + 'position' => ['lat' => 47.2184, 'lng' => -1.5536], + 'title' => 'Nantes', + 'infoWindow' => ['headerContent' => 'Nantes', 'content' => 'Nantes !', 'position' => ['lat' => 47.2184, 'lng' => -1.5536], 'opened' => true, 'autoClose' => true], + ], + ], + 'infoWindows' => [ + [ + 'headerContent' => 'Strasbourg', + 'content' => 'Strasbourg !', + 'position' => ['lat' => 48.5734, 'lng' => 7.7521], + 'opened' => true, + 'autoClose' => true, + ], + ], + ], $map->toArray()); + } +} diff --git a/src/Map/tests/MarkerTest.php b/src/Map/tests/MarkerTest.php new file mode 100644 index 00000000000..45e13389435 --- /dev/null +++ b/src/Map/tests/MarkerTest.php @@ -0,0 +1,55 @@ + + * + * 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\InfoWindow; +use Symfony\UX\Map\LatLng; +use Symfony\UX\Map\Marker; + +class MarkerTest extends TestCase +{ + public function testToArray(): void + { + $marker = new Marker( + position: new LatLng(48.8566, 2.3522), + ); + + $this->assertSame([ + 'position' => ['lat' => 48.8566, 'lng' => 2.3522], + 'title' => null, + 'infoWindow' => null, + ], $marker->toArray()); + + $marker = new Marker( + position: new LatLng(48.8566, 2.3522), + title: 'Paris', + infoWindow: new InfoWindow( + headerContent: 'Paris', + content: "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", + opened: true, + ), + ); + + $this->assertSame([ + 'position' => ['lat' => 48.8566, 'lng' => 2.3522], + 'title' => 'Paris', + 'infoWindow' => [ + 'headerContent' => 'Paris', + 'content' => "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", + 'position' => null, + 'opened' => true, + 'autoClose' => true, + ], + ], $marker->toArray()); + } +} diff --git a/src/Map/tests/Provider/DsnTest.php b/src/Map/tests/Provider/DsnTest.php new file mode 100644 index 00000000000..3e73f54c2ce --- /dev/null +++ b/src/Map/tests/Provider/DsnTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Exception\InvalidArgumentException; +use Symfony\UX\Map\Provider\Dsn; + +final class DsnTest extends TestCase +{ + /** + * @dataProvider constructProvider + */ + public function testConstruct(string $dsnString, string $scheme, string $host, ?string $user = null, array $options = [], ?string $path = null) + { + $dsn = new Dsn($dsnString); + $this->assertSame($dsnString, $dsn->getOriginalDsn()); + + $this->assertSame($scheme, $dsn->getScheme()); + $this->assertSame($host, $dsn->getHost()); + $this->assertSame($user, $dsn->getUser()); + $this->assertSame($options, $dsn->getOptions()); + } + + public static function constructProvider(): iterable + { + yield 'simple dsn' => [ + 'scheme://default', + 'scheme', + 'default', + ]; + + yield 'simple dsn including @ sign, but no user/password/token' => [ + 'scheme://@default', + 'scheme', + 'default', + ]; + + yield 'simple dsn including : sign and @ sign, but no user/password/token' => [ + 'scheme://:@default', + 'scheme', + 'default', + ]; + + yield 'simple dsn including user, : sign and @ sign, but no password' => [ + 'scheme://user1:@default', + 'scheme', + 'default', + 'user1', + ]; + + yield 'dsn with user' => [ + 'scheme://u$er@default', + 'scheme', + 'default', + 'u$er', + ]; + + yield 'dsn with user, and custom option' => [ + 'scheme://u$er@default?api_key=MY_API_KEY', + 'scheme', + 'default', + 'u$er', + [ + 'api_key' => 'MY_API_KEY', + ], + '/channel', + ]; + } + + /** + * @dataProvider invalidDsnProvider + */ + public function testInvalidDsn(string $dsnString, string $exceptionMessage) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($exceptionMessage); + + new Dsn($dsnString); + } + + public static function invalidDsnProvider(): iterable + { + yield [ + 'leaflet://', + 'The map provider DSN is invalid.', + ]; + + yield [ + '//default', + 'The map provider DSN must contain a scheme.', + ]; + } +} diff --git a/src/Map/tests/Provider/DummyProvider.php b/src/Map/tests/Provider/DummyProvider.php new file mode 100644 index 00000000000..c4c12bd9699 --- /dev/null +++ b/src/Map/tests/Provider/DummyProvider.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider; + +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Provider\ProviderInterface; +use Symfony\UX\Map\Provider\ProviderTrait; + +final readonly class DummyProvider implements ProviderInterface +{ + use ProviderTrait; + + public function getName(): string + { + return 'dummy'; + } + + public function __toString() + { + return 'dummy://default'; + } + + public static function getDefaultMapOptions(): MapOptionsInterface + { + return new class() implements MapOptionsInterface { + public function toArray(): array + { + return []; + } + }; + } + + public function getOptions(): array + { + return []; + } +} diff --git a/src/Map/tests/Provider/GoogleMaps/GoogleMapsOptionsTest.php b/src/Map/tests/Provider/GoogleMaps/GoogleMapsOptionsTest.php new file mode 100644 index 00000000000..a52f9b96bd5 --- /dev/null +++ b/src/Map/tests/Provider/GoogleMaps/GoogleMapsOptionsTest.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Provider\GoogleMaps\GoogleMapsOptions; +use Symfony\UX\Map\Provider\GoogleMaps\Option as GoogleMapsOption; + +class GoogleMapsOptionsTest extends TestCase +{ + public function testWithMinimalConfiguration() + { + $options = new GoogleMapsOptions(); + + self::assertSame([ + 'mapId' => null, + 'gestureHandling' => 'auto', + 'backgroundColor' => null, + 'disableDoubleClickZoom' => false, + 'zoomControl' => true, + 'zoomControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::INLINE_END_BLOCK_END->value, + ], + 'mapTypeControl' => true, + 'mapTypeControlOptions' => [ + 'mapTypeIds' => [], + 'position' => GoogleMapsOption\ControlPosition::BLOCK_START_INLINE_START->value, + 'style' => GoogleMapsOption\MapTypeControlStyle::DEFAULT->value, + ], + 'streetViewControl' => true, + 'streetViewControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::INLINE_END_BLOCK_END->value, + ], + 'fullscreenControl' => true, + 'fullscreenControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::INLINE_END_BLOCK_START->value, + ], + ], $options->toArray()); + } + + public function testWithMaximumConfiguration() + { + $options = new GoogleMapsOptions( + mapId: '2b2d73ba4b8c7b41', + gestureHandling: GoogleMapsOption\GestureHandling::Greedy, + backgroundColor: '#f00', + disableDoubleClickZoom: true, + zoomControl: false, + zoomControlOptions: new GoogleMapsOption\ZoomControlOptions( + position: GoogleMapsOption\ControlPosition::BLOCK_START_INLINE_END, + ), + mapTypeControl: false, + mapTypeControlOptions: new GoogleMapsOption\MapTypeControlOptions( + mapTypeIds: [GoogleMapsOption\MapTypeId::HYBRID, GoogleMapsOption\MapTypeId::ROADMAP], + position: GoogleMapsOption\ControlPosition::INLINE_END_BLOCK_START, + style: GoogleMapsOption\MapTypeControlStyle::DROPDOWN_MENU, + ), + streetViewControl: false, + streetViewControlOptions: new GoogleMapsOption\StreetViewControlOptions( + position: GoogleMapsOption\ControlPosition::BLOCK_END_INLINE_START, + ), + fullscreenControl: false, + fullscreenControlOptions: new GoogleMapsOption\FullscreenControlOptions( + position: GoogleMapsOption\ControlPosition::INLINE_START_BLOCK_END, + ), + ); + + self::assertSame([ + 'mapId' => '2b2d73ba4b8c7b41', + 'gestureHandling' => GoogleMapsOption\GestureHandling::Greedy->value, + 'backgroundColor' => '#f00', + 'disableDoubleClickZoom' => true, + 'zoomControl' => false, + 'zoomControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::BLOCK_START_INLINE_END->value, + ], + 'mapTypeControl' => false, + 'mapTypeControlOptions' => [ + 'mapTypeIds' => [GoogleMapsOption\MapTypeId::HYBRID->value, GoogleMapsOption\MapTypeId::ROADMAP->value], + 'position' => GoogleMapsOption\ControlPosition::INLINE_END_BLOCK_START->value, + 'style' => GoogleMapsOption\MapTypeControlStyle::DROPDOWN_MENU->value, + ], + 'streetViewControl' => false, + 'streetViewControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::BLOCK_END_INLINE_START->value, + ], + 'fullscreenControl' => false, + 'fullscreenControlOptions' => [ + 'position' => GoogleMapsOption\ControlPosition::INLINE_START_BLOCK_END->value, + ], + ], $options->toArray()); + } +} diff --git a/src/Map/tests/Provider/GoogleMaps/GoogleMapsProviderTest.php b/src/Map/tests/Provider/GoogleMaps/GoogleMapsProviderTest.php new file mode 100644 index 00000000000..e46d1b26c45 --- /dev/null +++ b/src/Map/tests/Provider/GoogleMaps/GoogleMapsProviderTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; + +use Symfony\UX\Map\Provider\GoogleMaps\GoogleMapsProvider; +use Symfony\UX\Map\Test\ProviderTestCase; + +class GoogleMapsProviderTest extends ProviderTestCase +{ + public function testGetName(): void + { + $provider = new GoogleMapsProvider('api_key'); + + self::assertSame('google-maps', $provider->getName()); + } + + public function provideTestOptions(): iterable + { + yield [ + 'provider' => new GoogleMapsProvider('api_key'), + 'expectedToString' => 'google-maps://*******@default/?', + 'expectedOptions' => ['apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap', + 'expectedOptions' => ['id' => 'gmap', 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr', region: 'FR'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr®ion=FR', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'region' => 'FR', 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr®ion=FR&nonce=abcd', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'region' => 'FR', 'nonce' => 'abcd', 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr®ion=FR&nonce=abcd&retries=10', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'region' => 'FR', 'nonce' => 'abcd', 'retries' => 10, 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr®ion=FR&nonce=abcd&retries=10&url=https%3A%2F%2Fmaps.googleapis.com%2Fmaps%2Fapi%2Fjs', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'region' => 'FR', 'nonce' => 'abcd', 'retries' => 10, 'url' => 'https://maps.googleapis.com/maps/api/js', 'apiKey' => 'api_key'], + ]; + + yield [ + 'provider' => new GoogleMapsProvider('api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'weekly'), + 'expectedToString' => 'google-maps://*******@default/?id=gmap&language=fr®ion=FR&nonce=abcd&retries=10&url=https%3A%2F%2Fmaps.googleapis.com%2Fmaps%2Fapi%2Fjs&version=weekly', + 'expectedOptions' => ['id' => 'gmap', 'language' => 'fr', 'region' => 'FR', 'nonce' => 'abcd', 'retries' => 10, 'url' => 'https://maps.googleapis.com/maps/api/js', 'version' => 'weekly', 'apiKey' => 'api_key'], + ]; + } +} diff --git a/src/Map/tests/Provider/GoogleMaps/GoogleProviderFactoryTest.php b/src/Map/tests/Provider/GoogleMaps/GoogleProviderFactoryTest.php new file mode 100644 index 00000000000..ccc59a90b17 --- /dev/null +++ b/src/Map/tests/Provider/GoogleMaps/GoogleProviderFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; + +use Symfony\UX\Map\Provider\GoogleMaps\GoogleMapsProviderFactory; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Test\ProviderFactoryTestCase; + +final class GoogleProviderFactoryTest extends ProviderFactoryTestCase +{ + public function createFactory(): ProviderFactoryInterface + { + return new GoogleMapsProviderFactory(); + } + + public static function supportsProvider(): iterable + { + yield [true, 'google-maps://GOOGLE_MAPS_API_KEY@default']; + yield [false, 'somethingElse://login:apiKey@default']; + } + + public static function createProvider(): iterable + { + yield [ + 'google-maps://*******************@default/?version=weekly', + 'google-maps://GOOGLE_MAPS_API_KEY@default', + ]; + + yield [ + 'google-maps://*******************@default/?version=quartly', + 'google-maps://GOOGLE_MAPS_API_KEY@default?version=quartly', + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://foo@default']; + } +} diff --git a/src/Map/tests/Provider/GoogleMaps/InfoWindowTest.php b/src/Map/tests/Provider/GoogleMaps/InfoWindowTest.php deleted file mode 100644 index ad050a8dcd6..00000000000 --- a/src/Map/tests/Provider/GoogleMaps/InfoWindowTest.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\Exception\InvalidArgumentException; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\GoogleMaps\InfoWindow; -use Symfony\UX\Map\Provider\GoogleMaps\Marker; - -class InfoWindowTest extends TestCase -{ - public function testConstructShouldThrowIfNoPositionOrMarkerIsPassed(): void - { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('An "Symfony\UX\Map\Provider\GoogleMaps\InfoWindow" must be associated with a position or a marker.'); - - new InfoWindow(); - } - - public function testMarkerPositionShouldTakePrecedenceOverPosition(): void - { - $infoWindow = new InfoWindow( - marker: new Marker(position: new LatLng(1, 2)), - position: new LatLng(3, 4) - ); - - $position = \Closure::bind(fn () => $infoWindow->position, $infoWindow, InfoWindow::class)(); - - self::assertEquals(new LatLng(1, 2), $position); - } - - public function testPositionFallbackToPositionIfNoMarkerIsPassed(): void - { - $infoWindow = new InfoWindow( - position: new LatLng(3, 4) - ); - - $position = \Closure::bind(fn () => $infoWindow->position, $infoWindow, InfoWindow::class)(); - - self::assertEquals(new LatLng(3, 4), $position); - } - - public function testCreateView(): void - { - $infoWindow = new InfoWindow( - position: new LatLng(3, 4) - ); - - self::assertEquals([ - 'headerContent' => null, - 'content' => null, - 'position' => ['lat' => 3, 'lng' => 4], - 'opened' => false, - 'autoClose' => true, - '_markerId' => null, - ], $infoWindow->createView()); - } -} diff --git a/src/Map/tests/Provider/GoogleMaps/MapFactoryTest.php b/src/Map/tests/Provider/GoogleMaps/MapFactoryTest.php deleted file mode 100644 index 75d61b565b3..00000000000 --- a/src/Map/tests/Provider/GoogleMaps/MapFactoryTest.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\Provider\GoogleMaps\GestureHandling; -use Symfony\UX\Map\Provider\GoogleMaps\Map; -use Symfony\UX\Map\Provider\GoogleMaps\MapFactory; - -class MapFactoryTest extends TestCase -{ - public function testCreate(): void - { - $mapFactory = new MapFactory(); - - $map = $mapFactory->createMap('map_name'); - - self::assertInstanceOf(Map::class, $map); - self::assertSame('map_name', $map->getName()); - } - - public function testCreateWithCustomOptions(): void - { - $mapFactory = new MapFactory(); - - $map = $mapFactory->createMap('map_name', [ - 'mapId' => 'DEMO_MAP_ID', - 'center' => [37.7749, -122.4194], - 'zoom' => 3, - 'gestureHandling' => 'auto', - 'backgroundColor' => '#f8f9fa', - 'enableDoubleClickZoom' => true, - 'zoomControl' => true, - 'mapTypeControl' => true, - 'streetViewControl' => true, - 'fullscreenControl' => true, - 'fitBoundsToMarkers' => true, - ]); - - self::assertInstanceOf(Map::class, $map); - self::assertSame('map_name', $map->getName()); - - $view = $map->createView(); - - self::assertSame('DEMO_MAP_ID', $view['mapId']); - self::assertSame(['lat' => 37.7749, 'lng' => -122.4194], $view['center']); - self::assertSame(3.0, $view['zoom']); - self::assertSame(GestureHandling::Auto->value, $view['gestureHandling']); - self::assertSame('#f8f9fa', $view['backgroundColor']); - self::assertFalse($view['disableDoubleClickZoom']); - self::assertTrue($view['zoomControl']); - self::assertTrue($view['mapTypeControl']); - self::assertTrue($view['streetViewControl']); - self::assertTrue($view['fullscreenControl']); - self::assertTrue($view['fitBoundsToMarkers']); - } -} diff --git a/src/Map/tests/Provider/GoogleMaps/MapTest.php b/src/Map/tests/Provider/GoogleMaps/MapTest.php deleted file mode 100644 index c974c5a0a49..00000000000 --- a/src/Map/tests/Provider/GoogleMaps/MapTest.php +++ /dev/null @@ -1,201 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\GoogleMaps\ControlPosition; -use Symfony\UX\Map\Provider\GoogleMaps\FullscreenControlOptions; -use Symfony\UX\Map\Provider\GoogleMaps\GestureHandling; -use Symfony\UX\Map\Provider\GoogleMaps\InfoWindow; -use Symfony\UX\Map\Provider\GoogleMaps\Map; -use Symfony\UX\Map\Provider\GoogleMaps\MapTypeControlOptions; -use Symfony\UX\Map\Provider\GoogleMaps\MapTypeControlStyle; -use Symfony\UX\Map\Provider\GoogleMaps\MapTypeId; -use Symfony\UX\Map\Provider\GoogleMaps\Marker; -use Symfony\UX\Map\Provider\GoogleMaps\StreetViewControlOptions; -use Symfony\UX\Map\Provider\GoogleMaps\ZoomControlOptions; - -class MapTest extends TestCase -{ - public function testCreateViewWithDefaultOptions(): void - { - $map = new Map('map_name'); - - self::assertEquals([ - 'mapId' => null, - 'center' => null, - 'zoom' => null, - 'gestureHandling' => 'auto', - 'backgroundColor' => null, - 'disableDoubleClickZoom' => false, - 'zoomControl' => true, - 'zoomControlOptions' => [ - 'position' => 22, - ], - 'mapTypeControl' => true, - 'mapTypeControlOptions' => [ - 'mapTypeIds' => [], - 'position' => 14, - 'style' => 0, - ], - 'streetViewControl' => true, - 'streetViewControlOptions' => [ - 'position' => 22, - ], - 'fullscreenControl' => true, - 'fullscreenControlOptions' => [ - 'position' => 20, - ], - 'fitBoundsToMarkers' => false, - 'markers' => [], - 'infoWindows' => [], - ], $map->createView()); - } - - public function testCreateViewWithCustomOptions(): void - { - $map = (new Map( - name: 'map_name', - )) - ->setCenter(new LatLng(48.8566, 2.3522)) - ->setZoom(12) - ->addMarker($paris = new Marker(position: new LatLng(48.8566, 2.3522), title: 'Paris')) - ->addMarker($lyon = new Marker(position: new LatLng(45.7640, 4.8357), title: 'Lyon')) - ->addMarker($marseille = new Marker(position: new LatLng(43.2965, 5.3698), title: 'Marseille')) - ->addInfoWindow(new InfoWindow( - headerContent: 'Paris', - content: "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", - marker: $paris, - )) - ->addInfoWindow(new InfoWindow( - headerContent: 'Lyon', - content: 'Ville française de la région historique Rhône-Alpes, se trouve à la jonction du Rhône et de la Saône.', - marker: $lyon - )) - ->addInfoWindow(new InfoWindow( - headerContent: 'Marseille', - content: 'Ville portuaire du sud de la France, est une ville cosmopolite qui a été un centre d\'échanges commerciaux et culturels depuis sa fondation par les Grecs vers 600 av. J.-C.', - marker: $marseille, - )) - ->addInfoWindow(new InfoWindow( - headerContent: 'Strasbourg', - content: 'Ville française située dans le Grand Est, est également le siège du Parlement européen.', - position: new LatLng(48.5734, 7.7521), - opened: true, - )) - ->setMapId('2b2d73ba4b8c7b41') - ->setGestureHandling(GestureHandling::Greedy) - ->setBackgroundColor('#f0f0f0') - ->enableDoubleClickZoom(false) - ->enableZoomControl(false) - ->setZoomControlOptions(new ZoomControlOptions( - position: ControlPosition::BLOCK_END_INLINE_END - )) - ->enableMapTypeControl(false) - ->setMapTypeControlOptions(new MapTypeControlOptions( - mapTypeIds: ['roadmap', 'satellite', MapTypeId::TERRAIN], - position: ControlPosition::BLOCK_END_INLINE_START, - style: MapTypeControlStyle::HORIZONTAL_BAR, - )) - ->enableStreetViewControl(false) - ->setStreetViewControlOptions(new StreetViewControlOptions( - position: ControlPosition::BLOCK_END_INLINE_START, - )) - ->enableFullscreenControl(false) - ->setFullscreenControlOptions(new FullscreenControlOptions( - position: ControlPosition::BLOCK_END_INLINE_START, - )) - ->enableFitBoundsToMarkers(false); - - self::assertEquals([ - 'mapId' => '2b2d73ba4b8c7b41', - 'center' => ['lat' => 48.8566, 'lng' => 2.3522], - 'zoom' => 12.0, - 'gestureHandling' => 'greedy', - 'backgroundColor' => '#f0f0f0', - 'disableDoubleClickZoom' => true, - 'zoomControl' => false, - 'zoomControlOptions' => [ - 'position' => ControlPosition::BLOCK_END_INLINE_END->value, - ], - 'mapTypeControl' => false, - 'mapTypeControlOptions' => [ - 'mapTypeIds' => ['roadmap', 'satellite', 'terrain'], - 'position' => ControlPosition::BLOCK_END_INLINE_START->value, - 'style' => 1, - ], - 'streetViewControl' => false, - 'streetViewControlOptions' => [ - 'position' => ControlPosition::BLOCK_END_INLINE_START->value, - ], - 'fullscreenControl' => false, - 'fullscreenControlOptions' => [ - 'position' => ControlPosition::BLOCK_END_INLINE_START->value, - ], - 'fitBoundsToMarkers' => false, - 'markers' => [ - [ - '_id' => $paris->getId(), - 'position' => ['lat' => 48.8566, 'lng' => 2.3522], - 'title' => 'Paris', - ], - [ - '_id' => $lyon->getId(), - 'position' => ['lat' => 45.764, 'lng' => 4.8357], - 'title' => 'Lyon', - ], - [ - '_id' => $marseille->getId(), - 'position' => ['lat' => 43.2965, 'lng' => 5.3698], - 'title' => 'Marseille', - ], - ], - 'infoWindows' => [ - [ - '_markerId' => $paris->getId(), - 'headerContent' => 'Paris', - 'content' => "Capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", - 'position' => ['lat' => 48.8566, 'lng' => 2.3522], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => $lyon->getId(), - 'headerContent' => 'Lyon', - 'content' => 'Ville française de la région historique Rhône-Alpes, se trouve à la jonction du Rhône et de la Saône.', - 'position' => ['lat' => 45.764, 'lng' => 4.8357], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => $marseille->getId(), - 'headerContent' => 'Marseille', - 'content' => 'Ville portuaire du sud de la France, est une ville cosmopolite qui a été un centre d\'échanges commerciaux et culturels depuis sa fondation par les Grecs vers 600 av. J.-C.', - 'position' => ['lat' => 43.2965, 'lng' => 5.3698], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => null, - 'headerContent' => 'Strasbourg', - 'content' => 'Ville française située dans le Grand Est, est également le siège du Parlement européen.', - 'position' => ['lat' => 48.5734, 'lng' => 7.7521], - 'opened' => true, - 'autoClose' => true, - ], - ], - ], $map->createView()); - } -} diff --git a/src/Map/tests/Provider/GoogleMaps/MarkerTest.php b/src/Map/tests/Provider/GoogleMaps/MarkerTest.php deleted file mode 100644 index 2da87edabdf..00000000000 --- a/src/Map/tests/Provider/GoogleMaps/MarkerTest.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\GoogleMaps; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\GoogleMaps\Marker; - -class MarkerTest extends TestCase -{ - public function testCreateView(): void - { - $marker = new Marker( - new LatLng(48.8566, 2.3522), - 'Paris' - ); - - self::assertSame([ - '_id' => $marker->getId(), - 'position' => [ - 'lat' => 48.8566, - 'lng' => 2.3522, - ], - 'title' => 'Paris', - ], $marker->createView()); - } -} diff --git a/src/Map/tests/Provider/GoogleMaps/ControlPositionTest.php b/src/Map/tests/Provider/GoogleMaps/Option/ControlPositionTest.php similarity index 94% rename from src/Map/tests/Provider/GoogleMaps/ControlPositionTest.php rename to src/Map/tests/Provider/GoogleMaps/Option/ControlPositionTest.php index 0b528e19009..ec4b7174055 100644 --- a/src/Map/tests/Provider/GoogleMaps/ControlPositionTest.php +++ b/src/Map/tests/Provider/GoogleMaps/Option/ControlPositionTest.php @@ -1,7 +1,5 @@ ControlPosition::BLOCK_END_INLINE_CENTER->value, - ], $options->createView()); + ], $options->toArray()); } } diff --git a/src/Map/tests/Provider/GoogleMaps/GestureHandlingTest.php b/src/Map/tests/Provider/GoogleMaps/Option/GestureHandlingTest.php similarity index 82% rename from src/Map/tests/Provider/GoogleMaps/GestureHandlingTest.php rename to src/Map/tests/Provider/GoogleMaps/Option/GestureHandlingTest.php index 02894d17d1f..bb0a21e2f0e 100644 --- a/src/Map/tests/Provider/GoogleMaps/GestureHandlingTest.php +++ b/src/Map/tests/Provider/GoogleMaps/Option/GestureHandlingTest.php @@ -1,7 +1,5 @@ ['satellite', 'hybrid'], 'position' => ControlPosition::BLOCK_END_INLINE_END->value, 'style' => MapTypeControlStyle::HORIZONTAL_BAR->value, - ], $options->createView()); + ], $options->toArray()); } } diff --git a/src/Map/tests/Provider/GoogleMaps/MapTypeControlStyleTest.php b/src/Map/tests/Provider/GoogleMaps/Option/MapTypeControlStyleTest.php similarity index 81% rename from src/Map/tests/Provider/GoogleMaps/MapTypeControlStyleTest.php rename to src/Map/tests/Provider/GoogleMaps/Option/MapTypeControlStyleTest.php index 78abf3ad7f8..0bf56be6fae 100644 --- a/src/Map/tests/Provider/GoogleMaps/MapTypeControlStyleTest.php +++ b/src/Map/tests/Provider/GoogleMaps/Option/MapTypeControlStyleTest.php @@ -1,7 +1,5 @@ ControlPosition::INLINE_END_BLOCK_CENTER->value, - ], $options->createView()); + ], $options->toArray()); } } diff --git a/src/Map/tests/Provider/GoogleMaps/ZoomControlOptionsTest.php b/src/Map/tests/Provider/GoogleMaps/Option/ZoomControlOptionsTest.php similarity index 66% rename from src/Map/tests/Provider/GoogleMaps/ZoomControlOptionsTest.php rename to src/Map/tests/Provider/GoogleMaps/Option/ZoomControlOptionsTest.php index cf808c08bdb..b2e74a238a8 100644 --- a/src/Map/tests/Provider/GoogleMaps/ZoomControlOptionsTest.php +++ b/src/Map/tests/Provider/GoogleMaps/Option/ZoomControlOptionsTest.php @@ -1,7 +1,5 @@ ControlPosition::BOTTOM_CENTER->value, - ], $options->createView()); + ], $options->toArray()); } } diff --git a/src/Map/tests/Provider/Leaflet/LeafletOptionsTest.php b/src/Map/tests/Provider/Leaflet/LeafletOptionsTest.php new file mode 100644 index 00000000000..586203723d1 --- /dev/null +++ b/src/Map/tests/Provider/Leaflet/LeafletOptionsTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\Leaflet; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Provider\Leaflet\LeafletOptions; +use Symfony\UX\Map\Provider\Leaflet\Option as LeafletOption; + +class LeafletOptionsTest extends TestCase +{ + public function testWithMinimalConfiguration(): void + { + $leafletOptions = new LeafletOptions(); + + self::assertSame([ + 'tileLayer' => [ + 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + 'attribution' => '© OpenStreetMap', + 'options' => [], + ], + ], $leafletOptions->toArray()); + } + + public function testWithMaximumConfiguration(): void + { + $leafletOptions = new LeafletOptions( + tileLayer: new LeafletOption\TileLayer( + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + attribution: '© OpenStreetMap', + options: [ + 'maxZoom' => 19, + 'minZoom' => 1, + 'maxNativeZoom' => 18, + 'zoomOffset' => 0, + ], + ), + ); + + self::assertSame([ + 'tileLayer' => [ + 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + 'attribution' => '© OpenStreetMap', + 'options' => [ + 'maxZoom' => 19, + 'minZoom' => 1, + 'maxNativeZoom' => 18, + 'zoomOffset' => 0, + ], + ], + ], $leafletOptions->toArray()); + } +} diff --git a/src/Map/tests/Provider/Leaflet/LeafletProviderFactoryTest.php b/src/Map/tests/Provider/Leaflet/LeafletProviderFactoryTest.php new file mode 100644 index 00000000000..c6692cf9390 --- /dev/null +++ b/src/Map/tests/Provider/Leaflet/LeafletProviderFactoryTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\Leaflet; + +use Symfony\UX\Map\Provider\Leaflet\LeafletProviderFactory; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Test\ProviderFactoryTestCase; + +final class LeafletProviderFactoryTest extends ProviderFactoryTestCase +{ + public function createFactory(): ProviderFactoryInterface + { + return new LeafletProviderFactory(); + } + + public static function supportsProvider(): iterable + { + yield [true, 'leaflet://default']; + yield [false, 'foo://default']; + } + + public static function createProvider(): iterable + { + yield [ + 'leaflet://default', + 'leaflet://default', + ]; + } + + public static function unsupportedSchemeProvider(): iterable + { + yield ['somethingElse://foo@default']; + } +} diff --git a/src/Map/tests/Provider/Leaflet/LeafletProviderTest.php b/src/Map/tests/Provider/Leaflet/LeafletProviderTest.php new file mode 100644 index 00000000000..bb22e3b60fc --- /dev/null +++ b/src/Map/tests/Provider/Leaflet/LeafletProviderTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\Leaflet; + +use Symfony\UX\Map\Provider\Leaflet\LeafletProvider; +use Symfony\UX\Map\Test\ProviderTestCase; + +class LeafletProviderTest extends ProviderTestCase +{ + public function testGetName(): void + { + $leafletProvider = new LeafletProvider(); + + self::assertEquals('leaflet', $leafletProvider->getName()); + } + + public function provideTestOptions(): iterable + { + yield [ + new LeafletProvider(), + 'leaflet://default', + [], + ]; + } +} diff --git a/src/Map/tests/Provider/Leaflet/MapFactoryTest.php b/src/Map/tests/Provider/Leaflet/MapFactoryTest.php deleted file mode 100644 index 17de970b05e..00000000000 --- a/src/Map/tests/Provider/Leaflet/MapFactoryTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\Leaflet; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\Provider\Leaflet\Map; -use Symfony\UX\Map\Provider\Leaflet\MapFactory; - -class MapFactoryTest extends TestCase -{ - public function testCreate(): void - { - $mapFactory = new MapFactory(); - - $map = $mapFactory->createMap('map_name'); - - self::assertInstanceOf(Map::class, $map); - self::assertSame('map_name', $map->getName()); - - $view = $map->createView(); - self::assertNull($view['center']); - self::assertNull($view['zoom']); - self::assertFalse($view['fitBoundsToMarkers']); - self::assertEquals([ - 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap', - ], $view['tileLayer']); - } - - public function testCreateWithCustomOptions(): void - { - $mapFactory = new MapFactory(); - - $map = $mapFactory->createMap('map_name', [ - 'center' => [37.7749, -122.4194], - 'zoom' => 3, - 'tileLayer' => [ - 'url' => 'https://foobar.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap contributors (kudos)', - 'options' => [ - 'maxZoom' => 19, - ], - ], - 'fitBoundsToMarkers' => true, - ]); - - self::assertInstanceOf(Map::class, $map); - self::assertSame('map_name', $map->getName()); - - $view = $map->createView(); - self::assertSame(['lat' => 37.7749, 'lng' => -122.4194], $view['center']); - self::assertSame(3.0, $view['zoom']); - self::assertTrue($view['fitBoundsToMarkers']); - - self::assertEquals([ - 'url' => 'https://foobar.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap contributors (kudos)', - 'maxZoom' => 19, - ], $view['tileLayer']); - } -} diff --git a/src/Map/tests/Provider/Leaflet/MapTest.php b/src/Map/tests/Provider/Leaflet/MapTest.php deleted file mode 100644 index e365f632f2e..00000000000 --- a/src/Map/tests/Provider/Leaflet/MapTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\Leaflet; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\Leaflet; -use Symfony\UX\Map\Provider\Leaflet\Map; -use Symfony\UX\Map\Provider\Leaflet\Marker; -use Symfony\UX\Map\Provider\Leaflet\TileLayer; - -class MapTest extends TestCase -{ - public function testCreateViewWithDefaultOptions(): void - { - $map = new Map( - 'map_name', - new TileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', '© OpenStreetMap') - ); - - self::assertEquals([ - 'center' => null, - 'zoom' => null, - 'fitBoundsToMarkers' => false, - 'markers' => [], - 'popups' => [], - 'tileLayer' => [ - 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap', - ], - ], $map->createView()); - } - - public function testCreateViewWithCustomOptions(): void - { - $map = (new Map( - name: 'map_name', - tileLayer: new TileLayer( - url: 'https://foobar.org/{z}/{x}/{y}.png', - attribution: '© OpenStreetMap contributors (kudos)', - options: ['maxZoom' => 19] - ) - )) - ->setCenter(new LatLng(48.8566, 2.3522)) - ->setZoom(12) - ->addMarker($paris = new Marker(position: new LatLng(48.8566, 2.3522), title: 'Paris')) - ->addMarker($lyon = new Marker(position: new LatLng(45.7640, 4.8357), title: 'Lyon')) - ->addMarker($marseille = new Marker(position: new LatLng(43.2965, 5.3698), title: 'Marseille')) - ->addPopup(new Leaflet\Popup( - content: "Paris, capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", - marker: $paris, - )) - ->addPopup(new Leaflet\Popup( - content: 'Lyon, ville française de la région historique Rhône-Alpes, se trouve à la jonction du Rhône et de la Saône.', - marker: $lyon - )) - ->addPopup(new Leaflet\Popup( - content: 'Marseille, ville portuaire du sud de la France, est une ville cosmopolite qui a été un centre d\'échanges commerciaux et culturels depuis sa fondation par les Grecs vers 600 av. J.-C.', - marker: $marseille, - )) - ->addPopup(new Leaflet\Popup( - content: 'Strasbourg, ville française située dans le Grand Est, est également le siège du Parlement européen.', - position: new LatLng(48.5734, 7.7521), - opened: true, - )) - ->enableFitBoundsToMarkers(false); - - self::assertEquals([ - 'center' => ['lat' => 48.8566, 'lng' => 2.3522], - 'zoom' => 12.0, - 'fitBoundsToMarkers' => false, - 'tileLayer' => [ - 'url' => 'https://foobar.org/{z}/{x}/{y}.png', - 'attribution' => '© OpenStreetMap contributors (kudos)', - 'maxZoom' => 19, - ], - 'markers' => [ - [ - '_id' => $paris->getId(), - 'position' => ['lat' => 48.8566, 'lng' => 2.3522], - 'title' => 'Paris', - 'riseOnHover' => false, - 'riseOffset' => 250, - 'draggable' => false, - ], - [ - '_id' => $lyon->getId(), - 'position' => ['lat' => 45.764, 'lng' => 4.8357], - 'title' => 'Lyon', - 'riseOnHover' => false, - 'riseOffset' => 250, - 'draggable' => false, - ], - [ - '_id' => $marseille->getId(), - 'position' => ['lat' => 43.2965, 'lng' => 5.3698], - 'title' => 'Marseille', - 'riseOnHover' => false, - 'riseOffset' => 250, - 'draggable' => false, - ], - ], - 'popups' => [ - [ - '_markerId' => $paris->getId(), - 'content' => "Paris, capitale de la France, est une grande ville européenne et un centre mondial de l'art, de la mode, de la gastronomie et de la culture.", - 'position' => ['lat' => 48.8566, 'lng' => 2.3522], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => $lyon->getId(), - 'content' => 'Lyon, ville française de la région historique Rhône-Alpes, se trouve à la jonction du Rhône et de la Saône.', - 'position' => ['lat' => 45.764, 'lng' => 4.8357], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => $marseille->getId(), - 'content' => 'Marseille, ville portuaire du sud de la France, est une ville cosmopolite qui a été un centre d\'échanges commerciaux et culturels depuis sa fondation par les Grecs vers 600 av. J.-C.', - 'position' => ['lat' => 43.2965, 'lng' => 5.3698], - 'opened' => false, - 'autoClose' => true, - ], - [ - '_markerId' => null, - 'content' => 'Strasbourg, ville française située dans le Grand Est, est également le siège du Parlement européen.', - 'position' => ['lat' => 48.5734, 'lng' => 7.7521], - 'opened' => true, - 'autoClose' => true, - ], - ], - ], $map->createView()); - } -} diff --git a/src/Map/tests/Provider/Leaflet/MarkerTest.php b/src/Map/tests/Provider/Leaflet/MarkerTest.php deleted file mode 100644 index 160850587d4..00000000000 --- a/src/Map/tests/Provider/Leaflet/MarkerTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\Leaflet; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\Leaflet\Marker; - -class MarkerTest extends TestCase -{ - public function testCreateView(): void - { - $marker = new Marker( - new LatLng(48.8566, 2.3522), - 'Paris' - ); - - self::assertSame([ - '_id' => $marker->getId(), - 'position' => [ - 'lat' => 48.8566, - 'lng' => 2.3522, - ], - 'title' => 'Paris', - 'riseOnHover' => false, - 'riseOffset' => 250, - 'draggable' => false, - ], $marker->createView()); - } -} diff --git a/src/Map/tests/Provider/Leaflet/Option/TileLayerTest.php b/src/Map/tests/Provider/Leaflet/Option/TileLayerTest.php new file mode 100644 index 00000000000..a0e1c262581 --- /dev/null +++ b/src/Map/tests/Provider/Leaflet/Option/TileLayerTest.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider\Leaflet\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Provider\Leaflet\Option as LeafletOption; + +class TileLayerTest extends TestCase +{ + public function testToArray() + { + $tileLayer = new LeafletOption\TileLayer( + url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + attribution: '© OpenStreetMap contributors', + options: [ + 'maxZoom' => 19, + ], + ); + + self::assertSame([ + 'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + 'attribution' => '© OpenStreetMap contributors', + 'options' => [ + 'maxZoom' => 19, + ], + ], $tileLayer->toArray()); + } +} diff --git a/src/Map/tests/Provider/Leaflet/PopupTest.php b/src/Map/tests/Provider/Leaflet/PopupTest.php deleted file mode 100644 index 03bb397279e..00000000000 --- a/src/Map/tests/Provider/Leaflet/PopupTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\UX\Map\Tests\Provider\Leaflet; - -use PHPUnit\Framework\TestCase; -use Symfony\UX\Map\Exception\InvalidArgumentException; -use Symfony\UX\Map\LatLng; -use Symfony\UX\Map\Provider\Leaflet\Marker; -use Symfony\UX\Map\Provider\Leaflet\Popup; - -class PopupTest extends TestCase -{ - public function testConstructShouldThrowIfNoPositionOrMarkerIsPassed(): void - { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('An "Symfony\UX\Map\Provider\Leaflet\Popup" must be associated with a position or a marker.'); - - new Popup(content: 'Hello!'); - } - - public function testMarkerPositionShouldTakePrecedenceOverPosition(): void - { - $Popup = new Popup( - content: 'Hello!', - marker: new Marker(position: new LatLng(1, 2)), - position: new LatLng(3, 4) - ); - - $position = \Closure::bind(fn () => $Popup->position, $Popup, Popup::class)(); - - self::assertEquals(new LatLng(1, 2), $position); - } - - public function testPositionFallbackToPositionIfNoMarkerIsPassed(): void - { - $Popup = new Popup( - content: 'Hello!', - position: new LatLng(3, 4) - ); - - $position = \Closure::bind(fn () => $Popup->position, $Popup, Popup::class)(); - - self::assertEquals(new LatLng(3, 4), $position); - } - - public function testCreateView(): void - { - $Popup = new Popup( - content: 'Hello!', - position: new LatLng(3, 4) - ); - - self::assertEquals([ - 'content' => 'Hello!', - 'position' => ['lat' => 3, 'lng' => 4], - 'opened' => false, - 'autoClose' => true, - '_markerId' => null, - ], $Popup->createView()); - } -} diff --git a/src/Map/tests/Provider/ProviderTest.php b/src/Map/tests/Provider/ProviderTest.php new file mode 100644 index 00000000000..290b28b7726 --- /dev/null +++ b/src/Map/tests/Provider/ProviderTest.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Tests\Provider; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Exception\UnsupportedSchemeException; +use Symfony\UX\Map\Provider\Provider; +use Symfony\UX\Map\Provider\ProviderFactoryInterface; +use Symfony\UX\Map\Provider\ProviderInterface; + +final class ProviderTest extends TestCase +{ + public function testUnsupportedSchemeException(): void + { + $this->expectException(UnsupportedSchemeException::class); + $this->expectExceptionMessage('The provider "scheme" is not supported.'); + + $provider = new Provider([]); + $provider->fromString('scheme://default'); + } + + public function testSupportedFactory(): void + { + $provider = new Provider([ + 'one' => $oneFactory = $this->createMock(ProviderFactoryInterface::class), + 'two' => $twoFactory = $this->createMock(ProviderFactoryInterface::class), + ]); + + $oneFactory->expects($this->once())->method('supports')->willReturn(false); + $twoFactory->expects($this->once())->method('supports')->willReturn(true); + $twoFactory->expects($this->once())->method('create')->willReturn($twoProvider = $this->createMock(ProviderInterface::class)); + + $provider = $provider->fromString('scheme://default'); + + self::assertSame($twoProvider, $provider); + } +} diff --git a/src/Map/tests/TwigTest.php b/src/Map/tests/TwigTest.php index 34df090ebeb..d8ba5583a5a 100644 --- a/src/Map/tests/TwigTest.php +++ b/src/Map/tests/TwigTest.php @@ -1,7 +1,5 @@ load(function (ContainerBuilder $container) { $container->loadFromExtension('ux_map', [ - 'providers' => [ - 'google_maps' => [ - 'provider' => 'google_maps', - 'options' => [ - 'key' => 'GOOGLE_MAPS_API_KEY', - ], - ], - 'google_maps_2' => [ - 'provider' => 'google_maps', - 'options' => [ - 'key' => 'GOOGLE_MAPS_API_KEY_2', - ], - ], - 'leaflet' => [ - 'provider' => 'leaflet', - ], - ], - 'maps' => [ - 'google_maps_map_1' => [ - 'provider' => 'google_maps', - ], - 'google_maps_map_2' => [ - 'provider' => 'google_maps', - ], - 'google_maps_with_another_google_maps_provider' => [ - 'provider' => 'google_maps_2', - ], - 'leaflet_map_1' => [ - 'provider' => 'leaflet', - ], - ], + 'provider' => 'google-maps://GOOGLE_MAPS_API_KEY@default', ]); }); } @@ -86,64 +55,52 @@ public function testRenderScriptTagsShouldOutputTheScriptTagsForTheMaps(): void $twig = self::getContainer()->get('twig'); $mapFactory = self::getContainer()->get('ux_map.map_factory'); - $mapFactory->createMap('google_maps_map_1'); + $mapFactory->createMap(); self::assertStringContainsString( - '', + '', $twig->createTemplate('{{ ux_map_script_tags() }}')->render() ); - $mapFactory->createMap('google_maps_map_2'); + $mapFactory->createMap(); self::assertStringContainsString( - '', - $twig->createTemplate('{{ ux_map_script_tags() }}')->render() + '', + $twig->createTemplate('{{ ux_map_script_tags() }}')->render(), + 'The script tags should only be rendered once per provider' ); - $mapFactory->createMap('leaflet_map_1'); + $mapFactory->createMap(new LeafletProvider()); self::assertStringContainsString( - '', - $twig->createTemplate('{{ ux_map_script_tags() }}')->render() + '', + $twig->createTemplate('{{ ux_map_script_tags() }}')->render(), + '' ); } - public function testRenderScriptTagsShouldFailIfWeHaveTheSameKindOfProviderOnThePage(): void - { - $twig = self::getContainer()->get('twig'); - $mapFactory = self::getContainer()->get('ux_map.map_factory'); - - $mapFactory->createMap('google_maps_map_1'); - $mapFactory->createMap('google_maps_with_another_google_maps_provider'); - - try { - $twig->createTemplate('{{ ux_map_script_tags() }}')->render(); - self::assertTrue(false, 'This should not be reached.'); - } catch (\Twig\Error\RuntimeError $e) { - self::assertInstanceOf(ConflictingMapProvidersOnSamePageException::class, $e->getPrevious()); - self::assertSame('You cannot use the "google_maps" map provider on the same page as the following map providers: "google_maps_2", as their configuration will conflicts with each-other.', $e->getPrevious()->getMessage()); - } - } - public function testRenderMap(): void { $twig = self::getContainer()->get('twig'); $mapFactory = self::getContainer()->get('ux_map.map_factory'); - $map = $mapFactory->createMap('google_maps_map_1'); + $map = $mapFactory->createMap(); + $map + ->center(new LatLng(48.8566, 2.3522)) + ->zoom(12); self::assertStringContainsString( - '
', + '
', $twig->createTemplate('{{ render_map(map) }}')->render([ 'map' => $map, ]) ); self::assertStringContainsString( - '
', + '
', $twig->createTemplate('{{ render_map(map, { "data-controller": "my-map", "class": "foo" }) }}')->render([ 'map' => $map, - ]) + ]), ); } } diff --git a/src/Map/tests/UxMapBundleTest.php b/src/Map/tests/UxMapBundleTest.php index c7876986223..3c248ec4d8a 100644 --- a/src/Map/tests/UxMapBundleTest.php +++ b/src/Map/tests/UxMapBundleTest.php @@ -12,8 +12,6 @@ namespace Symfony\UX\Map\Tests; use PHPUnit\Framework\TestCase; -use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; use Symfony\UX\Map\Tests\Kernel\FrameworkAppKernel; use Symfony\UX\Map\Tests\Kernel\TwigAppKernel; @@ -34,76 +32,4 @@ public function testBootKernel(Kernel $kernel) $kernel->boot(); self::assertArrayHasKey('UXMapBundle', $kernel->getBundles()); } - - public function testMinimalConfigurationShouldNotThrow() - { - self::expectNotToPerformAssertions(); - - $kernel = $this->createAndConfigureKernel(uxMapConfiguration: []); - $kernel->boot(); - } - - public function testValidateSupportedProvider() - { - $this->expectException(\Symfony\Component\Config\Definition\Exception\InvalidConfigurationException::class); - $this->expectExceptionMessage('The provider "foo" is not supported.'); - - $kernel = $this->createAndConfigureKernel(uxMapConfiguration: [ - 'providers' => [ - 'foo' => [ - 'provider' => 'foo', - ], - ], - ]); - $kernel->boot(); - } - - public function testGoogleProviderValidation() - { - $this->expectException(\Symfony\Component\Config\Definition\Exception\InvalidConfigurationException::class); - $this->expectExceptionMessage('Invalid configuration for path "ux_map.providers.google_maps": The "key" option is required for the "google_maps" provider.'); - - $kernel = $this->createAndConfigureKernel(uxMapConfiguration: [ - 'providers' => [ - 'google_maps' => [ - 'provider' => 'google_maps', - ], - ], - ]); - $kernel->boot(); - } - - public function testMapProviderValidation() - { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The provider "google_map" for the map "google_map_1" is not found, has it been correctly registered?'); - - $kernel = $this->createAndConfigureKernel(uxMapConfiguration: [ - 'maps' => [ - 'google_map_1' => [ - 'provider' => 'google_map', - ], - ], - ]); - $kernel->boot(); - } - - private function createAndConfigureKernel(array $uxMapConfiguration) - { - return new class('test', true, $uxMapConfiguration) extends TwigAppKernel { - public function __construct(string $environment, bool $debug, private array $uxMapConfiguration) - { - parent::__construct($environment, $debug); - } - - public function registerContainerConfiguration(LoaderInterface $loader) - { - parent::registerContainerConfiguration($loader); - - $loader->load(function (ContainerBuilder $container) { - $container->loadFromExtension('ux_map', $this->uxMapConfiguration); - }); - } - }; - } }