Skip to content

Commit 3f43338

Browse files
committed
feature #2605 [Map] Add Marker Icon customization capability (sblondeau, Kocal)
This PR was merged into the 2.x branch. Discussion ---------- [Map] Add Marker Icon customization capability | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Issues | Fix #2109 <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT A nicely asked feature, adding Marker icon customization, it can be an UX Icon, an URL, or a SVG content: ```php // It can be a UX Icon (requires `symfony/ux-icons` package)... $icon = Icon::ux('fa:map-marker')->width(24)->height(24); // ... or an URL pointing to an image $icon = Icon::url('https://example.com/marker.png')->width(24)->height(24); // ... or a plain SVG string $icon = Icon::svg('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>'); $map->addMarker(new Marker( // ... icon: $icon )); ``` ## Rendering ![image](https://github.com/user-attachments/assets/08a4a887-06a0-421a-8e90-baa194b15554) Commits ------- c4f9828 [Map] Rework UxIconRenderer injection 9976692 [Map] Rework SvgIcon and factory method name, UX Icon to get generated HTML, and icons rendering 311689c [Map] Refactor and simplify the way to create a custom Icon 73151f6 [Map] Define IconTypes and IconType in TypeScript dd20dbe feat: custom icon on markers
2 parents 3ac2c46 + c4f9828 commit 3f43338

34 files changed

+836
-48
lines changed

src/Map/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2.24
44

55
- Installing the package in a Symfony app using Flex won't add the `@symfony/ux-map` dependency to the `package.json` file anymore.
6+
- Add `Icon` to customize a `Marker` icon (URL or SVG content)
67

78
## 2.23
89

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,30 @@ export type Identifier = string;
77
export type WithIdentifier<T extends Record<string, unknown>> = T & {
88
'@id': Identifier;
99
};
10+
export declare const IconTypes: {
11+
readonly Url: "url";
12+
readonly Svg: "svg";
13+
readonly UxIcon: "ux-icon";
14+
};
15+
export type Icon = {
16+
width: number;
17+
height: number;
18+
} & ({
19+
type: typeof IconTypes.UxIcon;
20+
name: string;
21+
_generated_html: string;
22+
} | {
23+
type: typeof IconTypes.Url;
24+
url: string;
25+
} | {
26+
type: typeof IconTypes.Svg;
27+
html: string;
28+
});
1029
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
1130
position: Point;
1231
title: string | null;
1332
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
33+
icon?: Icon;
1434
rawOptions?: MarkerOptions;
1535
extra: Record<string, unknown>;
1636
}>;
@@ -105,6 +125,10 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
105125
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
106126
element: Marker | Polygon | Polyline;
107127
}): InfoWindow;
128+
protected abstract doCreateIcon({ definition, element, }: {
129+
definition: Icon;
130+
element: Marker;
131+
}): void;
108132
private createDrawingFactory;
109133
private onDrawChanged;
110134
}

src/Map/assets/dist/abstract_map_controller.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Controller } from '@hotwired/stimulus';
22

3+
const IconTypes = {
4+
Url: 'url',
5+
Svg: 'svg',
6+
UxIcon: 'ux-icon',
7+
};
38
class default_1 extends Controller {
49
constructor() {
510
super(...arguments);
@@ -102,4 +107,4 @@ default_1.values = {
102107
options: Object,
103108
};
104109

105-
export { default_1 as default };
110+
export { IconTypes, default_1 as default };

src/Map/assets/src/abstract_map_controller.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,38 @@
11
import { Controller } from '@hotwired/stimulus';
22

33
export type Point = { lat: number; lng: number };
4-
54
export type Identifier = string;
65
export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };
76

7+
export const IconTypes = {
8+
Url: 'url',
9+
Svg: 'svg',
10+
UxIcon: 'ux-icon',
11+
} as const;
12+
export type Icon = {
13+
width: number;
14+
height: number;
15+
} & (
16+
| {
17+
type: typeof IconTypes.UxIcon;
18+
name: string;
19+
_generated_html: string;
20+
}
21+
| {
22+
type: typeof IconTypes.Url;
23+
url: string;
24+
}
25+
| {
26+
type: typeof IconTypes.Svg;
27+
html: string;
28+
}
29+
);
30+
831
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
932
position: Point;
1033
title: string | null;
1134
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
35+
icon?: Icon;
1236
/**
1337
* Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
1438
*/
@@ -268,6 +292,13 @@ export default abstract class<
268292
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
269293
element: Marker | Polygon | Polyline;
270294
}): InfoWindow;
295+
protected abstract doCreateIcon({
296+
definition,
297+
element,
298+
}: {
299+
definition: Icon;
300+
element: Marker;
301+
}): void;
271302

272303
//endregion
273304

src/Map/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"symfony/framework-bundle": "^6.4|^7.0",
4141
"symfony/phpunit-bridge": "^6.4|^7.0",
4242
"symfony/twig-bundle": "^6.4|^7.0",
43-
"symfony/ux-twig-component": "^2.18"
43+
"symfony/ux-twig-component": "^2.18",
44+
"symfony/ux-icons": "^2.18"
4445
},
4546
"conflict": {
4647
"symfony/ux-twig-component": "<2.21"

src/Map/config/services.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

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

14+
use Symfony\UX\Map\Icon\UxIconRenderer;
1415
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
1516
use Symfony\UX\Map\Renderer\Renderer;
1617
use Symfony\UX\Map\Renderer\Renderers;
@@ -20,6 +21,7 @@
2021
/*
2122
* @author Hugo Alliaume <hugo@alliau.me>
2223
*/
24+
2325
return static function (ContainerConfigurator $container): void {
2426
$container->services()
2527
->set('ux_map.renderers', Renderers::class)
@@ -28,10 +30,16 @@
2830
abstract_arg('renderers configuration'),
2931
])
3032

33+
->set('.ux_map.ux_icons.renderer', UxIconRenderer::class)
34+
->args([
35+
service('.ux_icons.icon_renderer')->nullOnInvalid(),
36+
])
37+
3138
->set('ux_map.renderer_factory.abstract', AbstractRendererFactory::class)
3239
->abstract()
3340
->args([
3441
service('stimulus.helper'),
42+
service('.ux_map.ux_icons.renderer'),
3543
])
3644

3745
->set('ux_map.renderer_factory', Renderer::class)

src/Map/doc/index.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ You can add markers to a map using the ``addMarker()`` method::
108108
infoWindow: new InfoWindow(
109109
headerContent: '<b>Lyon</b>',
110110
content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.'
111+
),
112+
icon: new Icon(
113+
content: '<svg>....</svg>'
114+
icontType: 'html'
111115
)
112116
))
113117

@@ -128,6 +132,27 @@ You can add markers to a map using the ``addMarker()`` method::
128132
))
129133
;
130134

135+
Add Marker icons
136+
~~~~~~~~~~~~~~~~
137+
138+
.. versionadded:: 2.24
139+
140+
``Marker`` icon customization is available since UX Map 2.24.
141+
142+
A ``Marker`` can be customized with an ``Icon`` instance, which can either be an UX Icon, an URL, or a SVG content::
143+
144+
// It can be a UX Icon (requires `symfony/ux-icons` package)...
145+
$icon = Icon::ux('fa:map-marker')->width(24)->height(24);
146+
// ... or an URL pointing to an image
147+
$icon = Icon::url('https://example.com/marker.png')->width(24)->height(24);
148+
// ... or a plain SVG string
149+
$icon = Icon::svg('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">...</svg>');
150+
151+
$map->addMarker(new Marker(
152+
// ...
153+
icon: $icon
154+
));
155+
131156
Remove elements from Map
132157
~~~~~~~~~~~~~~~~~~~~~~~~
133158

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { LoaderOptions } from '@googlemaps/js-api-loader';
22
import AbstractMapController from '@symfony/ux-map';
3-
import type { InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
3+
import type { Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
44
type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
55
export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon, google.maps.PolylineOptions, google.maps.Polyline> {
66
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
77
map: google.maps.Map;
8+
parser: DOMParser;
89
connect(): Promise<void>;
910
centerValueChanged(): void;
1011
zoomValueChanged(): void;
@@ -32,6 +33,10 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
3233
}): google.maps.InfoWindow;
3334
protected doFitBoundsToMarkers(): void;
3435
private createTextOrElement;
36+
protected doCreateIcon({ definition, element, }: {
37+
definition: Icon;
38+
element: google.maps.marker.AdvancedMarkerElement;
39+
}): void;
3540
private closeInfoWindowsExcept;
3641
}
3742
export {};

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { Loader } from '@googlemaps/js-api-loader';
22
import { Controller } from '@hotwired/stimulus';
33

4+
const IconTypes = {
5+
Url: 'url',
6+
Svg: 'svg',
7+
UxIcon: 'ux-icon',
8+
};
49
class default_1 extends Controller {
510
constructor() {
611
super(...arguments);
@@ -104,6 +109,7 @@ default_1.values = {
104109
};
105110

106111
let _google;
112+
const parser = new DOMParser();
107113
class map_controller extends default_1 {
108114
async connect() {
109115
if (!_google) {
@@ -126,6 +132,7 @@ class map_controller extends default_1 {
126132
});
127133
}
128134
super.connect();
135+
this.parser = new DOMParser();
129136
}
130137
centerValueChanged() {
131138
if (this.map && this.hasCenterValue && this.centerValue) {
@@ -158,7 +165,7 @@ class map_controller extends default_1 {
158165
});
159166
}
160167
doCreateMarker({ definition, }) {
161-
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
168+
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
162169
const marker = new _google.maps.marker.AdvancedMarkerElement({
163170
position,
164171
title,
@@ -169,6 +176,9 @@ class map_controller extends default_1 {
169176
if (infoWindow) {
170177
this.createInfoWindow({ definition: infoWindow, element: marker });
171178
}
179+
if (icon) {
180+
this.doCreateIcon({ definition: icon, element: marker });
181+
}
172182
return marker;
173183
}
174184
doRemoveMarker(marker) {
@@ -272,6 +282,25 @@ class map_controller extends default_1 {
272282
}
273283
return content;
274284
}
285+
doCreateIcon({ definition, element, }) {
286+
const { type, width, height } = definition;
287+
if (type === IconTypes.Svg) {
288+
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
289+
}
290+
else if (type === IconTypes.UxIcon) {
291+
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
292+
}
293+
else if (type === IconTypes.Url) {
294+
const icon = document.createElement('img');
295+
icon.width = width;
296+
icon.height = height;
297+
icon.src = definition.url;
298+
element.content = icon;
299+
}
300+
else {
301+
throw new Error(`Unsupported icon type: ${type}.`);
302+
}
303+
}
275304
closeInfoWindowsExcept(infoWindow) {
276305
this.infoWindows.forEach((otherInfoWindow) => {
277306
if (otherInfoWindow !== infoWindow) {

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
import type { LoaderOptions } from '@googlemaps/js-api-loader';
1111
import { Loader } from '@googlemaps/js-api-loader';
12-
import AbstractMapController from '@symfony/ux-map';
12+
import AbstractMapController, { IconTypes } from '@symfony/ux-map';
1313
import type {
14+
Icon,
1415
InfoWindowWithoutPositionDefinition,
1516
MarkerDefinition,
1617
Point,
@@ -36,6 +37,8 @@ type MapOptions = Pick<
3637

3738
let _google: typeof google;
3839

40+
const parser = new DOMParser();
41+
3942
export default class extends AbstractMapController<
4043
MapOptions,
4144
google.maps.Map,
@@ -55,6 +58,8 @@ export default class extends AbstractMapController<
5558

5659
declare map: google.maps.Map;
5760

61+
public parser: DOMParser;
62+
5863
async connect() {
5964
if (!_google) {
6065
_google = { maps: {} as typeof google.maps };
@@ -88,6 +93,7 @@ export default class extends AbstractMapController<
8893
}
8994

9095
super.connect();
96+
this.parser = new DOMParser();
9197
}
9298

9399
public centerValueChanged(): void {
@@ -139,7 +145,7 @@ export default class extends AbstractMapController<
139145
}: {
140146
definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;
141147
}): google.maps.marker.AdvancedMarkerElement {
142-
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
148+
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
143149

144150
const marker = new _google.maps.marker.AdvancedMarkerElement({
145151
position,
@@ -153,6 +159,10 @@ export default class extends AbstractMapController<
153159
this.createInfoWindow({ definition: infoWindow, element: marker });
154160
}
155161

162+
if (icon) {
163+
this.doCreateIcon({ definition: icon, element: marker });
164+
}
165+
156166
return marker;
157167
}
158168

@@ -297,6 +307,30 @@ export default class extends AbstractMapController<
297307
return content;
298308
}
299309

310+
protected doCreateIcon({
311+
definition,
312+
element,
313+
}: {
314+
definition: Icon;
315+
element: google.maps.marker.AdvancedMarkerElement;
316+
}): void {
317+
const { type, width, height } = definition;
318+
319+
if (type === IconTypes.Svg) {
320+
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
321+
} else if (type === IconTypes.UxIcon) {
322+
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
323+
} else if (type === IconTypes.Url) {
324+
const icon = document.createElement('img');
325+
icon.width = width;
326+
icon.height = height;
327+
icon.src = definition.url;
328+
element.content = icon;
329+
} else {
330+
throw new Error(`Unsupported icon type: ${type}.`);
331+
}
332+
}
333+
300334
private closeInfoWindowsExcept(infoWindow: google.maps.InfoWindow) {
301335
this.infoWindows.forEach((otherInfoWindow) => {
302336
if (otherInfoWindow !== infoWindow) {

src/Map/src/Bridge/Google/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"symfony/ux-map": "^2.19"
2222
},
2323
"require-dev": {
24-
"symfony/phpunit-bridge": "^6.4|^7.0"
24+
"symfony/phpunit-bridge": "^6.4|^7.0",
25+
"symfony/ux-icons": "^2.18"
2526
},
2627
"autoload": {
2728
"psr-4": { "Symfony\\UX\\Map\\Bridge\\Google\\": "src/" },

0 commit comments

Comments
 (0)