Skip to content

Commit 704617a

Browse files
committed
feat: custom icon on markers
1 parent 823b87c commit 704617a

27 files changed

+435
-17
lines changed

src/Map/CHANGELOG.md

+1
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

+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+
type: 'url' | 'inline-svg' | 'ux-icon';
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+
}): void;
108119
private createDrawingFactory;
109120
private onDrawChanged;
110121
}

src/Map/assets/src/abstract_map_controller.ts

+14-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+
type: 'url' | 'inline-svg' | 'ux-icon';
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,13 @@ export default abstract class<
268274
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
269275
element: Marker | Polygon | Polyline;
270276
}): InfoWindow;
277+
protected abstract doCreateIcon({
278+
definition,
279+
element,
280+
}: {
281+
definition: Icon;
282+
element: Marker;
283+
}): void;
271284

272285
//endregion
273286

src/Map/config/services.php

+8
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')->nullOnInvalid(),
3543
])
3644

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

src/Map/doc/index.rst

+21
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,23 @@ 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+
``Marker`` icon customizatisation is available since UX Map 2.24.
140+
When returning a Map, it's quite common to want to customize ``Marker`` icon. It is possible thanks to ``icon`` parameter and ``Icon`` class::
141+
// It can be a UX Icon (requires `symfony/ux-icons`)
142+
$icon = Icon::fromUxIcon('fa:map-marker');
143+
// Or an URL pointing to an image
144+
$icon = Icon::fromUrl('https://example.com/marker.png');
145+
// Or a plain SVG content
146+
$icon = Icon::fromInlineSVG('<svg>(...)</svg>');
147+
new Marker(
148+
// ...
149+
icon: $icon
150+
))
151+
131152
Remove elements from Map
132153
~~~~~~~~~~~~~~~~~~~~~~~~
133154

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

+6-1
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

+19-1
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class map_controller extends default_1 {
126126
});
127127
}
128128
super.connect();
129+
this.parser = new DOMParser();
129130
}
130131
centerValueChanged() {
131132
if (this.map && this.hasCenterValue && this.centerValue) {
@@ -158,7 +159,7 @@ class map_controller extends default_1 {
158159
});
159160
}
160161
doCreateMarker({ definition, }) {
161-
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
162+
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
162163
const marker = new _google.maps.marker.AdvancedMarkerElement({
163164
position,
164165
title,
@@ -169,6 +170,9 @@ class map_controller extends default_1 {
169170
if (infoWindow) {
170171
this.createInfoWindow({ definition: infoWindow, element: marker });
171172
}
173+
if (icon) {
174+
this.doCreateIcon({ definition: icon, element: marker });
175+
}
172176
return marker;
173177
}
174178
doRemoveMarker(marker) {
@@ -272,6 +276,20 @@ class map_controller extends default_1 {
272276
}
273277
return content;
274278
}
279+
doCreateIcon({ definition, element, }) {
280+
const { content, type, width, height } = definition;
281+
if (type === 'inline-svg') {
282+
const icon = this.parser.parseFromString(content, 'image/svg+xml').documentElement;
283+
element.content = icon;
284+
}
285+
else {
286+
const icon = document.createElement('img');
287+
icon.width = width;
288+
icon.height = height;
289+
icon.src = content;
290+
element.content = icon;
291+
}
292+
}
275293
closeInfoWindowsExcept(infoWindow) {
276294
this.infoWindows.forEach((otherInfoWindow) => {
277295
if (otherInfoWindow !== infoWindow) {

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

+29-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { LoaderOptions } from '@googlemaps/js-api-loader';
1111
import { Loader } from '@googlemaps/js-api-loader';
1212
import AbstractMapController from '@symfony/ux-map';
1313
import type {
14+
Icon,
1415
InfoWindowWithoutPositionDefinition,
1516
MarkerDefinition,
1617
Point,
@@ -55,6 +56,8 @@ export default class extends AbstractMapController<
5556

5657
declare map: google.maps.Map;
5758

59+
public parser: DOMParser;
60+
5861
async connect() {
5962
if (!_google) {
6063
_google = { maps: {} as typeof google.maps };
@@ -88,6 +91,7 @@ export default class extends AbstractMapController<
8891
}
8992

9093
super.connect();
94+
this.parser = new DOMParser();
9195
}
9296

9397
public centerValueChanged(): void {
@@ -139,7 +143,7 @@ export default class extends AbstractMapController<
139143
}: {
140144
definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>;
141145
}): google.maps.marker.AdvancedMarkerElement {
142-
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
146+
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
143147

144148
const marker = new _google.maps.marker.AdvancedMarkerElement({
145149
position,
@@ -153,6 +157,10 @@ export default class extends AbstractMapController<
153157
this.createInfoWindow({ definition: infoWindow, element: marker });
154158
}
155159

160+
if (icon) {
161+
this.doCreateIcon({ definition: icon, element: marker });
162+
}
163+
156164
return marker;
157165
}
158166

@@ -297,6 +305,26 @@ export default class extends AbstractMapController<
297305
return content;
298306
}
299307

308+
protected doCreateIcon({
309+
definition,
310+
element,
311+
}: {
312+
definition: Icon;
313+
element: google.maps.marker.AdvancedMarkerElement;
314+
}): void {
315+
const { content, type, width, height } = definition;
316+
if (type === 'inline-svg') {
317+
const icon = this.parser.parseFromString(content, 'image/svg+xml').documentElement;
318+
element.content = icon;
319+
} else {
320+
const icon = document.createElement('img');
321+
icon.width = width;
322+
icon.height = height;
323+
icon.src = content;
324+
element.content = icon;
325+
}
326+
}
327+
300328
private closeInfoWindowsExcept(infoWindow: google.maps.InfoWindow) {
301329
this.infoWindows.forEach((otherInfoWindow) => {
302330
if (otherInfoWindow !== infoWindow) {

src/Map/src/Bridge/Google/src/Renderer/GoogleRenderer.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\UX\Map\Bridge\Google\Renderer;
1313

1414
use Symfony\UX\Map\Bridge\Google\GoogleOptions;
15+
use Symfony\UX\Map\Icon\UxIconRenderer;
1516
use Symfony\UX\Map\MapOptionsInterface;
1617
use Symfony\UX\Map\Renderer\AbstractRenderer;
1718
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
@@ -42,8 +43,9 @@ public function __construct(
4243
*/
4344
private array $libraries = [],
4445
private ?string $defaultMapId = null,
46+
?UxIconRenderer $uxIconRenderer = null,
4547
) {
46-
parent::__construct($stimulusHelper);
48+
parent::__construct($stimulusHelper, $uxIconRenderer);
4749
}
4850

4951
protected function getName(): string

src/Map/src/Bridge/Google/src/Renderer/GoogleRendererFactory.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\UX\Map\Exception\InvalidArgumentException;
1515
use Symfony\UX\Map\Exception\UnsupportedSchemeException;
16+
use Symfony\UX\Map\Icon\UxIconRenderer;
1617
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
1718
use Symfony\UX\Map\Renderer\Dsn;
1819
use Symfony\UX\Map\Renderer\RendererFactoryInterface;
@@ -26,9 +27,10 @@ final class GoogleRendererFactory extends AbstractRendererFactory implements Ren
2627
{
2728
public function __construct(
2829
StimulusHelper $stimulus,
30+
?UxIconRenderer $uxIconRenderer = null,
2931
private ?string $defaultMapId = null,
3032
) {
31-
parent::__construct($stimulus);
33+
parent::__construct($stimulus, $uxIconRenderer);
3234
}
3335

3436
public function create(Dsn $dsn): RendererInterface
@@ -51,6 +53,7 @@ public function create(Dsn $dsn): RendererInterface
5153
version: $dsn->getOption('version', 'weekly'),
5254
libraries: ['maps', 'marker', ...$dsn->getOption('libraries', [])],
5355
defaultMapId: $this->defaultMapId,
56+
uxIconRenderer: $this->uxIconRenderer,
5457
);
5558
}
5659

0 commit comments

Comments
 (0)