Skip to content

Commit a7dd42e

Browse files
committed
feat: custom icon on markers
1 parent 6d2daab commit a7dd42e

22 files changed

+424
-423
lines changed

src/Map/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 2.23
44

5+
- Add marker `Icon` support (image and svg)
56
- Add `DistanceUnit` to represent distance units (`m`, `km`, `miles`, `nmi`) and
67
ease conversion between units.
78
- Add `DistanceCalculatorInterface` interface and three implementations:

src/Map/assets/dist/abstract_map_controller.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ export type Point = {
33
lat: number;
44
lng: number;
55
};
6+
export type Icon = {
7+
content: string;
8+
iconType: string;
9+
width: number;
10+
height: number;
11+
};
612
export type Identifier = string;
713
export type WithIdentifier<T extends Record<string, unknown>> = T & {
814
'@id': Identifier;
@@ -11,6 +17,7 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<
1117
position: Point;
1218
title: string | null;
1319
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
20+
icon?: Icon;
1421
rawOptions?: MarkerOptions;
1522
extra: Record<string, unknown>;
1623
}>;
@@ -105,6 +112,10 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
105112
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
106113
element: Marker | Polygon | Polyline;
107114
}): InfoWindow;
115+
protected abstract doCreateIcon({ definition, element }: {
116+
definition: Icon;
117+
element: Marker;
118+
}): Icon;
108119
private createDrawingFactory;
109120
private onDrawChanged;
110121
}

src/Map/assets/src/abstract_map_controller.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import { Controller } from '@hotwired/stimulus';
22

33
export type Point = { lat: number; lng: number };
4-
4+
export type Icon = {
5+
content: string;
6+
iconType: string;
7+
width: number;
8+
height: number;
9+
};
510
export type Identifier = string;
611
export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };
712

813
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
914
position: Point;
1015
title: string | null;
1116
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
17+
icon?: Icon;
1218
/**
1319
* Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
1420
*/
@@ -268,6 +274,10 @@ export default abstract class<
268274
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
269275
element: Marker | Polygon | Polyline;
270276
}): InfoWindow;
277+
protected abstract doCreateIcon({ definition, element }: {
278+
definition: Icon;
279+
element: Marker;
280+
}): Icon;
271281

272282
//endregion
273283

src/Map/config/icon.php

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
4+
5+
use Symfony\UX\Map\Icon\MapIconRenderer;
6+
use Symfony\UX\Map\Icon\MapIconRendererInterface;
7+
8+
return static function (ContainerConfigurator $container): void {
9+
$container->services()
10+
->set('.ux_map.ux_icons.renderer', MapIconRenderer::class)
11+
->args([
12+
service('.ux_icons.icon_renderer')->ignoreOnInvalid(),
13+
])
14+
->alias(MapIconRendererInterface::class, '.ux_map.ux_icons.renderer')
15+
;
16+
};

src/Map/config/services.php

+28-20
Original file line numberDiff line numberDiff line change
@@ -11,42 +11,50 @@
1111

1212
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1313

14-
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
14+
use Symfony\UX\Map\Icon;
15+
use Symfony\UX\Map\IconInterface;
16+
use Symfony\UX\Icons\IconRenderer;
17+
use Symfony\UX\Map\Twig\MapRuntime;
1518
use Symfony\UX\Map\Renderer\Renderer;
16-
use Symfony\UX\Map\Renderer\Renderers;
1719
use Symfony\UX\Map\Twig\MapExtension;
18-
use Symfony\UX\Map\Twig\MapRuntime;
20+
use Symfony\UX\Map\Renderer\Renderers;
21+
use Symfony\UX\Icons\IconRendererInterface;
22+
use Symfony\Component\DependencyInjection\Reference;
23+
use Symfony\UX\Map\Icon\IconFactory;
24+
use Symfony\UX\Map\Icon\IconFactoryInterface;
25+
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
1926

2027
/*
2128
* @author Hugo Alliaume <[email protected]>
2229
*/
30+
2331
return static function (ContainerConfigurator $container): void {
2432
$container->services()
2533
->set('ux_map.renderers', Renderers::class)
26-
->factory([service('ux_map.renderer_factory'), 'fromStrings'])
27-
->args([
28-
abstract_arg('renderers configuration'),
29-
])
34+
->factory([service('ux_map.renderer_factory'), 'fromStrings'])
35+
->args([
36+
abstract_arg('renderers configuration'),
37+
])
3038

3139
->set('ux_map.renderer_factory.abstract', AbstractRendererFactory::class)
32-
->abstract()
33-
->args([
34-
service('stimulus.helper'),
35-
])
40+
->abstract()
41+
->args([
42+
service('stimulus.helper'),
43+
])
3644

3745
->set('ux_map.renderer_factory', Renderer::class)
38-
->args([
39-
tagged_iterator('ux_map.renderer_factory'),
40-
])
46+
->args([
47+
tagged_iterator('ux_map.renderer_factory'),
48+
])
4149

4250
->set('ux_map.twig_extension', MapExtension::class)
43-
->tag('twig.extension')
51+
->tag('twig.extension')
4452

4553
->set('ux_map.twig_runtime', MapRuntime::class)
46-
->args([
47-
service('ux_map.renderers'),
48-
])
49-
->tag('twig.runtime')
50-
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:map'])
54+
->args([
55+
service('ux_map.renderers'),
56+
])
57+
->tag('twig.runtime')
58+
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:map'])
5159
;
5260
};

src/Map/doc/index.rst

+40
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ You can add markers to a map using the ``addMarker()`` method::
116116
infoWindow: new InfoWindow(
117117
headerContent: '<b>Lyon</b>',
118118
content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.'
119+
),
120+
icon: new Icon(
121+
content: '<svg>....</svg>'
122+
icontType: 'html'
119123
)
120124
))
121125

@@ -136,6 +140,42 @@ You can add markers to a map using the ``addMarker()`` method::
136140
))
137141
;
138142

143+
Add Marker icons
144+
~~~~~~~~~~~~~~~~
145+
146+
It is possible to add icon to Marker::
147+
148+
new Marker(
149+
position: new Point(45.7640, 4.8357),
150+
title: 'Lyon',
151+
infoWindow: new InfoWindow(
152+
headerContent: '<b>Lyon</b>',
153+
content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.'
154+
),
155+
icon: new Icon(
156+
content: '<svg>....</svg>'
157+
icontType: 'html'
158+
)
159+
))
160+
161+
You can add image icon (type 'url')::
162+
163+
new Icon (
164+
content: 'https://my-image.png',
165+
iconType: 'url',
166+
width: 24, // default
167+
height: 24, // default
168+
)
169+
170+
or SVG icon (type 'html')::
171+
172+
new Icon (
173+
content: '<svg>...</svg>',
174+
iconType: 'html',
175+
width: 36,
176+
height: 36,
177+
)
178+
139179
Remove elements from Map
140180
~~~~~~~~~~~~~~~~~~~~~~~~
141181

src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
3232
}): google.maps.InfoWindow;
3333
protected doFitBoundsToMarkers(): void;
3434
private createTextOrElement;
35+
protected doCreateIcon({ definition, element }: {
36+
definition: any;
37+
element: any;
38+
}): void;
3539
private closeInfoWindowsExcept;
3640
}
3741
export {};

src/Map/src/Bridge/Google/assets/dist/map_controller.js

+21-105
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,8 @@
11
import { Loader } from '@googlemaps/js-api-loader';
2-
import { Controller } from '@hotwired/stimulus';
3-
4-
class default_1 extends Controller {
5-
constructor() {
6-
super(...arguments);
7-
this.markers = new Map();
8-
this.polygons = new Map();
9-
this.polylines = new Map();
10-
this.infoWindows = [];
11-
this.isConnected = false;
12-
}
13-
connect() {
14-
const options = this.optionsValue;
15-
this.dispatchEvent('pre-connect', { options });
16-
this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
17-
this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
18-
this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
19-
this.map = this.doCreateMap({
20-
center: this.hasCenterValue ? this.centerValue : null,
21-
zoom: this.hasZoomValue ? this.zoomValue : null,
22-
options,
23-
});
24-
this.markersValue.forEach((definition) => this.createMarker({ definition }));
25-
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
26-
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
27-
if (this.fitBoundsToMarkersValue) {
28-
this.doFitBoundsToMarkers();
29-
}
30-
this.dispatchEvent('connect', {
31-
map: this.map,
32-
markers: [...this.markers.values()],
33-
polygons: [...this.polygons.values()],
34-
polylines: [...this.polylines.values()],
35-
infoWindows: this.infoWindows,
36-
});
37-
this.isConnected = true;
38-
}
39-
createInfoWindow({ definition, element, }) {
40-
this.dispatchEvent('info-window:before-create', { definition, element });
41-
const infoWindow = this.doCreateInfoWindow({ definition, element });
42-
this.dispatchEvent('info-window:after-create', { infoWindow, element });
43-
this.infoWindows.push(infoWindow);
44-
return infoWindow;
45-
}
46-
markersValueChanged() {
47-
if (!this.isConnected) {
48-
return;
49-
}
50-
this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
51-
if (this.fitBoundsToMarkersValue) {
52-
this.doFitBoundsToMarkers();
53-
}
54-
}
55-
polygonsValueChanged() {
56-
if (!this.isConnected) {
57-
return;
58-
}
59-
this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
60-
}
61-
polylinesValueChanged() {
62-
if (!this.isConnected) {
63-
return;
64-
}
65-
this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
66-
}
67-
createDrawingFactory(type, draws, factory) {
68-
const eventBefore = `${type}:before-create`;
69-
const eventAfter = `${type}:after-create`;
70-
return ({ definition }) => {
71-
this.dispatchEvent(eventBefore, { definition });
72-
const drawing = factory({ definition });
73-
this.dispatchEvent(eventAfter, { [type]: drawing });
74-
draws.set(definition['@id'], drawing);
75-
return drawing;
76-
};
77-
}
78-
onDrawChanged(draws, newDrawDefinitions, factory, remover) {
79-
const idsToRemove = new Set(draws.keys());
80-
newDrawDefinitions.forEach((definition) => {
81-
idsToRemove.delete(definition['@id']);
82-
});
83-
idsToRemove.forEach((id) => {
84-
const draw = draws.get(id);
85-
remover(draw);
86-
draws.delete(id);
87-
});
88-
newDrawDefinitions.forEach((definition) => {
89-
if (!draws.has(definition['@id'])) {
90-
factory({ definition });
91-
}
92-
});
93-
}
94-
}
95-
default_1.values = {
96-
providerOptions: Object,
97-
center: Object,
98-
zoom: Number,
99-
fitBoundsToMarkers: Boolean,
100-
markers: Array,
101-
polygons: Array,
102-
polylines: Array,
103-
options: Object,
104-
};
2+
import AbstractMapController from '@symfony/ux-map';
1053

1064
let _google;
107-
class map_controller extends default_1 {
5+
class map_controller extends AbstractMapController {
1086
async connect() {
1097
if (!_google) {
1108
_google = { maps: {} };
@@ -158,7 +56,7 @@ class map_controller extends default_1 {
15856
});
15957
}
16058
doCreateMarker({ definition, }) {
161-
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
59+
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
16260
const marker = new _google.maps.marker.AdvancedMarkerElement({
16361
position,
16462
title,
@@ -169,6 +67,9 @@ class map_controller extends default_1 {
16967
if (infoWindow) {
17068
this.createInfoWindow({ definition: infoWindow, element: marker });
17169
}
70+
if (icon) {
71+
this.doCreateIcon({ definition: icon, element: marker });
72+
}
17273
return marker;
17374
}
17475
doRemoveMarker(marker) {
@@ -272,6 +173,21 @@ class map_controller extends default_1 {
272173
}
273174
return content;
274175
}
176+
doCreateIcon({ definition, element }) {
177+
const { content, iconType, width, height } = definition;
178+
if (iconType === 'html') {
179+
const parser = new DOMParser();
180+
const icon = parser.parseFromString(content, "image/svg+xml").documentElement;
181+
element.content = icon;
182+
}
183+
else {
184+
const icon = document.createElement('img');
185+
icon.width = width.toString();
186+
icon.height = height.toString();
187+
icon.src = content;
188+
element.content = icon;
189+
}
190+
}
275191
closeInfoWindowsExcept(infoWindow) {
276192
this.infoWindows.forEach((otherInfoWindow) => {
277193
if (otherInfoWindow !== infoWindow) {

0 commit comments

Comments
 (0)