Skip to content

[Map] Add Marker Icon customization capability #2605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 2.24

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

## 2.23

Expand Down
24 changes: 24 additions & 0 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ export type Identifier = string;
export type WithIdentifier<T extends Record<string, unknown>> = T & {
'@id': Identifier;
};
export declare const IconTypes: {
readonly Url: "url";
readonly Svg: "svg";
readonly UxIcon: "ux-icon";
};
export type Icon = {
width: number;
height: number;
} & ({
type: typeof IconTypes.UxIcon;
name: string;
_generated_html: string;
} | {
type: typeof IconTypes.Url;
url: string;
} | {
type: typeof IconTypes.Svg;
html: string;
});
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
position: Point;
title: string | null;
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
icon?: Icon;
rawOptions?: MarkerOptions;
extra: Record<string, unknown>;
}>;
Expand Down Expand Up @@ -105,6 +125,10 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
}): InfoWindow;
protected abstract doCreateIcon({ definition, element, }: {
definition: Icon;
element: Marker;
}): void;
private createDrawingFactory;
private onDrawChanged;
}
7 changes: 6 additions & 1 deletion src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Controller } from '@hotwired/stimulus';

const IconTypes = {
Url: 'url',
Svg: 'svg',
UxIcon: 'ux-icon',
};
class default_1 extends Controller {
constructor() {
super(...arguments);
Expand Down Expand Up @@ -102,4 +107,4 @@ default_1.values = {
options: Object,
};

export { default_1 as default };
export { IconTypes, default_1 as default };
33 changes: 32 additions & 1 deletion src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,38 @@
import { Controller } from '@hotwired/stimulus';

export type Point = { lat: number; lng: number };

export type Identifier = string;
export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };

export const IconTypes = {
Url: 'url',
Svg: 'svg',
UxIcon: 'ux-icon',
} as const;
export type Icon = {
width: number;
height: number;
} & (
| {
type: typeof IconTypes.UxIcon;
name: string;
_generated_html: string;
}
| {
type: typeof IconTypes.Url;
url: string;
}
| {
type: typeof IconTypes.Svg;
html: string;
}
);

export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = WithIdentifier<{
position: Point;
title: string | null;
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
icon?: Icon;
/**
* Raw options passed to the marker constructor, specific to the map provider (e.g.: `L.marker()` for Leaflet).
*/
Expand Down Expand Up @@ -268,6 +292,13 @@ export default abstract class<
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
}): InfoWindow;
protected abstract doCreateIcon({
definition,
element,
}: {
definition: Icon;
element: Marker;
}): void;

//endregion

Expand Down
3 changes: 2 additions & 1 deletion src/Map/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"symfony/framework-bundle": "^6.4|^7.0",
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/twig-bundle": "^6.4|^7.0",
"symfony/ux-twig-component": "^2.18"
"symfony/ux-twig-component": "^2.18",
"symfony/ux-icons": "^2.18"
},
"conflict": {
"symfony/ux-twig-component": "<2.21"
Expand Down
8 changes: 8 additions & 0 deletions src/Map/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\UX\Map\Icon\UxIconRenderer;
use Symfony\UX\Map\Renderer\AbstractRendererFactory;
use Symfony\UX\Map\Renderer\Renderer;
use Symfony\UX\Map\Renderer\Renderers;
Expand All @@ -20,6 +21,7 @@
/*
* @author Hugo Alliaume <hugo@alliau.me>
*/

return static function (ContainerConfigurator $container): void {
$container->services()
->set('ux_map.renderers', Renderers::class)
Expand All @@ -28,10 +30,16 @@
abstract_arg('renderers configuration'),
])

->set('.ux_map.ux_icons.renderer', UxIconRenderer::class)
->args([
service('.ux_icons.icon_renderer')->nullOnInvalid(),
])

->set('ux_map.renderer_factory.abstract', AbstractRendererFactory::class)
->abstract()
->args([
service('stimulus.helper'),
service('.ux_map.ux_icons.renderer'),
])

->set('ux_map.renderer_factory', Renderer::class)
Expand Down
25 changes: 25 additions & 0 deletions src/Map/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ You can add markers to a map using the ``addMarker()`` method::
infoWindow: new InfoWindow(
headerContent: '<b>Lyon</b>',
content: 'The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.'
),
icon: new Icon(
content: '<svg>....</svg>'
icontType: 'html'
)
))

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

Add Marker icons
~~~~~~~~~~~~~~~~

.. versionadded:: 2.24

``Marker`` icon customization is available since UX Map 2.24.

A ``Marker`` can be customized with an ``Icon`` instance, which can either be an UX Icon, an URL, or a SVG content::

// 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
));

Remove elements from Map
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
7 changes: 6 additions & 1 deletion src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { LoaderOptions } from '@googlemaps/js-api-loader';
import AbstractMapController from '@symfony/ux-map';
import type { InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
import type { Icon, InfoWindowWithoutPositionDefinition, MarkerDefinition, Point, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
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> {
providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
map: google.maps.Map;
parser: DOMParser;
connect(): Promise<void>;
centerValueChanged(): void;
zoomValueChanged(): void;
Expand Down Expand Up @@ -32,6 +33,10 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
}): google.maps.InfoWindow;
protected doFitBoundsToMarkers(): void;
private createTextOrElement;
protected doCreateIcon({ definition, element, }: {
definition: Icon;
element: google.maps.marker.AdvancedMarkerElement;
}): void;
private closeInfoWindowsExcept;
}
export {};
31 changes: 30 additions & 1 deletion src/Map/src/Bridge/Google/assets/dist/map_controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Loader } from '@googlemaps/js-api-loader';
import { Controller } from '@hotwired/stimulus';

const IconTypes = {
Url: 'url',
Svg: 'svg',
UxIcon: 'ux-icon',
};
class default_1 extends Controller {
constructor() {
super(...arguments);
Expand Down Expand Up @@ -104,6 +109,7 @@ default_1.values = {
};

let _google;
const parser = new DOMParser();
class map_controller extends default_1 {
async connect() {
if (!_google) {
Expand All @@ -126,6 +132,7 @@ class map_controller extends default_1 {
});
}
super.connect();
this.parser = new DOMParser();
}
centerValueChanged() {
if (this.map && this.hasCenterValue && this.centerValue) {
Expand Down Expand Up @@ -158,7 +165,7 @@ class map_controller extends default_1 {
});
}
doCreateMarker({ definition, }) {
const { '@id': _id, position, title, infoWindow, extra, rawOptions = {}, ...otherOptions } = definition;
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
const marker = new _google.maps.marker.AdvancedMarkerElement({
position,
title,
Expand All @@ -169,6 +176,9 @@ class map_controller extends default_1 {
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, element: marker });
}
if (icon) {
this.doCreateIcon({ definition: icon, element: marker });
}
return marker;
}
doRemoveMarker(marker) {
Expand Down Expand Up @@ -272,6 +282,25 @@ class map_controller extends default_1 {
}
return content;
}
doCreateIcon({ definition, element, }) {
const { type, width, height } = definition;
if (type === IconTypes.Svg) {
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
}
else if (type === IconTypes.UxIcon) {
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
}
else if (type === IconTypes.Url) {
const icon = document.createElement('img');
icon.width = width;
icon.height = height;
icon.src = definition.url;
element.content = icon;
}
else {
throw new Error(`Unsupported icon type: ${type}.`);
}
}
closeInfoWindowsExcept(infoWindow) {
this.infoWindows.forEach((otherInfoWindow) => {
if (otherInfoWindow !== infoWindow) {
Expand Down
38 changes: 36 additions & 2 deletions src/Map/src/Bridge/Google/assets/src/map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

import type { LoaderOptions } from '@googlemaps/js-api-loader';
import { Loader } from '@googlemaps/js-api-loader';
import AbstractMapController from '@symfony/ux-map';
import AbstractMapController, { IconTypes } from '@symfony/ux-map';
import type {
Icon,
InfoWindowWithoutPositionDefinition,
MarkerDefinition,
Point,
Expand All @@ -36,6 +37,8 @@ type MapOptions = Pick<

let _google: typeof google;

const parser = new DOMParser();

export default class extends AbstractMapController<
MapOptions,
google.maps.Map,
Expand All @@ -55,6 +58,8 @@ export default class extends AbstractMapController<

declare map: google.maps.Map;

public parser: DOMParser;

async connect() {
if (!_google) {
_google = { maps: {} as typeof google.maps };
Expand Down Expand Up @@ -88,6 +93,7 @@ export default class extends AbstractMapController<
}

super.connect();
this.parser = new DOMParser();
}

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

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

if (icon) {
this.doCreateIcon({ definition: icon, element: marker });
}

return marker;
}

Expand Down Expand Up @@ -297,6 +307,30 @@ export default class extends AbstractMapController<
return content;
}

protected doCreateIcon({
definition,
element,
}: {
definition: Icon;
element: google.maps.marker.AdvancedMarkerElement;
}): void {
const { type, width, height } = definition;

if (type === IconTypes.Svg) {
element.content = parser.parseFromString(definition.html, 'image/svg+xml').documentElement;
} else if (type === IconTypes.UxIcon) {
element.content = parser.parseFromString(definition._generated_html, 'image/svg+xml').documentElement;
} else if (type === IconTypes.Url) {
const icon = document.createElement('img');
icon.width = width;
icon.height = height;
icon.src = definition.url;
element.content = icon;
} else {
throw new Error(`Unsupported icon type: ${type}.`);
}
}

private closeInfoWindowsExcept(infoWindow: google.maps.InfoWindow) {
this.infoWindows.forEach((otherInfoWindow) => {
if (otherInfoWindow !== infoWindow) {
Expand Down
3 changes: 2 additions & 1 deletion src/Map/src/Bridge/Google/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"symfony/ux-map": "^2.19"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.4|^7.0"
"symfony/phpunit-bridge": "^6.4|^7.0",
"symfony/ux-icons": "^2.18"
},
"autoload": {
"psr-4": { "Symfony\\UX\\Map\\Bridge\\Google\\": "src/" },
Expand Down
Loading
Loading