[Map] Create Map component

This commit is contained in:
Hugo Alliaume
2024-06-10 23:06:54 +02:00
committed by Javier Eguiluz
commit feaa3c5f6a
21 changed files with 795 additions and 0 deletions

4
.gitattributes vendored Normal file
View File

@@ -0,0 +1,4 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# CHANGELOG
## Unreleased
- Bridge added

19
LICENSE Normal file
View File

@@ -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.

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
# Symfony UX Map: Leaflet
[Leaflet](https://leafletjs.com/) integration for Symfony UX Map.
## DSN example
```dotenv
UX_MAP_DSN=leaflet://default
```
## Map options
You can use the `LeafletOptions` class to configure your `Map`::
```php
use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
use Symfony\UX\Map\Point;
use Symfony\UX\Map\Map;
$map = (new Map())
->center(new Point(48.8566, 2.3522))
->zoom(6);
$leafletOptions = (new LeafletOptions())
->tileLayer(new TileLayer(
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
options: [
'minZoom' => 5,
'maxZoom' => 10,
]
))
;
// Add the custom options to the map
$map->options($leafletOptions);
```
## 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)

27
assets/dist/map_controller.d.ts vendored Normal file
View File

@@ -0,0 +1,27 @@
import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
import 'leaflet/dist/leaflet.min.css';
import { type Map as LeafletMap, Marker, type Popup } from 'leaflet';
import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
tileLayer: {
url: string;
attribution: string;
options: Record<string, unknown>;
};
};
export default class extends AbstractMapController<MapOptions, typeof LeafletMap, MarkerOptions, Marker, Popup, PopupOptions> {
connect(): void;
protected doCreateMap({ center, zoom, options }: {
center: Point;
zoom: number;
options: MapOptions;
}): LeafletMap;
protected doCreateMarker(definition: MarkerDefinition): Marker;
protected doCreateInfoWindow({ definition, marker, }: {
definition: MarkerDefinition['infoWindow'];
marker: Marker;
}): Popup;
protected doFitBoundsToMarkers(): void;
}
export {};

59
assets/dist/map_controller.js vendored Normal file
View File

@@ -0,0 +1,59 @@
import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
import 'leaflet/dist/leaflet.min.css';
import { Marker, divIcon, map, tileLayer, marker } from 'leaflet';
class map_controller extends AbstractMapController {
connect() {
Marker.prototype.options.icon = divIcon({
html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round" viewBox="0 0 500 820"><defs><linearGradient id="a" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-90 478.727 62.272) scale(37.566)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#126FC6"/><stop offset="1" stop-color="#4C9CD1"/></linearGradient><linearGradient id="b" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-90 468.484 54.002) scale(19.053)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2E6C97"/><stop offset="1" stop-color="#3883B7"/></linearGradient></defs><path fill="url(#a)" stroke="url(#b)" stroke-width="1.1" d="M416.544 503.612c-6.573 0-12.044 5.691-12.044 11.866 0 2.778 1.564 6.308 2.694 8.746l9.306 17.872 9.262-17.872c1.13-2.438 2.738-5.791 2.738-8.746 0-6.175-5.383-11.866-11.956-11.866Zm0 7.155a4.714 4.714 0 0 1 4.679 4.71c0 2.588-2.095 4.663-4.679 4.679-2.584-.017-4.679-2.09-4.679-4.679a4.714 4.714 0 0 1 4.679-4.71Z" transform="translate(-7889.1 -9807.44) scale(19.5417)"/></svg>',
iconSize: [25, 41],
iconAnchor: [12.5, 41],
popupAnchor: [0, -41],
className: '',
});
super.connect();
}
doCreateMap({ center, zoom, options }) {
const map$1 = map(this.element, {
...options,
center,
zoom,
});
tileLayer(options.tileLayer.url, {
attribution: options.tileLayer.attribution,
...options.tileLayer.options,
}).addTo(map$1);
return map$1;
}
doCreateMarker(definition) {
const { position, title, infoWindow, rawOptions = {}, ...otherOptions } = definition;
const marker$1 = marker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, marker: marker$1 });
}
return marker$1;
}
doCreateInfoWindow({ definition, marker, }) {
const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
marker.bindPopup(`${headerContent}<br>${content}`, { ...otherOptions, ...rawOptions });
if (definition.opened) {
marker.openPopup();
}
const popup = marker.getPopup();
if (!popup) {
throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
}
return popup;
}
doFitBoundsToMarkers() {
if (this.markers.length === 0) {
return;
}
this.map.fitBounds(this.markers.map((marker) => {
const position = marker.getLatLng();
return [position.lat, position.lng];
}));
}
}
export { map_controller as default };

39
assets/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "@symfony/ux-map-leaflet",
"description": "Leaflet 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",
"leaflet": "^1.9.4",
"@symfony/ux-map-leaflet/map-controller": "path:%PACKAGE%/dist/map_controller.js"
}
},
"peerDependencies": {
"@hotwired/stimulus": "^3.0.0",
"leaflet": "^1.9.4"
},
"peerDependenciesMeta": {
"leaflet": {
"optional": false
}
},
"devDependencies": {
"@hotwired/stimulus": "^3.0.0",
"@types/leaflet": "^1.9.12",
"happy-dom": "^14.12.3",
"leaflet": "^1.9.4"
}
}

View File

@@ -0,0 +1,99 @@
import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
import 'leaflet/dist/leaflet.min.css';
import {
map as createMap,
tileLayer as createTileLayer,
marker as createMarker,
divIcon,
type Map as LeafletMap,
Marker,
type Popup,
} from 'leaflet';
import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
tileLayer: { url: string; attribution: string; options: Record<string, unknown> };
};
export default class extends AbstractMapController<
MapOptions,
typeof LeafletMap,
MarkerOptions,
Marker,
Popup,
PopupOptions
> {
connect(): void {
Marker.prototype.options.icon = divIcon({
html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round" viewBox="0 0 500 820"><defs><linearGradient id="a" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-90 478.727 62.272) scale(37.566)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#126FC6"/><stop offset="1" stop-color="#4C9CD1"/></linearGradient><linearGradient id="b" x1="0" x2="1" y1="0" y2="0" gradientTransform="rotate(-90 468.484 54.002) scale(19.053)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2E6C97"/><stop offset="1" stop-color="#3883B7"/></linearGradient></defs><path fill="url(#a)" stroke="url(#b)" stroke-width="1.1" d="M416.544 503.612c-6.573 0-12.044 5.691-12.044 11.866 0 2.778 1.564 6.308 2.694 8.746l9.306 17.872 9.262-17.872c1.13-2.438 2.738-5.791 2.738-8.746 0-6.175-5.383-11.866-11.956-11.866Zm0 7.155a4.714 4.714 0 0 1 4.679 4.71c0 2.588-2.095 4.663-4.679 4.679-2.584-.017-4.679-2.09-4.679-4.679a4.714 4.714 0 0 1 4.679-4.71Z" transform="translate(-7889.1 -9807.44) scale(19.5417)"/></svg>',
iconSize: [25, 41],
iconAnchor: [12.5, 41],
popupAnchor: [0, -41],
className: '',
});
super.connect();
}
protected doCreateMap({ center, zoom, options }: { center: Point; zoom: number; options: MapOptions }): LeafletMap {
const map = createMap(this.element, {
...options,
center,
zoom,
});
createTileLayer(options.tileLayer.url, {
attribution: options.tileLayer.attribution,
...options.tileLayer.options,
}).addTo(map);
return map;
}
protected doCreateMarker(definition: MarkerDefinition): Marker {
const { position, title, infoWindow, rawOptions = {}, ...otherOptions } = definition;
const marker = createMarker(position, { title, ...otherOptions, ...rawOptions }).addTo(this.map);
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, marker });
}
return marker;
}
protected doCreateInfoWindow({
definition,
marker,
}: {
definition: MarkerDefinition['infoWindow'];
marker: Marker;
}): Popup {
const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
marker.bindPopup(`${headerContent}<br>${content}`, { ...otherOptions, ...rawOptions });
if (definition.opened) {
marker.openPopup();
}
const popup = marker.getPopup();
if (!popup) {
throw new Error('Unable to get the Popup associated to the Marker, this should not happens.');
}
return popup;
}
protected doFitBoundsToMarkers(): void {
if (this.markers.length === 0) {
return;
}
this.map.fitBounds(
this.markers.map((marker: Marker) => {
const position = marker.getLatLng();
return [position.lat, position.lng];
})
);
}
}

View File

@@ -0,0 +1,62 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 LeafletController 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('leaflet', LeafletController);
};
describe('LeafletController', () => {
let container: HTMLElement;
beforeEach(() => {
container = mountDOM(`
<div
data-testid="map"
data-controller="check leaflet"
style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px"
data-leaflet-provider-options-value="&#x7B;&#x7D;"
data-leaflet-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
></div>
`);
});
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'));
});
});

18
assets/vitest.config.js Normal file
View File

@@ -0,0 +1,18 @@
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',
'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
},
},
test: {
// We need a browser(-like) environment to run the tests
environment: 'happy-dom',
},
})
);

33
composer.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "symfony/ux-map-leaflet",
"type": "symfony-ux-map-bridge",
"description": "Symfony UX Map Leaflet Bridge",
"keywords": ["leaflet", "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\\Leaflet\\": "src/" },
"exclude-from-classmap": []
},
"autoload-dev": {
"psr-4": { "Symfony\\UX\\Map\\Bridge\\Leaflet\\Tests\\": "tests/" }
},
"minimum-stability": "dev"
}

26
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/bin/.phpunit/phpunit.xsd"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0"/>
</php>
<testsuites>
<testsuite name="Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>
</phpunit>

43
src/LeafletOptions.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet;
use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
use Symfony\UX\Map\MapOptionsInterface;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*/
final class LeafletOptions implements MapOptionsInterface
{
public function __construct(
private TileLayer $tileLayer = new TileLayer(
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
),
) {
}
public function tileLayer(TileLayer $tileLayer): self
{
$this->tileLayer = $tileLayer;
return $this;
}
public function toArray(): array
{
return [
'tileLayer' => $this->tileLayer->toArray(),
];
}
}

41
src/Option/TileLayer.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Option;
/**
* Represents a tile layer for a Leaflet map.
*
* @see https://leafletjs.com/reference.html#tilelayer
*
* @author Hugo Alliaume <hugo@alliau.me>
*/
final readonly class TileLayer
{
/**
* @param array<mixed> $options
*/
public function __construct(
private string $url,
private string $attribution,
private array $options = [],
) {
}
public function toArray(): array
{
return [
'url' => $this->url,
'attribution' => $this->attribution,
'options' => (object) $this->options,
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Renderer;
use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
use Symfony\UX\Map\MapOptionsInterface;
use Symfony\UX\Map\Renderer\AbstractRenderer;
/**
* @author Hugo Alliaume <hugo@alliau.me>
*
* @internal
*/
final readonly class LeafletRenderer extends AbstractRenderer
{
protected function getName(): string
{
return 'leaflet';
}
protected function getProviderOptions(): array
{
return [];
}
protected function getDefaultMapOptions(): MapOptionsInterface
{
return new LeafletOptions();
}
public function __toString(): string
{
return 'leaflet://default';
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Renderer;
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 <hugo@alliau.me>
*/
final class LeafletRendererFactory extends AbstractRendererFactory implements RendererFactoryInterface
{
public function create(Dsn $dsn): RendererInterface
{
if (!$this->supports($dsn)) {
throw new UnsupportedSchemeException($dsn);
}
return new LeafletRenderer($this->stimulus);
}
protected function getSupportedSchemes(): array
{
return ['leaflet'];
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
class LeafletOptionsTest extends TestCase
{
public function testWithMinimalConfiguration(): void
{
$leafletOptions = new LeafletOptions();
$array = $leafletOptions->toArray();
self::assertSame([
'tileLayer' => [
'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'attribution' => '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
'options' => $array['tileLayer']['options'], // stdClass
],
], $array);
}
public function testWithMaximumConfiguration(): void
{
$leafletOptions = new LeafletOptions(
tileLayer: new TileLayer(
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
options: [
'maxZoom' => 19,
'minZoom' => 1,
'maxNativeZoom' => 18,
'zoomOffset' => 0,
],
),
);
$array = $leafletOptions->toArray();
self::assertSame([
'tileLayer' => [
'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'attribution' => '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
'options' => $array['tileLayer']['options'], // stdClass
],
], $array);
self::assertSame(19, $array['tileLayer']['options']->maxZoom);
self::assertSame(1, $array['tileLayer']['options']->minZoom);
self::assertSame(18, $array['tileLayer']['options']->maxNativeZoom);
self::assertSame(0, $array['tileLayer']['options']->zoomOffset);
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Tests;
use Symfony\UX\Map\Bridge\Leaflet\Renderer\LeafletRendererFactory;
use Symfony\UX\Map\Renderer\RendererFactoryInterface;
use Symfony\UX\Map\Test\RendererFactoryTestCase;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
final class LeafletRendererFactoryTest extends RendererFactoryTestCase
{
public function createRendererFactory(): RendererFactoryInterface
{
return new LeafletRendererFactory(new StimulusHelper(null));
}
public static function supportsRenderer(): iterable
{
yield [true, 'leaflet://default'];
yield [false, 'foo://default'];
}
public static function createRenderer(): iterable
{
yield [
'leaflet://default',
'leaflet://default',
];
}
public static function unsupportedSchemeRenderer(): iterable
{
yield ['somethingElse://foo@default'];
}
}

View File

@@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Tests;
use Symfony\UX\Map\Bridge\Leaflet\Renderer\LeafletRenderer;
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 LeafletRendererTest extends RendererTestCase
{
public function provideTestRenderMap(): iterable
{
$map = (new Map())
->center(new Point(48.8566, 2.3522))
->zoom(12);
yield 'simple map' => [
'expected_render' => '<div data-controller="symfony--ux-map-leaflet--map" data-symfony--ux-map-leaflet--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-map-leaflet--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x5D;&#x7D;"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'map' => $map,
];
yield 'with markers and infoWindows' => [
'expected_render' => '<div data-controller="symfony--ux-map-leaflet--map" data-symfony--ux-map-leaflet--map-provider-options-value="&#x7B;&#x7D;" data-symfony--ux-map-leaflet--map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;12,&quot;fitBoundsToMarkers&quot;&#x3A;false,&quot;options&quot;&#x3A;&#x7B;&quot;tileLayer&quot;&#x3A;&#x7B;&quot;url&quot;&#x3A;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;tile.openstreetmap.org&#x5C;&#x2F;&#x7B;z&#x7D;&#x5C;&#x2F;&#x7B;x&#x7D;&#x5C;&#x2F;&#x7B;y&#x7D;.png&quot;,&quot;attribution&quot;&#x3A;&quot;&#x5C;u00a9&#x20;&lt;a&#x20;href&#x3D;&#x5C;&quot;https&#x3A;&#x5C;&#x2F;&#x5C;&#x2F;www.openstreetmap.org&#x5C;&#x2F;copyright&#x5C;&quot;&gt;OpenStreetMap&lt;&#x5C;&#x2F;a&gt;&quot;,&quot;options&quot;&#x3A;&#x7B;&#x7D;&#x7D;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;null,&quot;content&quot;&#x3A;&quot;Lyon&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"></div>',
'renderer' => new LeafletRenderer(new StimulusHelper(null)),
'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'))),
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Map\Bridge\Leaflet\Tests\Option;
use PHPUnit\Framework\TestCase;
use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
class TileLayerTest extends TestCase
{
public function testToArray()
{
$tileLayer = new TileLayer(
url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors',
options: [
'maxZoom' => 19,
],
);
$array = $tileLayer->toArray();
self::assertSame([
'url' => 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
'attribution' => '© OpenStreetMap contributors',
'options' => $array['options'], // stdClass
], $array);
self::assertSame(19, $array['options']->maxZoom);
}
}