From 12cb4b2dd1a901ad53e127b54a378a476c5e4a38 Mon Sep 17 00:00:00 2001 From: Hugo Alliaume Date: Mon, 10 Jun 2024 23:06:54 +0200 Subject: [PATCH] [Map] Create Map component --- .gitattributes | 4 + .gitignore | 3 + CHANGELOG.md | 5 + LICENSE | 19 ++ README.md | 87 ++++++++ assets/dist/map_controller.d.ts | 26 +++ assets/dist/map_controller.js | 103 ++++++++++ assets/package.json | 39 ++++ assets/src/map_controller.ts | 187 ++++++++++++++++++ assets/test/map_controller.test.ts | 62 ++++++ assets/vitest.config.js | 17 ++ composer.json | 33 ++++ phpunit.xml.dist | 26 +++ src/GoogleOptions.php | 157 +++++++++++++++ src/Option/ControlPosition.php | 80 ++++++++ src/Option/FullscreenControlOptions.php | 34 ++++ src/Option/GestureHandling.php | 45 +++++ src/Option/MapTypeControlOptions.php | 41 ++++ src/Option/MapTypeControlStyle.php | 38 ++++ src/Option/StreetViewControlOptions.php | 34 ++++ src/Option/ZoomControlOptions.php | 34 ++++ src/Renderer/GoogleRenderer.php | 83 ++++++++ src/Renderer/GoogleRendererFactory.php | 51 +++++ tests/GoogleOptionsTest.php | 68 +++++++ tests/GoogleRendererFactoryTest.php | 49 +++++ tests/GoogleRendererTest.php | 75 +++++++ tests/Option/ControlPositionTest.php | 34 ++++ tests/Option/FullscreenControlOptionsTest.php | 30 +++ tests/Option/GestureHandlingTest.php | 26 +++ tests/Option/MapTypeControlOptionsTest.php | 35 ++++ tests/Option/MapTypeControlStyleTest.php | 25 +++ tests/Option/StreetViewControlOptionsTest.php | 30 +++ tests/Option/ZoomControlOptionsTest.php | 30 +++ 33 files changed, 1610 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/dist/map_controller.d.ts create mode 100644 assets/dist/map_controller.js create mode 100644 assets/package.json create mode 100644 assets/src/map_controller.ts create mode 100644 assets/test/map_controller.test.ts create mode 100644 assets/vitest.config.js create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/GoogleOptions.php create mode 100644 src/Option/ControlPosition.php create mode 100644 src/Option/FullscreenControlOptions.php create mode 100644 src/Option/GestureHandling.php create mode 100644 src/Option/MapTypeControlOptions.php create mode 100644 src/Option/MapTypeControlStyle.php create mode 100644 src/Option/StreetViewControlOptions.php create mode 100644 src/Option/ZoomControlOptions.php create mode 100644 src/Renderer/GoogleRenderer.php create mode 100644 src/Renderer/GoogleRendererFactory.php create mode 100644 tests/GoogleOptionsTest.php create mode 100644 tests/GoogleRendererFactoryTest.php create mode 100644 tests/GoogleRendererTest.php create mode 100644 tests/Option/ControlPositionTest.php create mode 100644 tests/Option/FullscreenControlOptionsTest.php create mode 100644 tests/Option/GestureHandlingTest.php create mode 100644 tests/Option/MapTypeControlOptionsTest.php create mode 100644 tests/Option/MapTypeControlStyleTest.php create mode 100644 tests/Option/StreetViewControlOptionsTest.php create mode 100644 tests/Option/ZoomControlOptionsTest.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..84c7add --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2b5de26 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## Unreleased + +- Bridge added diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e374a5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +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 +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..449db09 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Symfony UX Map: Google Maps + +[Google Maps](https://developers.google.com/maps/documentation/javascript/overview) integration for Symfony UX Map. + +## DSN example + +```dotenv +UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default + +# With options +UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default?version=weekly +UX_MAP_DSN=google://GOOGLE_MAPS_API_KEY@default?language=fr®ion=FR +``` + +Available options: + +| Option | Description | Default | +|------------|------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------| +| `id` | The id of the script tag | `__googleMapsScriptId` | +| `language` | Force language, see [list of supported languages](https://developers.google.com/maps/faq#languagesupport) specified in the browser | The user's preferred language | +| `region` | Unicode region subtag identifiers compatible with [ISO 3166-1](https://en.wikipedia.org/wiki/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 script | `https://maps.googleapis.com/maps/api/js` | +| `version` | The release channels or version numbers | `weekly` | + +## Map options + +You can use the `GoogleOptions` class to configure your `Map`:: + +```php +use Symfony\UX\Map\Bridge\Google\GoogleOptions; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\FullscreenControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\GestureHandling; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlStyle; +use Symfony\UX\Map\Bridge\Google\Option\StreetViewControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\ZoomControlOptions; +use Symfony\UX\Map\Point; +use Symfony\UX\Map\Map; + +$map = (new Map()) + ->center(new Point(48.8566, 2.3522)) + ->zoom(6); + +// To configure controls options, and some other options: +$googleOptions = (new GoogleOptions()) + ->mapId('YOUR_MAP_ID') + ->gestureHandling(GestureHandling::GREEDY) + ->backgroundColor('#f00') + ->doubleClickZoom(true) + ->zoomControlOptions(new ZoomControlOptions( + position: ControlPosition::BLOCK_START_INLINE_END, + )) + ->mapTypeControlOptions(new MapTypeControlOptions( + mapTypeIds: ['roadmap'], + position: ControlPosition::INLINE_END_BLOCK_START, + style: MapTypeControlStyle::DROPDOWN_MENU, + )) + ->streetViewControlOptions(new StreetViewControlOptions( + position: ControlPosition::BLOCK_END_INLINE_START, + )) + ->fullscreenControlOptions(new FullscreenControlOptions( + position: ControlPosition::INLINE_START_BLOCK_END, + )) +; + +// To disable controls: +$googleOptions = (new GoogleOptions()) + ->mapId('YOUR_MAP_ID') + ->zoomControl(false) + ->mapTypeControl(false) + ->streetViewControl(false) + ->fullscreenControl(false) +; + +// Add the custom options to the map +$map->options($googleOptions); +``` + +## Resources + +- [Documentation](https://symfony.com/bundles/ux-map/current/index.html) +- [Report issues](https://github.com/symfony/ux/issues) and + [send Pull Requests](https://github.com/symfony/ux/pulls) + in the [main Symfony UX repository](https://github.com/symfony/ux) diff --git a/assets/dist/map_controller.d.ts b/assets/dist/map_controller.d.ts new file mode 100644 index 0000000..406671b --- /dev/null +++ b/assets/dist/map_controller.d.ts @@ -0,0 +1,26 @@ +/// +import AbstractMapController from '@symfony/ux-map/abstract-map-controller'; +import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller'; +import type { LoaderOptions } from '@googlemaps/js-api-loader'; +type MapOptions = Pick; +export default class extends AbstractMapController { + static values: { + providerOptions: ObjectConstructor; + }; + providerOptionsValue: Pick; + connect(): Promise; + protected doCreateMap({ center, zoom, options, }: { + center: Point; + zoom: number; + options: MapOptions; + }): google.maps.Map; + protected doCreateMarker(definition: MarkerDefinition): google.maps.marker.AdvancedMarkerElement; + protected doCreateInfoWindow({ definition, marker, }: { + definition: MarkerDefinition['infoWindow']; + marker: google.maps.marker.AdvancedMarkerElement; + }): google.maps.InfoWindow; + private createTextOrElement; + private closeInfoWindowsExcept; + protected doFitBoundsToMarkers(): void; +} +export {}; diff --git a/assets/dist/map_controller.js b/assets/dist/map_controller.js new file mode 100644 index 0000000..d74b126 --- /dev/null +++ b/assets/dist/map_controller.js @@ -0,0 +1,103 @@ +import AbstractMapController from '@symfony/ux-map/abstract-map-controller'; +import { Loader } from '@googlemaps/js-api-loader'; + +let loader; +let library; +class default_1 extends AbstractMapController { + async connect() { + if (!loader) { + loader = new Loader(this.providerOptionsValue); + } + const { Map: _Map, InfoWindow } = await loader.importLibrary('maps'); + const { AdvancedMarkerElement } = await loader.importLibrary('marker'); + library = { _Map, AdvancedMarkerElement, InfoWindow }; + super.connect(); + } + doCreateMap({ center, zoom, options, }) { + options.zoomControl = typeof options.zoomControlOptions !== 'undefined'; + options.mapTypeControl = typeof options.mapTypeControlOptions !== 'undefined'; + options.streetViewControl = typeof options.streetViewControlOptions !== 'undefined'; + options.fullscreenControl = typeof options.fullscreenControlOptions !== 'undefined'; + return new library._Map(this.element, { + ...options, + center, + zoom, + }); + } + doCreateMarker(definition) { + const { position, title, infoWindow, rawOptions = {}, ...otherOptions } = definition; + const marker = new library.AdvancedMarkerElement({ + position, + title, + ...otherOptions, + ...rawOptions, + map: this.map, + }); + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, marker }); + } + return marker; + } + doCreateInfoWindow({ definition, marker, }) { + const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; + const infoWindow = new library.InfoWindow({ + headerContent: this.createTextOrElement(headerContent), + content: this.createTextOrElement(content), + ...otherOptions, + ...rawOptions, + }); + if (definition.opened) { + infoWindow.open({ + map: this.map, + shouldFocus: false, + anchor: marker, + }); + } + marker.addListener('click', () => { + if (definition.autoClose) { + this.closeInfoWindowsExcept(infoWindow); + } + infoWindow.open({ + map: this.map, + anchor: marker, + }); + }); + return infoWindow; + } + createTextOrElement(content) { + if (!content) { + return null; + } + if (content.includes('<')) { + const div = document.createElement('div'); + div.innerHTML = content; + return div; + } + return content; + } + closeInfoWindowsExcept(infoWindow) { + this.infoWindows.forEach((otherInfoWindow) => { + if (otherInfoWindow !== infoWindow) { + otherInfoWindow.close(); + } + }); + } + doFitBoundsToMarkers() { + if (this.markers.length === 0) { + return; + } + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + bounds.extend(marker.position); + }); + this.map.fitBounds(bounds); + } +} +default_1.values = { + providerOptions: Object, +}; + +export { default_1 as default }; diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..b189b4f --- /dev/null +++ b/assets/package.json @@ -0,0 +1,39 @@ +{ + "name": "@symfony/ux-map-google", + "description": "GoogleMaps bridge for Symfony UX Map, integrate interactive maps in your Symfony applications", + "license": "MIT", + "version": "1.0.0", + "type": "module", + "main": "dist/map_controller.js", + "types": "dist/map_controller.d.ts", + "symfony": { + "controllers": { + "map": { + "main": "dist/map_controller.js", + "webpackMode": "lazy", + "fetch": "lazy", + "enabled": true + } + }, + "importmap": { + "@hotwired/stimulus": "^3.0.0", + "@googlemaps/js-api-loader": "^1.16.6", + "@symfony/ux-map-google/map-controller": "path:%PACKAGE%/dist/map_controller.js" + } + }, + "peerDependencies": { + "@googlemaps/js-api-loader": "^1.16.6", + "@hotwired/stimulus": "^3.0.0" + }, + "peerDependenciesMeta": { + "@googlemaps/js-api-loader": { + "optional": false + } + }, + "devDependencies": { + "@googlemaps/js-api-loader": "^1.16.6", + "@hotwired/stimulus": "^3.0.0", + "@types/google.maps": "^3.55.9", + "happy-dom": "^14.12.3" + } +} diff --git a/assets/src/map_controller.ts b/assets/src/map_controller.ts new file mode 100644 index 0000000..a0def42 --- /dev/null +++ b/assets/src/map_controller.ts @@ -0,0 +1,187 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import AbstractMapController from '@symfony/ux-map/abstract-map-controller'; +import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller'; +import type { LoaderOptions } from '@googlemaps/js-api-loader'; +import { Loader } from '@googlemaps/js-api-loader'; + +type MapOptions = Pick< + google.maps.MapOptions, + | 'mapId' + | 'gestureHandling' + | 'backgroundColor' + | 'disableDoubleClickZoom' + | 'zoomControl' + | 'zoomControlOptions' + | 'mapTypeControl' + | 'mapTypeControlOptions' + | 'streetViewControl' + | 'streetViewControlOptions' + | 'fullscreenControl' + | 'fullscreenControlOptions' +>; + +let loader: Loader; +let library: { + _Map: typeof google.maps.Map; + AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement; + InfoWindow: typeof google.maps.InfoWindow; +}; + +export default class extends AbstractMapController< + MapOptions, + google.maps.Map, + google.maps.marker.AdvancedMarkerElement, + google.maps.InfoWindow +> { + static values = { + providerOptions: Object, + }; + + declare providerOptionsValue: Pick< + LoaderOptions, + 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' + >; + + async connect() { + if (!loader) { + loader = new Loader(this.providerOptionsValue); + } + + const { Map: _Map, InfoWindow } = await loader.importLibrary('maps'); + const { AdvancedMarkerElement } = await loader.importLibrary('marker'); + library = { _Map, AdvancedMarkerElement, InfoWindow }; + + super.connect(); + } + + protected doCreateMap({ + center, + zoom, + options, + }: { + center: Point; + zoom: number; + options: MapOptions; + }): google.maps.Map { + // We assume the following control options are enabled if their options are set + options.zoomControl = typeof options.zoomControlOptions !== 'undefined'; + options.mapTypeControl = typeof options.mapTypeControlOptions !== 'undefined'; + options.streetViewControl = typeof options.streetViewControlOptions !== 'undefined'; + options.fullscreenControl = typeof options.fullscreenControlOptions !== 'undefined'; + + return new library._Map(this.element, { + ...options, + center, + zoom, + }); + } + + protected doCreateMarker( + definition: MarkerDefinition + ): google.maps.marker.AdvancedMarkerElement { + const { position, title, infoWindow, rawOptions = {}, ...otherOptions } = definition; + + const marker = new library.AdvancedMarkerElement({ + position, + title, + ...otherOptions, + ...rawOptions, + map: this.map, + }); + + if (infoWindow) { + this.createInfoWindow({ definition: infoWindow, marker }); + } + + return marker; + } + + protected doCreateInfoWindow({ + definition, + marker, + }: { + definition: MarkerDefinition< + google.maps.marker.AdvancedMarkerElementOptions, + google.maps.InfoWindowOptions + >['infoWindow']; + marker: google.maps.marker.AdvancedMarkerElement; + }): google.maps.InfoWindow { + const { headerContent, content, rawOptions = {}, ...otherOptions } = definition; + + const infoWindow = new library.InfoWindow({ + headerContent: this.createTextOrElement(headerContent), + content: this.createTextOrElement(content), + ...otherOptions, + ...rawOptions, + }); + + if (definition.opened) { + infoWindow.open({ + map: this.map, + shouldFocus: false, + anchor: marker, + }); + } + + marker.addListener('click', () => { + if (definition.autoClose) { + this.closeInfoWindowsExcept(infoWindow); + } + + infoWindow.open({ + map: this.map, + anchor: marker, + }); + }); + + return infoWindow; + } + + private createTextOrElement(content: string | null): string | HTMLElement | null { + if (!content) { + return null; + } + + // we assume it's HTML if it includes "<" + if (content.includes('<')) { + const div = document.createElement('div'); + div.innerHTML = content; + return div; + } + + return content; + } + + private closeInfoWindowsExcept(infoWindow: google.maps.InfoWindow) { + this.infoWindows.forEach((otherInfoWindow) => { + if (otherInfoWindow !== infoWindow) { + otherInfoWindow.close(); + } + }); + } + + protected doFitBoundsToMarkers(): void { + if (this.markers.length === 0) { + return; + } + + const bounds = new google.maps.LatLngBounds(); + this.markers.forEach((marker) => { + if (!marker.position) { + return; + } + + bounds.extend(marker.position); + }); + + this.map.fitBounds(bounds); + } +} diff --git a/assets/test/map_controller.test.ts b/assets/test/map_controller.test.ts new file mode 100644 index 0000000..ebaf637 --- /dev/null +++ b/assets/test/map_controller.test.ts @@ -0,0 +1,62 @@ +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Application, Controller } from '@hotwired/stimulus'; +import { getByTestId, waitFor } from '@testing-library/dom'; +import { clearDOM, mountDOM } from '@symfony/stimulus-testing'; +import GoogleController from '../src/map_controller'; + +// Controller used to check the actual controller was properly booted +class CheckController extends Controller { + connect() { + this.element.addEventListener('ux:map:pre-connect', (event) => { + this.element.classList.add('pre-connected'); + }); + + this.element.addEventListener('ux:map:connect', (event) => { + this.element.classList.add('connected'); + }); + } +} + +const startStimulus = () => { + const application = Application.start(); + application.register('check', CheckController); + application.register('google', GoogleController); +}; + +describe('GoogleMapsController', () => { + let container: HTMLElement; + + beforeEach(() => { + container = mountDOM(` +
+ `); + }); + + afterEach(() => { + clearDOM(); + }); + + it('connect', async () => { + const div = getByTestId(container, 'map'); + expect(div).not.toHaveClass('pre-connected'); + expect(div).not.toHaveClass('connected'); + + startStimulus(); + await waitFor(() => expect(div).toHaveClass('pre-connected')); + await waitFor(() => expect(div).toHaveClass('connected')); + }); +}); diff --git a/assets/vitest.config.js b/assets/vitest.config.js new file mode 100644 index 0000000..3892eef --- /dev/null +++ b/assets/vitest.config.js @@ -0,0 +1,17 @@ +import { defineConfig, mergeConfig } from 'vitest/config'; +import configShared from '../../../../../../vitest.config.js' + +export default mergeConfig( + configShared, + defineConfig({ + resolve: { + alias: { + '@symfony/ux-map/abstract-map-controller': __dirname + '/../../../../assets/src/abstract_map_controller.ts', + }, + }, + test: { + // We need a browser(-like) environment to run the tests + environment: 'happy-dom', + }, + }) +); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5e8ea16 --- /dev/null +++ b/composer.json @@ -0,0 +1,33 @@ +{ + "name": "symfony/ux-map-google", + "type": "symfony-ux-map-bridge", + "description": "Symfony UX Map GoogleMaps Bridge", + "keywords": ["google-maps", "map", "symfony", "ux"], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Hugo Alliaume", + "email": "hugo@alliau.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": ">=8.3", + "symfony/ux-map": "^2.19" + }, + "require-dev": { + "symfony/phpunit-bridge": "^6.4|^7.0" + }, + "autoload": { + "psr-4": { "Symfony\\UX\\Map\\Bridge\\Google\\": "src/" }, + "exclude-from-classmap": [] + }, + "autoload-dev": { + "psr-4": { "Symfony\\UX\\Map\\Bridge\\Google\\Tests\\": "tests/" } + }, + "minimum-stability": "dev" +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..1c3807e --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,26 @@ + + + + + + ./src + + + + + + + + + + + ./tests + + + diff --git a/src/GoogleOptions.php b/src/GoogleOptions.php new file mode 100644 index 0000000..8b26efc --- /dev/null +++ b/src/GoogleOptions.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google; + +use Symfony\UX\Map\Bridge\Google\Option\FullscreenControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\GestureHandling; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\StreetViewControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\ZoomControlOptions; +use Symfony\UX\Map\MapOptionsInterface; + +/** + * @author Hugo Alliaume + */ +final class GoogleOptions 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->zoomControl = true; + $this->zoomControlOptions = $zoomControlOptions; + + return $this; + } + + public function mapTypeControl(bool $enable = true): self + { + $this->mapTypeControl = $enable; + + return $this; + } + + public function mapTypeControlOptions(MapTypeControlOptions $mapTypeControlOptions): self + { + $this->mapTypeControl = true; + $this->mapTypeControlOptions = $mapTypeControlOptions; + + return $this; + } + + public function streetViewControl(bool $enable = true): self + { + $this->streetViewControl = $enable; + + return $this; + } + + public function streetViewControlOptions(StreetViewControlOptions $streetViewControlOptions): self + { + $this->streetViewControl = true; + $this->streetViewControlOptions = $streetViewControlOptions; + + return $this; + } + + public function fullscreenControl(bool $enable = true): self + { + $this->fullscreenControl = $enable; + + return $this; + } + + public function fullscreenControlOptions(FullscreenControlOptions $fullscreenControlOptions): self + { + $this->fullscreenControl = true; + $this->fullscreenControlOptions = $fullscreenControlOptions; + + return $this; + } + + public function toArray(): array + { + $array = [ + 'mapId' => $this->mapId, + 'gestureHandling' => $this->gestureHandling->value, + 'backgroundColor' => $this->backgroundColor, + 'disableDoubleClickZoom' => $this->disableDoubleClickZoom, + ]; + + if ($this->zoomControl) { + $array['zoomControlOptions'] = $this->zoomControlOptions->toArray(); + } + + if ($this->mapTypeControl) { + $array['mapTypeControlOptions'] = $this->mapTypeControlOptions->toArray(); + } + + if ($this->streetViewControl) { + $array['streetViewControlOptions'] = $this->streetViewControlOptions->toArray(); + } + + if ($this->fullscreenControl) { + $array['fullscreenControlOptions'] = $this->fullscreenControlOptions->toArray(); + } + + return $array; + } +} diff --git a/src/Option/ControlPosition.php b/src/Option/ControlPosition.php new file mode 100644 index 0000000..0ef209a --- /dev/null +++ b/src/Option/ControlPosition.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\Bridge\Google\Option; + +/** + * @see https://developers.google.com/maps/documentation/javascript/reference/control#ControlPosition + * + * @author Hugo Alliaume + */ +enum ControlPosition: int +{ + /** + * Equivalent to bottom-center in both LTR and RTL. + */ + case BLOCK_END_INLINE_CENTER = 24; + + /** + * Equivalent to bottom-right in LTR, or bottom-left in RTL. + */ + case BLOCK_END_INLINE_END = 25; + + /** + * Equivalent to bottom-left in LTR, or bottom-right in RTL. + */ + case BLOCK_END_INLINE_START = 23; + + /** + * Equivalent to top-center in both LTR and RTL. + */ + case BLOCK_START_INLINE_CENTER = 15; + + /** + * Equivalent to top-right in LTR, or top-left in RTL. + */ + case BLOCK_START_INLINE_END = 16; + + /** + * Equivalent to top-left in LTR, or top-right in RTL. + */ + case BLOCK_START_INLINE_START = 14; + + /** + * Equivalent to right-center in LTR, or left-center in RTL. + */ + case INLINE_END_BLOCK_CENTER = 21; + + /** + * Equivalent to right-bottom in LTR, or left-bottom in RTL. + */ + case INLINE_END_BLOCK_END = 22; + + /** + * Equivalent to right-top in LTR, or left-top in RTL. + */ + case INLINE_END_BLOCK_START = 20; + + /** + * Equivalent to left-center in LTR, or right-center in RTL. + */ + case INLINE_START_BLOCK_CENTER = 17; + + /** + * Equivalent to left-bottom in LTR, or right-bottom in RTL. + */ + case INLINE_START_BLOCK_END = 19; + + /** + * Equivalent to left-top in LTR, or right-top in RTL. + */ + case INLINE_START_BLOCK_START = 18; +} diff --git a/src/Option/FullscreenControlOptions.php b/src/Option/FullscreenControlOptions.php new file mode 100644 index 0000000..3525655 --- /dev/null +++ b/src/Option/FullscreenControlOptions.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\Bridge\Google\Option; + +/** + * Options for the rendering of the fullscreen control. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/control#FullscreenControlOptions + * + * @author Hugo Alliaume + */ +final readonly class FullscreenControlOptions +{ + public function __construct( + private ControlPosition $position = ControlPosition::INLINE_END_BLOCK_START, + ) { + } + + public function toArray(): array + { + return [ + 'position' => $this->position->value, + ]; + } +} diff --git a/src/Option/GestureHandling.php b/src/Option/GestureHandling.php new file mode 100644 index 0000000..ac8a354 --- /dev/null +++ b/src/Option/GestureHandling.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Option; + +/** + * This setting controls how the API handles gestures on the map. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.gestureHandling + * + * @author Hugo Alliaume + */ +enum GestureHandling: string +{ + /** + * Scroll events and one-finger touch gestures scroll the page, and do not zoom or pan the map. + * Two-finger touch gestures pan and zoom the map. + * Scroll events with a ctrl key or ⌘ key pressed zoom the map. + * In this mode the map cooperates with the page. + */ + case COOPERATIVE = 'cooperative'; + + /** + * All touch gestures and scroll events pan or zoom the map. + */ + case GREEDY = 'greedy'; + + /** + * The map cannot be panned or zoomed by user gestures. + */ + case NONE = 'none'; + + /** + * Gesture handling is either cooperative or greedy, depending on whether the page is scrollable or in an iframe. + */ + case AUTO = 'auto'; +} diff --git a/src/Option/MapTypeControlOptions.php b/src/Option/MapTypeControlOptions.php new file mode 100644 index 0000000..99e1fba --- /dev/null +++ b/src/Option/MapTypeControlOptions.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\Bridge\Google\Option; + +/** + * Options for the rendering of the map type control. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlOptions + * + * @author Hugo Alliaume + */ +final readonly class MapTypeControlOptions +{ + /** + * @param array<'hybrid'|'roadmap'|'satellite'|'terrain'|string> $mapTypeIds + */ + public function __construct( + private array $mapTypeIds = [], + private ControlPosition $position = ControlPosition::BLOCK_START_INLINE_START, + private MapTypeControlStyle $style = MapTypeControlStyle::DEFAULT, + ) { + } + + public function toArray(): array + { + return [ + 'mapTypeIds' => $this->mapTypeIds, + 'position' => $this->position->value, + 'style' => $this->style->value, + ]; + } +} diff --git a/src/Option/MapTypeControlStyle.php b/src/Option/MapTypeControlStyle.php new file mode 100644 index 0000000..5242ce4 --- /dev/null +++ b/src/Option/MapTypeControlStyle.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\Bridge\Google\Option; + +/** + * Identifiers for common MapTypesControls. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlStyle + * + * @author Hugo Alliaume + */ +enum MapTypeControlStyle: int +{ + /** + * Uses the default map type control. When the DEFAULT control is shown, it will vary according to window size and other factors. + * The DEFAULT control may change in future versions of the API. + */ + case DEFAULT = 0; + + /** + * A dropdown menu for the screen realestate conscious. + */ + case DROPDOWN_MENU = 2; + + /** + * The standard horizontal radio buttons bar. + */ + case HORIZONTAL_BAR = 1; +} diff --git a/src/Option/StreetViewControlOptions.php b/src/Option/StreetViewControlOptions.php new file mode 100644 index 0000000..926b883 --- /dev/null +++ b/src/Option/StreetViewControlOptions.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\Bridge\Google\Option; + +/** + * Options for the rendering of the Street View pegman control on the map. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/control#StreetViewControlOptions + * + * @author Hugo Alliaume + */ +final readonly class StreetViewControlOptions +{ + public function __construct( + private ControlPosition $position = ControlPosition::INLINE_END_BLOCK_END, + ) { + } + + public function toArray(): array + { + return [ + 'position' => $this->position->value, + ]; + } +} diff --git a/src/Option/ZoomControlOptions.php b/src/Option/ZoomControlOptions.php new file mode 100644 index 0000000..979947a --- /dev/null +++ b/src/Option/ZoomControlOptions.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\Bridge\Google\Option; + +/** + * Options for the rendering of the zoom control. + * + * @see https://developers.google.com/maps/documentation/javascript/reference/control#ZoomControlOptions + * + * @author Hugo Alliaume + */ +final readonly class ZoomControlOptions +{ + public function __construct( + private ControlPosition $position = ControlPosition::INLINE_END_BLOCK_END, + ) { + } + + public function toArray(): array + { + return [ + 'position' => $this->position->value, + ]; + } +} diff --git a/src/Renderer/GoogleRenderer.php b/src/Renderer/GoogleRenderer.php new file mode 100644 index 0000000..fb21467 --- /dev/null +++ b/src/Renderer/GoogleRenderer.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Renderer; + +use Symfony\UX\Map\Bridge\Google\GoogleOptions; +use Symfony\UX\Map\MapOptionsInterface; +use Symfony\UX\Map\Renderer\AbstractRenderer; +use Symfony\UX\StimulusBundle\Helper\StimulusHelper; + +/** + * @author Hugo Alliaume + * + * @internal + */ +final readonly class GoogleRenderer extends AbstractRenderer +{ + /** + * Parameters are based from https://googlemaps.github.io/js-api-loader/interfaces/LoaderOptions.html documentation. + */ + public function __construct( + StimulusHelper $stimulusHelper, + #[\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, + ) { + parent::__construct($stimulusHelper); + } + + protected function getName(): string + { + return 'google'; + } + + protected function getProviderOptions(): 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]; + } + + protected function getDefaultMapOptions(): MapOptionsInterface + { + return new GoogleOptions(); + } + + public function __toString(): string + { + return \sprintf( + 'google://%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/Renderer/GoogleRendererFactory.php b/src/Renderer/GoogleRendererFactory.php new file mode 100644 index 0000000..04a3992 --- /dev/null +++ b/src/Renderer/GoogleRendererFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Renderer; + +use Symfony\UX\Map\Exception\InvalidArgumentException; +use Symfony\UX\Map\Exception\UnsupportedSchemeException; +use Symfony\UX\Map\Renderer\AbstractRendererFactory; +use Symfony\UX\Map\Renderer\Dsn; +use Symfony\UX\Map\Renderer\RendererFactoryInterface; +use Symfony\UX\Map\Renderer\RendererInterface; + +/** + * @author Hugo Alliaume + */ +final class GoogleRendererFactory extends AbstractRendererFactory implements RendererFactoryInterface +{ + public function create(Dsn $dsn): RendererInterface + { + if (!$this->supports($dsn)) { + throw new UnsupportedSchemeException($dsn); + } + + $apiKey = $dsn->getUser() ?: throw new InvalidArgumentException('The Google Maps renderer requires an API key as the user part of the DSN.'); + + return new GoogleRenderer( + $this->stimulus, + $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']; + } +} diff --git a/tests/GoogleOptionsTest.php b/tests/GoogleOptionsTest.php new file mode 100644 index 0000000..ccde8a7 --- /dev/null +++ b/tests/GoogleOptionsTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\GoogleOptions; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\GestureHandling; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlStyle; + +class GoogleOptionsTest extends TestCase +{ + public function testWithMinimalConfiguration(): void + { + $options = new GoogleOptions(); + + self::assertSame([ + 'mapId' => null, + 'gestureHandling' => 'auto', + 'backgroundColor' => null, + 'disableDoubleClickZoom' => false, + 'zoomControlOptions' => [ + 'position' => ControlPosition::INLINE_END_BLOCK_END->value, + ], + 'mapTypeControlOptions' => [ + 'mapTypeIds' => [], + 'position' => ControlPosition::BLOCK_START_INLINE_START->value, + 'style' => MapTypeControlStyle::DEFAULT->value, + ], + 'streetViewControlOptions' => [ + 'position' => ControlPosition::INLINE_END_BLOCK_END->value, + ], + 'fullscreenControlOptions' => [ + 'position' => ControlPosition::INLINE_END_BLOCK_START->value, + ], + ], $options->toArray()); + } + + public function testWithMinimalConfigurationAndWithoutControls(): void + { + $options = new GoogleOptions( + mapId: '2b2d73ba4b8c7b41', + gestureHandling: GestureHandling::GREEDY, + backgroundColor: '#f00', + disableDoubleClickZoom: true, + zoomControl: false, + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + ); + + self::assertSame([ + 'mapId' => '2b2d73ba4b8c7b41', + 'gestureHandling' => GestureHandling::GREEDY->value, + 'backgroundColor' => '#f00', + 'disableDoubleClickZoom' => true, + ], $options->toArray()); + } +} diff --git a/tests/GoogleRendererFactoryTest.php b/tests/GoogleRendererFactoryTest.php new file mode 100644 index 0000000..9854224 --- /dev/null +++ b/tests/GoogleRendererFactoryTest.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests; + +use Symfony\UX\Map\Bridge\Google\Renderer\GoogleRendererFactory; +use Symfony\UX\Map\Renderer\RendererFactoryInterface; +use Symfony\UX\Map\Test\RendererFactoryTestCase; +use Symfony\UX\StimulusBundle\Helper\StimulusHelper; + +final class GoogleRendererFactoryTest extends RendererFactoryTestCase +{ + public function createRendererFactory(): RendererFactoryInterface + { + return new GoogleRendererFactory(new StimulusHelper(null)); + } + + public static function supportsRenderer(): iterable + { + yield [true, 'google://GOOGLE_MAPS_API_KEY@default']; + yield [false, 'somethingElse://login:apiKey@default']; + } + + public static function createRenderer(): iterable + { + yield [ + 'google://*******************@default/?version=weekly', + 'google://GOOGLE_MAPS_API_KEY@default', + ]; + + yield [ + 'google://*******************@default/?version=quartly', + 'google://GOOGLE_MAPS_API_KEY@default?version=quartly', + ]; + } + + public static function unsupportedSchemeRenderer(): iterable + { + yield ['somethingElse://foo@default']; + } +} diff --git a/tests/GoogleRendererTest.php b/tests/GoogleRendererTest.php new file mode 100644 index 0000000..7536992 --- /dev/null +++ b/tests/GoogleRendererTest.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests; + +use Symfony\UX\Map\Bridge\Google\GoogleOptions; +use Symfony\UX\Map\Bridge\Google\Renderer\GoogleRenderer; +use Symfony\UX\Map\InfoWindow; +use Symfony\UX\Map\Map; +use Symfony\UX\Map\Marker; +use Symfony\UX\Map\Point; +use Symfony\UX\Map\Test\RendererTestCase; +use Symfony\UX\StimulusBundle\Helper\StimulusHelper; + +class GoogleRendererTest extends RendererTestCase +{ + public function provideTestRenderMap(): iterable + { + $map = (new Map()) + ->center(new Point(48.8566, 2.3522)) + ->zoom(12); + + yield 'simple map, with minimum options' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), + 'map' => $map, + ]; + + yield 'with every options' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'quarterly'), + 'map' => $map, + ]; + + yield 'with markers and infoWindows' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), + 'map' => (clone $map) + ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris')) + ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Lyon', infoWindow: new InfoWindow(content: 'Lyon'))), + ]; + + yield 'with controls enabled' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), + 'map' => (clone $map) + ->options(new GoogleOptions( + zoomControl: true, + mapTypeControl: true, + streetViewControl: true, + fullscreenControl: true, + )), + ]; + + yield 'without controls enabled' => [ + 'expected_render' => '
', + 'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'), + 'map' => (clone $map) + ->options(new GoogleOptions( + zoomControl: false, + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + )), + ]; + } +} diff --git a/tests/Option/ControlPositionTest.php b/tests/Option/ControlPositionTest.php new file mode 100644 index 0000000..71ef62e --- /dev/null +++ b/tests/Option/ControlPositionTest.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\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; + +class ControlPositionTest extends TestCase +{ + public function testEnumValues(): void + { + self::assertSame(24, ControlPosition::BLOCK_END_INLINE_CENTER->value); + self::assertSame(25, ControlPosition::BLOCK_END_INLINE_END->value); + self::assertSame(23, ControlPosition::BLOCK_END_INLINE_START->value); + self::assertSame(15, ControlPosition::BLOCK_START_INLINE_CENTER->value); + self::assertSame(16, ControlPosition::BLOCK_START_INLINE_END->value); + self::assertSame(14, ControlPosition::BLOCK_START_INLINE_START->value); + self::assertSame(21, ControlPosition::INLINE_END_BLOCK_CENTER->value); + self::assertSame(22, ControlPosition::INLINE_END_BLOCK_END->value); + self::assertSame(20, ControlPosition::INLINE_END_BLOCK_START->value); + self::assertSame(17, ControlPosition::INLINE_START_BLOCK_CENTER->value); + self::assertSame(19, ControlPosition::INLINE_START_BLOCK_END->value); + self::assertSame(18, ControlPosition::INLINE_START_BLOCK_START->value); + } +} diff --git a/tests/Option/FullscreenControlOptionsTest.php b/tests/Option/FullscreenControlOptionsTest.php new file mode 100644 index 0000000..3de7164 --- /dev/null +++ b/tests/Option/FullscreenControlOptionsTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\FullscreenControlOptions; + +class FullscreenControlOptionsTest extends TestCase +{ + public function testToArray(): void + { + $options = new FullscreenControlOptions( + position: ControlPosition::BLOCK_END_INLINE_CENTER + ); + + self::assertSame([ + 'position' => ControlPosition::BLOCK_END_INLINE_CENTER->value, + ], $options->toArray()); + } +} diff --git a/tests/Option/GestureHandlingTest.php b/tests/Option/GestureHandlingTest.php new file mode 100644 index 0000000..c6a1b6b --- /dev/null +++ b/tests/Option/GestureHandlingTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\GestureHandling; + +class GestureHandlingTest extends TestCase +{ + public function testEnumValues(): void + { + self::assertSame('cooperative', GestureHandling::COOPERATIVE->value); + self::assertSame('greedy', GestureHandling::GREEDY->value); + self::assertSame('none', GestureHandling::NONE->value); + self::assertSame('auto', GestureHandling::AUTO->value); + } +} diff --git a/tests/Option/MapTypeControlOptionsTest.php b/tests/Option/MapTypeControlOptionsTest.php new file mode 100644 index 0000000..fbc5ea1 --- /dev/null +++ b/tests/Option/MapTypeControlOptionsTest.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\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlOptions; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlStyle; + +class MapTypeControlOptionsTest extends TestCase +{ + public function testToArray(): void + { + $options = new MapTypeControlOptions( + mapTypeIds: ['satellite', 'hybrid'], + position: ControlPosition::BLOCK_END_INLINE_END, + style: MapTypeControlStyle::HORIZONTAL_BAR, + ); + + self::assertSame([ + 'mapTypeIds' => ['satellite', 'hybrid'], + 'position' => ControlPosition::BLOCK_END_INLINE_END->value, + 'style' => MapTypeControlStyle::HORIZONTAL_BAR->value, + ], $options->toArray()); + } +} diff --git a/tests/Option/MapTypeControlStyleTest.php b/tests/Option/MapTypeControlStyleTest.php new file mode 100644 index 0000000..43818e9 --- /dev/null +++ b/tests/Option/MapTypeControlStyleTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\MapTypeControlStyle; + +class MapTypeControlStyleTest extends TestCase +{ + public function testEnumValues(): void + { + self::assertSame(0, MapTypeControlStyle::DEFAULT->value); + self::assertSame(2, MapTypeControlStyle::DROPDOWN_MENU->value); + self::assertSame(1, MapTypeControlStyle::HORIZONTAL_BAR->value); + } +} diff --git a/tests/Option/StreetViewControlOptionsTest.php b/tests/Option/StreetViewControlOptionsTest.php new file mode 100644 index 0000000..5cf0742 --- /dev/null +++ b/tests/Option/StreetViewControlOptionsTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\StreetViewControlOptions; + +class StreetViewControlOptionsTest extends TestCase +{ + public function testToArray(): void + { + $options = new StreetViewControlOptions( + position: ControlPosition::INLINE_END_BLOCK_CENTER + ); + + self::assertSame([ + 'position' => ControlPosition::INLINE_END_BLOCK_CENTER->value, + ], $options->toArray()); + } +} diff --git a/tests/Option/ZoomControlOptionsTest.php b/tests/Option/ZoomControlOptionsTest.php new file mode 100644 index 0000000..929c960 --- /dev/null +++ b/tests/Option/ZoomControlOptionsTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\Map\Bridge\Google\Tests\Option; + +use PHPUnit\Framework\TestCase; +use Symfony\UX\Map\Bridge\Google\Option\ControlPosition; +use Symfony\UX\Map\Bridge\Google\Option\ZoomControlOptions; + +class ZoomControlOptionsTest extends TestCase +{ + public function testToArray(): void + { + $options = new ZoomControlOptions( + position: ControlPosition::BLOCK_START_INLINE_END, + ); + + self::assertSame([ + 'position' => ControlPosition::BLOCK_START_INLINE_END->value, + ], $options->toArray()); + } +}