Skip to content

Commit

Permalink
feat: custom icon on markers
Browse files Browse the repository at this point in the history
  • Loading branch information
sblondeau committed Feb 25, 2025
1 parent 6d2daab commit 721e596
Show file tree
Hide file tree
Showing 20 changed files with 460 additions and 409 deletions.
1 change: 1 addition & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2.23

- Add marker `Icon` support (image and svg)
- Add `DistanceUnit` to represent distance units (`m`, `km`, `miles`, `nmi`) and
ease conversion between units.
- Add `DistanceCalculatorInterface` interface and three implementations:
Expand Down
11 changes: 11 additions & 0 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ export type Point = {
lat: number;
lng: number;
};
export type Icon = {
content: string;
iconType: string;
width: number;
height: number;
};
export type Identifier = string;
export type WithIdentifier<T extends Record<string, unknown>> = T & {
'@id': Identifier;
Expand All @@ -11,6 +17,7 @@ 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 +112,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;
}): Icon;
private createDrawingFactory;
private onDrawChanged;
}
12 changes: 11 additions & 1 deletion src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { Controller } from '@hotwired/stimulus';

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

export type Icon = {
content: string;
iconType: string;
width: number;
height: number;
};
export type Identifier = string;
export type WithIdentifier<T extends Record<string, unknown>> = T & { '@id': Identifier };

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 +274,10 @@ export default abstract class<
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
}): InfoWindow;
protected abstract doCreateIcon({ definition, element }: {
definition: Icon;
element: Marker;
}): Icon;

//endregion

Expand Down
52 changes: 33 additions & 19 deletions src/Map/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,56 @@

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\UX\Map\Renderer\AbstractRendererFactory;
use Symfony\UX\Map\Icon;
use Symfony\UX\Map\IconInterface;
use Symfony\UX\Icons\IconRenderer;
use Symfony\UX\Map\Twig\MapRuntime;
use Symfony\UX\Map\Renderer\Renderer;
use Symfony\UX\Map\Renderer\Renderers;
use Symfony\UX\Map\Twig\MapExtension;
use Symfony\UX\Map\Twig\MapRuntime;
use Symfony\UX\Map\Renderer\Renderers;
use Symfony\UX\Icons\IconRendererInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\UX\Map\Icon\IconFactory;
use Symfony\UX\Map\Icon\IconFactoryInterface;
use Symfony\UX\Map\Renderer\AbstractRendererFactory;

/*
* @author Hugo Alliaume <[email protected]>
*/

return static function (ContainerConfigurator $container): void {
$container->services()
->set('ux_map.renderers', Renderers::class)
->factory([service('ux_map.renderer_factory'), 'fromStrings'])
->args([
abstract_arg('renderers configuration'),
])
->factory([service('ux_map.renderer_factory'), 'fromStrings'])
->args([
abstract_arg('renderers configuration'),
])

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

->set('ux_map.renderer_factory', Renderer::class)
->args([
tagged_iterator('ux_map.renderer_factory'),
])
->args([
tagged_iterator('ux_map.renderer_factory'),
])

->set('ux_map.twig_extension', MapExtension::class)
->tag('twig.extension')
->tag('twig.extension')

->set('ux_map.twig_runtime', MapRuntime::class)
->args([
service('ux_map.renderers'),
])
->tag('twig.runtime')
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:map'])

->alias(IconFactoryInterface::class, IconFactory::class)
->set(IconFactory::class)
->args([
service('ux_map.renderers'),
service('.ux_icons.icon_renderer')->ignoreOnInvalid()
])
->tag('twig.runtime')
->tag('ux.twig_component.twig_renderer', ['key' => 'ux:map'])
;
;
};
40 changes: 40 additions & 0 deletions src/Map/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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 @@ -136,6 +140,42 @@ You can add markers to a map using the ``addMarker()`` method::
))
;

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

It is possible to add icon to Marker::

new Marker(
position: new Point(45.7640, 4.8357),
title: 'Lyon',
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'
)
))

You can add image icon (type 'url')::

new Icon (
content: 'https://my-image.png',
iconType: 'url',
width: 24, // default
height: 24, // default
)

or SVG icon (type 'html')::

new Icon (
content: '<svg>...</svg>',
iconType: 'html',
width: 36,
height: 36,
)

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

Expand Down
4 changes: 4 additions & 0 deletions src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
}): google.maps.InfoWindow;
protected doFitBoundsToMarkers(): void;
private createTextOrElement;
protected doCreateIcon({ definition, element }: {
definition: any;
element: any;
}): void;
private closeInfoWindowsExcept;
}
export {};
126 changes: 21 additions & 105 deletions src/Map/src/Bridge/Google/assets/dist/map_controller.js
Original file line number Diff line number Diff line change
@@ -1,110 +1,8 @@
import { Loader } from '@googlemaps/js-api-loader';
import { Controller } from '@hotwired/stimulus';

class default_1 extends Controller {
constructor() {
super(...arguments);
this.markers = new Map();
this.polygons = new Map();
this.polylines = new Map();
this.infoWindows = [];
this.isConnected = false;
}
connect() {
const options = this.optionsValue;
this.dispatchEvent('pre-connect', { options });
this.createMarker = this.createDrawingFactory('marker', this.markers, this.doCreateMarker.bind(this));
this.createPolygon = this.createDrawingFactory('polygon', this.polygons, this.doCreatePolygon.bind(this));
this.createPolyline = this.createDrawingFactory('polyline', this.polylines, this.doCreatePolyline.bind(this));
this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
options,
});
this.markersValue.forEach((definition) => this.createMarker({ definition }));
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
this.dispatchEvent('connect', {
map: this.map,
markers: [...this.markers.values()],
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
infoWindows: this.infoWindows,
});
this.isConnected = true;
}
createInfoWindow({ definition, element, }) {
this.dispatchEvent('info-window:before-create', { definition, element });
const infoWindow = this.doCreateInfoWindow({ definition, element });
this.dispatchEvent('info-window:after-create', { infoWindow, element });
this.infoWindows.push(infoWindow);
return infoWindow;
}
markersValueChanged() {
if (!this.isConnected) {
return;
}
this.onDrawChanged(this.markers, this.markersValue, this.createMarker, this.doRemoveMarker);
if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
}
polygonsValueChanged() {
if (!this.isConnected) {
return;
}
this.onDrawChanged(this.polygons, this.polygonsValue, this.createPolygon, this.doRemovePolygon);
}
polylinesValueChanged() {
if (!this.isConnected) {
return;
}
this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
}
createDrawingFactory(type, draws, factory) {
const eventBefore = `${type}:before-create`;
const eventAfter = `${type}:after-create`;
return ({ definition }) => {
this.dispatchEvent(eventBefore, { definition });
const drawing = factory({ definition });
this.dispatchEvent(eventAfter, { [type]: drawing });
draws.set(definition['@id'], drawing);
return drawing;
};
}
onDrawChanged(draws, newDrawDefinitions, factory, remover) {
const idsToRemove = new Set(draws.keys());
newDrawDefinitions.forEach((definition) => {
idsToRemove.delete(definition['@id']);
});
idsToRemove.forEach((id) => {
const draw = draws.get(id);
remover(draw);
draws.delete(id);
});
newDrawDefinitions.forEach((definition) => {
if (!draws.has(definition['@id'])) {
factory({ definition });
}
});
}
}
default_1.values = {
providerOptions: Object,
center: Object,
zoom: Number,
fitBoundsToMarkers: Boolean,
markers: Array,
polygons: Array,
polylines: Array,
options: Object,
};
import AbstractMapController from '@symfony/ux-map';

let _google;
class map_controller extends default_1 {
class map_controller extends AbstractMapController {
async connect() {
if (!_google) {
_google = { maps: {} };
Expand Down Expand Up @@ -158,7 +56,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 +67,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 +173,21 @@ class map_controller extends default_1 {
}
return content;
}
doCreateIcon({ definition, element }) {
const { content, iconType, width, height } = definition;
if (iconType === 'html') {
const parser = new DOMParser();
const icon = parser.parseFromString(content, "image/svg+xml").documentElement;
element.content = icon;
}
else {
const icon = document.createElement('img');
icon.width = width.toString();
icon.height = height.toString();
icon.src = content;
element.content = icon;
}
}
closeInfoWindowsExcept(infoWindow) {
this.infoWindows.forEach((otherInfoWindow) => {
if (otherInfoWindow !== infoWindow) {
Expand Down
Loading

0 comments on commit 721e596

Please sign in to comment.