mirror of
https://github.com/symfony/ux-leaflet-map.git
synced 2026-03-23 16:42:19 +01:00
[Map] Create Map component
This commit is contained in:
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/Tests export-ignore
|
||||
/phpunit.xml.dist export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
vendor/
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# CHANGELOG
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Bridge added
|
||||
19
LICENSE
Normal file
19
LICENSE
Normal 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
45
README.md
Normal 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
27
assets/dist/map_controller.d.ts
vendored
Normal 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
59
assets/dist/map_controller.js
vendored
Normal 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
39
assets/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
99
assets/src/map_controller.ts
Normal file
99
assets/src/map_controller.ts
Normal 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];
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
62
assets/test/map_controller.test.ts
Normal file
62
assets/test/map_controller.test.ts
Normal 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: 700px; margin: 10px"
|
||||
data-leaflet-provider-options-value="{}"
|
||||
data-leaflet-view-value="{"center":{"lat":48.8566,"lng":2.3522},"zoom":4,"fitBoundsToMarkers":true,"options":{"tileLayer":{"url":"https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png","attribution":"\u00a9 <a href=\"https:\/\/www.openstreetmap.org\/copyright\">OpenStreetMap<\/a>","options":{}}},"markers":[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null},{"position":{"lat":45.764,"lng":4.8357},"title":"Lyon","infoWindow":{"headerContent":"<b>Lyon<\/b>","content":"The French town in the historic Rh\u00f4ne-Alpes region, located at the junction of the Rh\u00f4ne and Sa\u00f4ne rivers.","position":null,"opened":false,"autoClose":true}}]}"
|
||||
></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
18
assets/vitest.config.js
Normal 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
33
composer.json
Normal 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
26
phpunit.xml.dist
Normal 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&max[direct]=0"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory>./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
43
src/LeafletOptions.php
Normal file
43
src/LeafletOptions.php
Normal 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
41
src/Option/TileLayer.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
44
src/Renderer/LeafletRenderer.php
Normal file
44
src/Renderer/LeafletRenderer.php
Normal 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';
|
||||
}
|
||||
}
|
||||
38
src/Renderer/LeafletRendererFactory.php
Normal file
38
src/Renderer/LeafletRendererFactory.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
64
tests/LeafletOptionsTest.php
Normal file
64
tests/LeafletOptionsTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
44
tests/LeafletRendererFactoryTest.php
Normal file
44
tests/LeafletRendererFactoryTest.php
Normal 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'];
|
||||
}
|
||||
}
|
||||
44
tests/LeafletRendererTest.php
Normal file
44
tests/LeafletRendererTest.php
Normal 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="{}" data-symfony--ux-map-leaflet--map-view-value="{"center":{"lat":48.8566,"lng":2.3522},"zoom":12,"fitBoundsToMarkers":false,"options":{"tileLayer":{"url":"https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png","attribution":"\u00a9 <a href=\"https:\/\/www.openstreetmap.org\/copyright\">OpenStreetMap<\/a>","options":{}}},"markers":[]}"></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="{}" data-symfony--ux-map-leaflet--map-view-value="{"center":{"lat":48.8566,"lng":2.3522},"zoom":12,"fitBoundsToMarkers":false,"options":{"tileLayer":{"url":"https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png","attribution":"\u00a9 <a href=\"https:\/\/www.openstreetmap.org\/copyright\">OpenStreetMap<\/a>","options":{}}},"markers":[{"position":{"lat":48.8566,"lng":2.3522},"title":"Paris","infoWindow":null},{"position":{"lat":48.8566,"lng":2.3522},"title":"Lyon","infoWindow":{"headerContent":null,"content":"Lyon","position":null,"opened":false,"autoClose":true}}]}"></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'))),
|
||||
];
|
||||
}
|
||||
}
|
||||
38
tests/Option/TileLayerTest.php
Normal file
38
tests/Option/TileLayerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user