├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── LICENSE
├── README.md
├── dist
│ ├── map_controller.d.ts
│ └── map_controller.js
├── package.json
└── vitest.config.mjs
├── composer.json
└── src
├── LeafletOptions.php
├── Option
└── TileLayer.php
└── Renderer
├── LeafletRenderer.php
└── LeafletRendererFactory.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # CHANGELOG
2 |
3 | ## 2.26
4 |
5 | - Using `new LeafletOptions(tileLayer: false)` will now disable the default `TileLayer`.
6 | Useful when using a custom tiles layer rendering engine not configurable with `L.tileLayer().addTo(map)` method
7 | (e.g.: [Esri/esri-leaflet-vector](https://github.com/Esri/esri-leaflet-vector))
8 |
9 | ## 2.25
10 |
11 | - Downgrade PHP requirement from 8.3 to 8.1
12 |
13 | ## 2.20
14 |
15 | ### BC Breaks
16 |
17 | - Renamed importmap entry `@symfony/ux-leaflet-map/map-controller` to `@symfony/ux-leaflet-map`,
18 | you will need to update your importmap.
19 |
20 | ## 2.19
21 |
22 | - Bridge added
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024-present Fabien Potencier
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Symfony UX Map: Leaflet
2 |
3 | [Leaflet](https://leafletjs.com/) integration for Symfony UX Map.
4 |
5 | ## Installation
6 |
7 | Install the bridge using Composer and Symfony Flex:
8 |
9 | ```shell
10 | composer require symfony/ux-leaflet-map
11 | ```
12 |
13 | If you're using WebpackEncore, install your assets and restart Encore (not
14 | needed if you're using AssetMapper):
15 |
16 | ```shell
17 | npm install --force
18 | npm run watch
19 | ```
20 |
21 | > [!NOTE]
22 | > Alternatively, [@symfony/ux-leaflet-map package](https://www.npmjs.com/package/@symfony/ux-leaflet-map) can be used to install the JavaScript assets without requiring PHP.
23 |
24 | ## DSN example
25 |
26 | ```dotenv
27 | UX_MAP_DSN=leaflet://default
28 | ```
29 |
30 | ## Map options
31 |
32 | You can use the `LeafletOptions` class to configure your `Map`::
33 |
34 | ```php
35 | use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
36 | use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
37 | use Symfony\UX\Map\Point;
38 | use Symfony\UX\Map\Map;
39 |
40 | $map = (new Map())
41 | ->center(new Point(48.8566, 2.3522))
42 | ->zoom(6);
43 |
44 | $leafletOptions = (new LeafletOptions())
45 | ->tileLayer(new TileLayer(
46 | url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
47 | attribution: '© OpenStreetMap',
48 | options: [
49 | 'minZoom' => 5,
50 | 'maxZoom' => 10,
51 | ]
52 | ))
53 | ;
54 |
55 | // Add the custom options to the map
56 | $map->options($leafletOptions);
57 | ```
58 |
59 | ## Use cases
60 |
61 | Below are some common or advanced use cases when using a map.
62 |
63 | ### Customize the marker
64 |
65 | A common use case is to customize the marker. You can listen to the `ux:map:marker:before-create` event to customize the marker before it is created.
66 |
67 | Assuming you have a map with a custom controller:
68 | ```twig
69 | {{ ux_map(map, {'data-controller': 'my-map' }) }}
70 | ```
71 |
72 | You can create a Stimulus controller to customize the markers before they are created:
73 | ```js
74 | // assets/controllers/my_map_controller.js
75 | import {Controller} from "@hotwired/stimulus";
76 |
77 | export default class extends Controller
78 | {
79 | connect() {
80 | this.element.addEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
81 | }
82 |
83 | disconnect() {
84 | // Always remove listeners when the controller is disconnected
85 | this.element.removeEventListener('ux:map:marker:before-create', this._onMarkerBeforeCreate);
86 | }
87 |
88 | _onMarkerBeforeCreate(event) {
89 | // You can access the marker definition and the Leaflet object
90 | // Note: `definition.rawOptions` is the raw options object that will be passed to the `L.marker` constructor.
91 | const { definition, L } = event.detail;
92 |
93 | // Use a custom icon for the marker
94 | const redIcon = L.icon({
95 | // Note: instead of using a hardcoded URL, you can use the `extra` parameter from `new Marker()` (PHP) and access it here with `definition.extra`.
96 | iconUrl: 'https://leafletjs.com/examples/custom-icons/leaf-red.png',
97 | shadowUrl: 'https://leafletjs.com/examples/custom-icons/leaf-shadow.png',
98 | iconSize: [38, 95], // size of the icon
99 | shadowSize: [50, 64], // size of the shadow
100 | iconAnchor: [22, 94], // point of the icon which will correspond to marker's location
101 | shadowAnchor: [4, 62], // the same for the shadow
102 | popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
103 | })
104 |
105 | definition.rawOptions = {
106 | icon: redIcon,
107 | }
108 | }
109 | }
110 | ```
111 |
112 | ### Disable the default tile layer
113 |
114 | If you need to use a custom tiles layer rendering engine that is not compatible with the `L.tileLayer().addTo(map)` method
115 | (e.g. e.g.: [Esri/esri-leaflet-vector](https://github.com/Esri/esri-leaflet-vector)), you can disable the default tile layer by passing `tileLayer: false` to the `LeafletOptions`:
116 |
117 | ```php
118 | use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
119 |
120 | $leafletOptions = new LeafletOptions(tileLayer: false);
121 | // or
122 | $leafletOptions = (new LeafletOptions())
123 | ->tileLayer(false);
124 | ```
125 |
126 | ## Known issues
127 |
128 | ### Unable to find `leaflet/dist/leaflet.min.css` file when using Webpack Encore
129 |
130 | When using Webpack Encore with the Leaflet bridge, you may encounter the following error:
131 | ```
132 | Module build failed: Module not found:
133 | "./node_modules/.pnpm/file+vendor+symfony+ux-leaflet-map+assets_@hotwired+stimulus@3.0.0_leaflet@1.9.4/node_modules/@symfony/ux-leaflet-map/dist/map_controller.js" contains a reference to the file "leaflet/dist/leaflet.min.css".
134 | This file can not be found, please check it for typos or update it if the file got moved.
135 |
136 | Entrypoint app = runtime.67292354.js 488.0777101a.js app.b75294ae.css app.0975a86d.js
137 | webpack compiled with 1 error
138 | ELIFECYCLE Command failed with exit code 1.
139 | ```
140 |
141 | That's because the Leaflet's Stimulus controller references the `leaflet/dist/leaflet.min.css` file,
142 | which exists on [jsDelivr](https://www.jsdelivr.com/package/npm/leaflet) (used by the Symfony AssetMapper component),
143 | but does not in the [`leaflet` npm package](https://www.npmjs.com/package/leaflet).
144 | The correct path is `leaflet/dist/leaflet.css`, but it is not possible to fix it because it would break compatibility
145 | with the Symfony AssetMapper component.
146 |
147 | As a workaround, you can configure Webpack Encore to add an alias for the `leaflet/dist/leaflet.min.css` file:
148 | ```js
149 | Encore.addAliases({
150 | 'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
151 | })
152 | ```
153 |
154 | ## Resources
155 |
156 | - [Documentation](https://symfony.com/bundles/ux-map/current/index.html)
157 | - [Report issues](https://github.com/symfony/ux/issues) and
158 | [send Pull Requests](https://github.com/symfony/ux/pulls)
159 | in the [main Symfony UX repository](https://github.com/symfony/ux)
160 |
--------------------------------------------------------------------------------
/assets/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024-present Fabien Potencier
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/assets/README.md:
--------------------------------------------------------------------------------
1 | # @symfony/ux-leaflet-map
2 |
3 | JavaScript assets of the [symfony/ux-leaflet-map](https://packagist.org/packages/symfony/ux-leaflet-map) PHP package.
4 |
5 | ## Installation
6 |
7 | This npm package is **reserved for advanced users** who want to decouple their JavaScript dependencies from their PHP dependencies (e.g., when building Docker images, running JavaScript-only pipelines, etc.).
8 |
9 | We **strongly recommend not installing this package directly**, but instead install the PHP package [symfony/ux-leaflet-map](https://packagist.org/packages/symfony/ux-leaflet-map) in your Symfony application with [Flex](https://github.com/symfony/flex) enabled.
10 |
11 | If you still want to install this package directly, please make sure its version exactly matches [symfony/ux-leaflet-map](https://packagist.org/packages/symfony/ux-leaflet-map) PHP package version:
12 | ```shell
13 | composer require symfony/ux-leaflet-map:2.23.0
14 | npm add @symfony/ux-leaflet-map@2.23.0
15 | ```
16 |
17 | **Tip:** Your `package.json` file will be automatically modified by [Flex](https://github.com/symfony/flex) when installing or upgrading a PHP package. To prevent this behavior, ensure to **use at least Flex 1.22.0 or 2.5.0**, and run `composer config extra.symfony.flex.synchronize_package_json false`.
18 |
19 | ## Resources
20 |
21 | - [Documentation](https://github.com/symfony/ux/tree/2.x/src/Map/src/Bridge/Google)
22 | - [Report issues](https://github.com/symfony/ux/issues) and
23 | [send Pull Requests](https://github.com/symfony/ux/pulls)
24 | in the [main Symfony UX repository](https://github.com/symfony/ux)
25 |
--------------------------------------------------------------------------------
/assets/dist/map_controller.d.ts:
--------------------------------------------------------------------------------
1 | import AbstractMapController from '@symfony/ux-map';
2 | import type { Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
3 | import 'leaflet/dist/leaflet.min.css';
4 | import * as L from 'leaflet';
5 | import type { MapOptions as LeafletMapOptions, MarkerOptions, PolylineOptions as PolygonOptions, PolylineOptions, PopupOptions } from 'leaflet';
6 | type MapOptions = Pick & {
7 | tileLayer: {
8 | url: string;
9 | attribution: string;
10 | options: Record;
11 | } | false;
12 | };
13 | export default class extends AbstractMapController {
14 | map: L.Map;
15 | connect(): void;
16 | centerValueChanged(): void;
17 | zoomValueChanged(): void;
18 | protected dispatchEvent(name: string, payload?: Record): void;
19 | protected doCreateMap({ center, zoom, options, }: {
20 | center: Point | null;
21 | zoom: number | null;
22 | options: MapOptions;
23 | }): L.Map;
24 | protected doCreateMarker({ definition }: {
25 | definition: MarkerDefinition;
26 | }): L.Marker;
27 | protected doRemoveMarker(marker: L.Marker): void;
28 | protected doCreatePolygon({ definition, }: {
29 | definition: PolygonDefinition;
30 | }): L.Polygon;
31 | protected doRemovePolygon(polygon: L.Polygon): void;
32 | protected doCreatePolyline({ definition, }: {
33 | definition: PolylineDefinition;
34 | }): L.Polyline;
35 | protected doRemovePolyline(polyline: L.Polyline): void;
36 | protected doCreateInfoWindow({ definition, element, }: {
37 | definition: InfoWindowWithoutPositionDefinition;
38 | element: L.Marker | L.Polygon | L.Polyline;
39 | }): L.Popup;
40 | protected doCreateIcon({ definition, element, }: {
41 | definition: Icon;
42 | element: L.Marker;
43 | }): void;
44 | protected doFitBoundsToMarkers(): void;
45 | }
46 | export {};
47 |
--------------------------------------------------------------------------------
/assets/dist/map_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from '@hotwired/stimulus';
2 | import 'leaflet/dist/leaflet.min.css';
3 | import * as L from 'leaflet';
4 |
5 | const IconTypes = {
6 | Url: 'url',
7 | Svg: 'svg',
8 | UxIcon: 'ux-icon',
9 | };
10 | class default_1 extends Controller {
11 | constructor() {
12 | super(...arguments);
13 | this.markers = new Map();
14 | this.polygons = new Map();
15 | this.polylines = new Map();
16 | this.infoWindows = [];
17 | this.isConnected = false;
18 | }
19 | connect() {
20 | const options = this.optionsValue;
21 | this.dispatchEvent('pre-connect', { options });
22 | this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
23 | this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
24 | this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
25 | this.map = this.doCreateMap({
26 | center: this.hasCenterValue ? this.centerValue : null,
27 | zoom: this.hasZoomValue ? this.zoomValue : null,
28 | options,
29 | });
30 | this.markersValue.forEach((definition) => this.createMarker({ definition }));
31 | this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
32 | this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
33 | if (this.fitBoundsToMarkersValue) {
34 | this.doFitBoundsToMarkers();
35 | }
36 | this.dispatchEvent('connect', {
37 | map: this.map,
38 | markers: [...this.markers.values()],
39 | polygons: [...this.polygons.values()],
40 | polylines: [...this.polylines.values()],
41 | infoWindows: this.infoWindows,
42 | });
43 | this.isConnected = true;
44 | }
45 | createInfoWindow({ definition, element, }) {
46 | this.dispatchEvent('info-window:before-create', { definition, element });
47 | const infoWindow = this.doCreateInfoWindow({ definition, element });
48 | this.dispatchEvent('info-window:after-create', { infoWindow, definition, element });
49 | this.infoWindows.push(infoWindow);
50 | return infoWindow;
51 | }
52 | markersValueChanged() {
53 | if (!this.isConnected) {
54 | return;
55 | }
56 | this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
57 | if (this.fitBoundsToMarkersValue) {
58 | this.doFitBoundsToMarkers();
59 | }
60 | }
61 | polygonsValueChanged() {
62 | if (!this.isConnected) {
63 | return;
64 | }
65 | this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
66 | }
67 | polylinesValueChanged() {
68 | if (!this.isConnected) {
69 | return;
70 | }
71 | this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
72 | }
73 | createDrawingFactory(type, draws, factory) {
74 | const eventBefore = `${type}:before-create`;
75 | const eventAfter = `${type}:after-create`;
76 | return ({ definition }) => {
77 | this.dispatchEvent(eventBefore, { definition });
78 | const drawing = factory({ definition });
79 | this.dispatchEvent(eventAfter, { [type]: drawing, definition });
80 | draws.set(definition['@id'], drawing);
81 | return drawing;
82 | };
83 | }
84 | onDrawChanged(draws, newDrawDefinitions, factory, remover) {
85 | const idsToRemove = new Set(draws.keys());
86 | newDrawDefinitions.forEach((definition) => {
87 | idsToRemove.delete(definition['@id']);
88 | });
89 | idsToRemove.forEach((id) => {
90 | const draw = draws.get(id);
91 | remover(draw);
92 | draws.delete(id);
93 | });
94 | newDrawDefinitions.forEach((definition) => {
95 | if (!draws.has(definition['@id'])) {
96 | factory({ definition });
97 | }
98 | });
99 | }
100 | }
101 | default_1.values = {
102 | providerOptions: Object,
103 | center: Object,
104 | zoom: Number,
105 | fitBoundsToMarkers: Boolean,
106 | markers: Array,
107 | polygons: Array,
108 | polylines: Array,
109 | options: Object,
110 | };
111 |
112 | class map_controller extends default_1 {
113 | connect() {
114 | L.Marker.prototype.options.icon = L.divIcon({
115 | html: '',
116 | iconSize: [25, 41],
117 | iconAnchor: [12.5, 41],
118 | popupAnchor: [0, -41],
119 | className: '',
120 | });
121 | super.connect();
122 | }
123 | centerValueChanged() {
124 | if (this.map && this.hasCenterValue && this.centerValue && this.hasZoomValue && this.zoomValue) {
125 | this.map.setView(this.centerValue, this.zoomValue);
126 | }
127 | }
128 | zoomValueChanged() {
129 | if (this.map && this.hasZoomValue && this.zoomValue) {
130 | this.map.setZoom(this.zoomValue);
131 | }
132 | }
133 | dispatchEvent(name, payload = {}) {
134 | this.dispatch(name, {
135 | prefix: 'ux:map',
136 | detail: {
137 | ...payload,
138 | L,
139 | },
140 | });
141 | }
142 | doCreateMap({ center, zoom, options, }) {
143 | const map = L.map(this.element, {
144 | ...options,
145 | center: center === null ? undefined : center,
146 | zoom: zoom === null ? undefined : zoom,
147 | });
148 | if (options.tileLayer) {
149 | L.tileLayer(options.tileLayer.url, {
150 | attribution: options.tileLayer.attribution,
151 | ...options.tileLayer.options,
152 | }).addTo(map);
153 | }
154 | return map;
155 | }
156 | doCreateMarker({ definition }) {
157 | const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
158 | const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions }).addTo(this.map);
159 | if (infoWindow) {
160 | this.createInfoWindow({ definition: infoWindow, element: marker });
161 | }
162 | if (icon) {
163 | this.doCreateIcon({ definition: icon, element: marker });
164 | }
165 | return marker;
166 | }
167 | doRemoveMarker(marker) {
168 | marker.remove();
169 | }
170 | doCreatePolygon({ definition, }) {
171 | const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
172 | const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
173 | if (title) {
174 | polygon.bindPopup(title);
175 | }
176 | if (infoWindow) {
177 | this.createInfoWindow({ definition: infoWindow, element: polygon });
178 | }
179 | return polygon;
180 | }
181 | doRemovePolygon(polygon) {
182 | polygon.remove();
183 | }
184 | doCreatePolyline({ definition, }) {
185 | const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
186 | const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
187 | if (title) {
188 | polyline.bindPopup(title);
189 | }
190 | if (infoWindow) {
191 | this.createInfoWindow({ definition: infoWindow, element: polyline });
192 | }
193 | return polyline;
194 | }
195 | doRemovePolyline(polyline) {
196 | polyline.remove();
197 | }
198 | doCreateInfoWindow({ definition, element, }) {
199 | const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
200 | element.bindPopup([headerContent, content].filter((x) => x).join('
'), { ...otherOptions, ...rawOptions });
201 | if (definition.opened) {
202 | element.openPopup();
203 | }
204 | const popup = element.getPopup();
205 | if (!popup) {
206 | throw new Error('Unable to get the Popup associated with the element.');
207 | }
208 | return popup;
209 | }
210 | doCreateIcon({ definition, element, }) {
211 | const { type, width, height } = definition;
212 | let icon;
213 | if (type === IconTypes.Svg) {
214 | icon = L.divIcon({
215 | html: definition.html,
216 | iconSize: [width, height],
217 | className: '',
218 | });
219 | }
220 | else if (type === IconTypes.UxIcon) {
221 | icon = L.divIcon({
222 | html: definition._generated_html,
223 | iconSize: [width, height],
224 | className: '',
225 | });
226 | }
227 | else if (type === IconTypes.Url) {
228 | icon = L.icon({
229 | iconUrl: definition.url,
230 | iconSize: [width, height],
231 | className: '',
232 | });
233 | }
234 | else {
235 | throw new Error(`Unsupported icon type: ${type}.`);
236 | }
237 | element.setIcon(icon);
238 | }
239 | doFitBoundsToMarkers() {
240 | if (this.markers.size === 0) {
241 | return;
242 | }
243 | const bounds = [];
244 | this.markers.forEach((marker) => {
245 | const position = marker.getLatLng();
246 | bounds.push([position.lat, position.lng]);
247 | });
248 | this.map.fitBounds(bounds);
249 | }
250 | }
251 |
252 | export { map_controller as default };
253 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@symfony/ux-leaflet-map",
3 | "description": "Leaflet bridge for Symfony UX Map, integrate interactive maps in your Symfony applications",
4 | "license": "MIT",
5 | "version": "2.26.1",
6 | "keywords": [
7 | "symfony-ux",
8 | "leaflet",
9 | "map"
10 | ],
11 | "homepage": "https://ux.symfony.com/map",
12 | "repository": "https://github.com/symfony/ux-leaflet-map",
13 | "type": "module",
14 | "files": [
15 | "dist"
16 | ],
17 | "main": "dist/map_controller.js",
18 | "types": "dist/map_controller.d.ts",
19 | "scripts": {
20 | "build": "node ../../../../../../bin/build_package.js .",
21 | "watch": "node ../../../../../../bin/build_package.js . --watch",
22 | "test": "../../../../../../bin/test_package.sh .",
23 | "check": "biome check",
24 | "ci": "biome ci"
25 | },
26 | "symfony": {
27 | "controllers": {
28 | "map": {
29 | "main": "dist/map_controller.js",
30 | "webpackMode": "lazy",
31 | "fetch": "lazy",
32 | "enabled": true
33 | }
34 | },
35 | "importmap": {
36 | "@hotwired/stimulus": "^3.0.0",
37 | "leaflet": "^1.9.4",
38 | "@symfony/ux-leaflet-map": "path:%PACKAGE%/dist/map_controller.js"
39 | }
40 | },
41 | "peerDependencies": {
42 | "@hotwired/stimulus": "^3.0.0",
43 | "leaflet": "^1.9.4"
44 | },
45 | "peerDependenciesMeta": {
46 | "leaflet": {
47 | "optional": false
48 | }
49 | },
50 | "devDependencies": {
51 | "@hotwired/stimulus": "^3.0.0",
52 | "@symfony/ux-map": "workspace:*",
53 | "@types/leaflet": "^1.9.12",
54 | "leaflet": "^1.9.4"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/assets/vitest.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig, mergeConfig } from 'vitest/config';
2 | import configShared from '../../../../../../vitest.config.mjs'
3 |
4 | export default mergeConfig(
5 | configShared,
6 | defineConfig({
7 | resolve: {
8 | alias: {
9 | '@symfony/ux-map': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
10 | 'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
11 | },
12 | },
13 | define: {
14 | // Prevent the following error:
15 | // ReferenceError: global is not defined
16 | // ❯ ../../../../../../node_modules/pretty-format/build/plugins/AsymmetricMatcher.js ../../../../../../../../../../node_modules/.vite/deps/@testing-library_dom.js:139:19
17 | // ❯ ../../../../../../node_modules/pretty-format/build/index.js ../../../../../../../../../../node_modules/.vite/deps/@testing-library_dom.js:805:7
18 | // ❯ ../../../../../../../../../../node_modules/.vite/deps/@testing-library_dom.js:13445:36
19 | global: {}
20 | },
21 | test: {
22 | browser: {
23 | enabled: true,
24 | provider: 'playwright', // or 'webdriverio'
25 | name: 'chromium', // browser name is required
26 | headless: true,
27 | },
28 | },
29 | })
30 | );
31 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "symfony/ux-leaflet-map",
3 | "type": "symfony-ux-map-bridge",
4 | "description": "Symfony UX Map Leaflet Bridge",
5 | "keywords": ["symfony-ux", "leaflet", "map"],
6 | "homepage": "https://symfony.com",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Hugo Alliaume",
11 | "email": "hugo@alliau.me"
12 | },
13 | {
14 | "name": "Symfony Community",
15 | "homepage": "https://symfony.com/contributors"
16 | }
17 | ],
18 | "require": {
19 | "php": ">=8.1",
20 | "symfony/stimulus-bundle": "^2.18.1",
21 | "symfony/ux-map": "^2.19"
22 | },
23 | "require-dev": {
24 | "symfony/phpunit-bridge": "^7.2",
25 | "symfony/ux-icons": "^2.18",
26 | "spatie/phpunit-snapshot-assertions": "^4.2.17",
27 | "phpunit/phpunit": "^9.6.22"
28 | },
29 | "autoload": {
30 | "psr-4": { "Symfony\\UX\\Map\\Bridge\\Leaflet\\": "src/" },
31 | "exclude-from-classmap": []
32 | },
33 | "autoload-dev": {
34 | "psr-4": { "Symfony\\UX\\Map\\Bridge\\Leaflet\\Tests\\": "tests/" }
35 | },
36 | "minimum-stability": "dev"
37 | }
38 |
--------------------------------------------------------------------------------
/src/LeafletOptions.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\UX\Map\Bridge\Leaflet;
13 |
14 | use Symfony\UX\Map\Bridge\Leaflet\Option\TileLayer;
15 | use Symfony\UX\Map\MapOptionsInterface;
16 |
17 | /**
18 | * @author Hugo Alliaume
19 | */
20 | final class LeafletOptions implements MapOptionsInterface
21 | {
22 | public function __construct(
23 | private TileLayer|false $tileLayer = new TileLayer(
24 | url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
25 | attribution: '© OpenStreetMap',
26 | ),
27 | ) {
28 | }
29 |
30 | public function tileLayer(TileLayer|false $tileLayer): self
31 | {
32 | $this->tileLayer = $tileLayer;
33 |
34 | return $this;
35 | }
36 |
37 | /**
38 | * @internal
39 | */
40 | public static function fromArray(array $array): MapOptionsInterface
41 | {
42 | return new self(
43 | tileLayer: $array['tileLayer'] ? TileLayer::fromArray($array['tileLayer']) : false,
44 | );
45 | }
46 |
47 | /**
48 | * @internal
49 | */
50 | public function toArray(): array
51 | {
52 | return [
53 | 'tileLayer' => $this->tileLayer ? $this->tileLayer->toArray() : false,
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Option/TileLayer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\UX\Map\Bridge\Leaflet\Option;
13 |
14 | /**
15 | * Represents a tile layer for a Leaflet map.
16 | *
17 | * @see https://leafletjs.com/reference.html#tilelayer
18 | *
19 | * @author Hugo Alliaume
20 | */
21 | final class TileLayer
22 | {
23 | /**
24 | * @param array $options
25 | */
26 | public function __construct(
27 | private readonly string $url,
28 | private readonly string $attribution,
29 | private readonly array $options = [],
30 | ) {
31 | }
32 |
33 | /**
34 | * @internal
35 | */
36 | public static function fromArray(array $array): self
37 | {
38 | return new self(
39 | url: $array['url'],
40 | attribution: $array['attribution'],
41 | options: $array['options'],
42 | );
43 | }
44 |
45 | /**
46 | * @internal
47 | */
48 | public function toArray(): array
49 | {
50 | return [
51 | 'url' => $this->url,
52 | 'attribution' => $this->attribution,
53 | 'options' => $this->options,
54 | ];
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Renderer/LeafletRenderer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\UX\Map\Bridge\Leaflet\Renderer;
13 |
14 | use Symfony\UX\Map\Bridge\Leaflet\LeafletOptions;
15 | use Symfony\UX\Map\MapOptionsInterface;
16 | use Symfony\UX\Map\Renderer\AbstractRenderer;
17 |
18 | /**
19 | * @author Hugo Alliaume
20 | *
21 | * @internal
22 | */
23 | final class LeafletRenderer extends AbstractRenderer
24 | {
25 | protected function getName(): string
26 | {
27 | return 'leaflet';
28 | }
29 |
30 | protected function getProviderOptions(): array
31 | {
32 | return [];
33 | }
34 |
35 | protected function getDefaultMapOptions(): MapOptionsInterface
36 | {
37 | return new LeafletOptions();
38 | }
39 |
40 | public function __toString(): string
41 | {
42 | return 'leaflet://default';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Renderer/LeafletRendererFactory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | namespace Symfony\UX\Map\Bridge\Leaflet\Renderer;
13 |
14 | use Symfony\UX\Map\Exception\UnsupportedSchemeException;
15 | use Symfony\UX\Map\Renderer\AbstractRendererFactory;
16 | use Symfony\UX\Map\Renderer\Dsn;
17 | use Symfony\UX\Map\Renderer\RendererFactoryInterface;
18 | use Symfony\UX\Map\Renderer\RendererInterface;
19 |
20 | /**
21 | * @author Hugo Alliaume
22 | */
23 | final class LeafletRendererFactory extends AbstractRendererFactory implements RendererFactoryInterface
24 | {
25 | public function create(Dsn $dsn): RendererInterface
26 | {
27 | if (!$this->supports($dsn)) {
28 | throw new UnsupportedSchemeException($dsn);
29 | }
30 |
31 | return new LeafletRenderer($this->stimulus, $this->uxIconRenderer);
32 | }
33 |
34 | protected function getSupportedSchemes(): array
35 | {
36 | return ['leaflet'];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------