Skip to content

Add color to marker #2770

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

Open
JeDeveloppe opened this issue May 24, 2025 · 7 comments
Open

Add color to marker #2770

JeDeveloppe opened this issue May 24, 2025 · 7 comments
Labels

Comments

@JeDeveloppe
Copy link

Hello,

Is it possible to change the color of a marker when using a Symfony UX icon?

Thank you.

@smnandre
Copy link
Member

You can, by passing your own icon / svg, either directly in the good color, or you can add a tiny bit of CSS

@JeDeveloppe
Copy link
Author

Is it not a way easy like this ?
$icon = Icon::ux('tabler:truck-filled')->width(22)->height(22)->color(#FF5733)

@JeDeveloppe
Copy link
Author

JeDeveloppe commented May 24, 2025

In a loop it would be nice to be able to assign a different color per entity, I tried to change the code a little, without success, can you help me? Thank you

In the Icon.php file I add some params:

/**
 * Sets the color of the icon.
 *
 * @param non-empty-string $color
 */
public function color(string $color): static
{
    $this->color = $color;

    return $this;
}

/**
 * @internal
 */
public function toArray(): array
{
    return [
        'type' => $this->type->value,
        'width' => $this->width,
        'height' => $this->height,
        'color' => $this->color,
    ];
}

/**
 * @param array{ type: value-of<IconType>, width: positive-int, height: positive-int, color: non-empty-string }
 *     &(array{ url: non-empty-string }
 *      |array{ html: non-empty-string }
 *      |array{ name: non-empty-string }) $data
 *
 * @internal
 */
public static function fromArray(array $data): static
{
    return match ($data['type']) {
        IconType::Url->value => UrlIcon::fromArray($data),
        IconType::Svg->value => SvgIcon::fromArray($data),
        IconType::UxIcon->value => UxIcon::fromArray($data),
        default => throw new InvalidArgumentException(\sprintf('Invalid icon type %s.', $data['type'])),
    };
}

Without success...

@smnandre
Copy link
Member

Would you like to work on a PR ?

@JeDeveloppe
Copy link
Author

JeDeveloppe commented May 24, 2025

Euh what is PR ?

Edit: I don't think I have the skill to do this, but I'd like to try :)

@smnandre
Copy link
Member

A Pull Request.. meaning you try to code this (we can assist --when we have time 😅 ) and we then integrate the feature for everyone ?

You could, to start, look at the documentation: https://symfony.com/doc/current/contributing/code/pull_requests.html and/or this great tutorial from symfonycast : https://symfonycasts.com/screencast/contributing/submitting-pr

@JeDeveloppe
Copy link
Author

Sorry I don't know how to do PR, but I write code with a little bit of IA...

//vendor/symfony/ux-leaflet-map/assets/dist/map_controller.fs
import { Controller } from '@hotwired/stimulus';
import 'leaflet/dist/leaflet.min.css';
import * as L from 'leaflet';
const IconTypes = {
Url: 'url',
Svg: 'svg',
UxIcon: 'ux-icon',
};
// Classe parente fournie par Symfony UX Map
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,
};
// Votre contrôleur de carte personnalisé étendant la classe parente
class map_controller extends default_1 {
connect() {
// Le L.Marker.prototype.options.icon par défaut est commenté.
// Cela permet à chaque marqueur d'avoir son icône définie individuellement,
// y compris avec des couleurs personnalisées, ou d'utiliser l'icône par défaut
// que nous gérons dans doCreateMarker si aucune icône n'est spécifiée.
// L.Marker.prototype.options.icon = L.divIcon({
// html: '',
// iconSize: [25, 41],
// iconAnchor: [12.5, 41],
// popupAnchor: [0, -41],
// className: '',
// });
super.connect();
}
centerValueChanged() {
if (this.map && this.hasCenterValue && this.centerValue && this.hasZoomValue && this.zoomValue) {
this.map.setView(this.centerValue, this.zoomValue);
}
}
zoomValueChanged() {
if (this.map && this.hasZoomValue && this.zoomValue) {
this.map.setZoom(this.zoomValue);
}
}
dispatchEvent(name, payload = {}) {
this.dispatch(name, {
prefix: 'ux:map',
detail: {
...payload,
L,
},
});
}
doCreateMap({ center, zoom, options, }) {
const map = L.map(this.element, {
...options,
center: center === null ? undefined : center,
zoom: zoom === null ? undefined : zoom,
});
L.tileLayer(options.tileLayer.url, {
attribution: options.tileLayer.attribution,
...options.tileLayer.options,
}).addTo(map);
return map;
}
doCreateMarker({ definition }) {
const { '@id': _id, position, title, infoWindow, icon, extra, rawOptions = {}, ...otherOptions } = definition;
// Crée le marqueur sans icône pour l'instant
const marker = L.marker(position, { title: title || undefined, ...otherOptions, ...rawOptions });
if (icon) {
// Si une icône est définie, on utilise notre fonction pour la créer et l'appliquer
this.doCreateIcon({ definition: icon, element: marker });
} else {
// Si aucune icône n'est spécifiée, on applique une icône par défaut
// C'est le SVG par défaut de Symfony UX Map. Vous pouvez le modifier ou le remplacer.
const defaultIconHtml = '';
marker.setIcon(L.divIcon({
html: defaultIconHtml,
iconSize: [25, 41],
iconAnchor: [12.5, 41],
popupAnchor: [0, -41],
className: '',
}));
}
// Ajoute le marqueur à la carte après avoir défini son icône
marker.addTo(this.map);
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, element: marker });
}
return marker;
}
doRemoveMarker(marker) {
marker.remove();
}
doCreatePolygon({ definition, }) {
const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
const polygon = L.polygon(points, { ...rawOptions }).addTo(this.map);
if (title) {
polygon.bindPopup(title);
}
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, element: polygon });
}
return polygon;
}
doRemovePolygon(polygon) {
polygon.remove();
}
doCreatePolyline({ definition, }) {
const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
if (title) {
polyline.bindPopup(title);
}
if (infoWindow) {
this.createInfoWindow({ definition: infoWindow, element: polyline });
}
return polyline;
}
doRemovePolyline(polyline) {
polyline.remove();
}
doCreateInfoWindow({ definition, element, }) {
const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
element.bindPopup([headerContent, content].filter((x) => x).join('
'), { ...otherOptions, ...rawOptions });
if (definition.opened) {
element.openPopup();
}
const popup = element.getPopup();
if (!popup) {
throw new Error('Unable to get the Popup associated with the element.');
}
return popup;
}
/**
* Crée une icône Leaflet et l'applique à un élément (généralement un marqueur).
* Gère la coloration des icônes SVG/UxIcon via la propriété color.
*/
doCreateIcon({ definition, element, }) {
const { type, width, height, color, url, html, _generated_html } = definition;
let iconHtml = '';
let icon;
if (type === IconTypes.Svg) {
iconHtml = html;
} else if (type === IconTypes.UxIcon) {
iconHtml = _generated_html;
} else if (type === IconTypes.Url) {
// Pour les icônes de type URL, la couleur n'est pas directement applicable via la modification du SVG.
// Vous devrez fournir des images d'icônes pré-colorées ou gérer la coloration via CSS si possible.
icon = L.icon({
iconUrl: url,
iconSize: [width, height],
className: '',
});
element.setIcon(icon);
return; // Sortir après avoir défini l'icône URL
} else {
throw new Error(Unsupported icon type: ${type}.);
}
// Si une couleur est fournie et que l'icône est un SVG ou un UxIcon,
// nous allons tenter de remplacer les couleurs de remplissage et de contour dans le SVG.
if (color && (type === IconTypes.Svg || type === IconTypes.UxIcon)) {
// Remplace les couleurs des dégradés (souvent utilisés dans les SVGs de Symfony UX Map)
iconHtml = iconHtml.replace(/stop-color="[^"]+"/g, stop-color="${color}");
// Remplace les couleurs de remplissage directes
iconHtml = iconHtml.replace(/fill="[^"]+"/g, fill="${color}");
// Remplace les couleurs de contour directes
iconHtml = iconHtml.replace(/stroke="[^"]+"/g, stroke="${color}");
// Cas spécifique pour les ID de dégradé si vous utilisez le SVG par défaut de UX Map
// Si vos SVGs ont des ID de dégradé différents, ces remplacements devront être ajustés.
iconHtml = iconHtml.replace(/id="__sf_ux_map_gradient_marker_fill"/g, id="__sf_ux_map_gradient_marker_fill_${color.replace('#', '')}");
iconHtml = iconHtml.replace(/id="__sf_ux_map_gradient_marker_border"/g, id="__sf_ux_map_gradient_marker_border_${color.replace('#', '')}");
iconHtml = iconHtml.replace(/url(#__sf_ux_map_gradient_marker_fill)/g, url(#__sf_ux_map_gradient_marker_fill_${color.replace('#', '')}));
iconHtml = iconHtml.replace(/url(#__sf_ux_map_gradient_marker_border)/g, url(#__sf_ux_map_gradient_marker_border_${color.replace('#', '')}));
}
icon = L.divIcon({
html: iconHtml,
iconSize: [width, height],
iconAnchor: [width / 2, height], // Ajustement de l'ancre pour centrer l'icône en bas
popupAnchor: [0, -height], // Ajustement de l'ancre du popup pour qu'il s'affiche au-dessus de l'icône
className: '',
});
element.setIcon(icon);
}
doFitBoundsToMarkers() {
if (this.markers.size === 0) {
return;
}
const bounds = [];
this.markers.forEach((marker) => {
const position = marker.getLatLng();
bounds.push([position.lat, position.lng]);
});
this.map.fitBounds(bounds);
}
}
export { map_controller as default };


And...

* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\UX\Map\Icon; use Symfony\UX\Map\Exception\InvalidArgumentException; /** * Represents an icon that can be displayed on a map marker. * * @author Sylvain Blondeau * @author Hugo Alliaume */ abstract class Icon { /** * Creates a new icon based on a URL (e.g.: `https://cdn.jsdelivr.net/npm/[email protected]/icons/geo-alt.svg`). * * @param non-empty-string $url */ public static function url(string $url): UrlIcon { return new UrlIcon($url); } /** * Creates a new icon based on an SVG string (e.g.: `...`). * Using an SVG string may not be the best option if you want to customize the icon afterward, * it would be preferable to use {@see Icon::ux()} or {@see Icon::url()} instead. * * @param non-empty-string $html */ public static function svg(string $html): SvgIcon { return new SvgIcon($html); } /** * Creates a new icon based on a UX icon name (e.g.: `fa:map-marker`). * * @param non-empty-string $name */ public static function ux(string $name): UxIcon { return new UxIcon($name); } /** * @param positive-int $width * @param positive-int $height */ protected function __construct( protected IconType $type, protected int $width = 24, protected int $height = 24, protected ?string $color = null, // Nouvelle propriété pour la couleur ) { } /** * Sets the width of the icon. * * @param positive-int $width */ public function width(int $width): static { $this->width = $width; return $this; } /** * Sets the height of the icon. * * @param positive-int $height */ public function height(int $height): static { $this->height = $height; return $this; } /** * Sets the color of the icon. * Note: This only has an effect on SvgIcon and UxIcon types when rendered client-side by the JavaScript controller. * The color should be a valid CSS color string (e.g., 'red', '#FF0000', 'rgb(255,0,0)'). * * @param non-empty-string $color */ public function color(string $color): static { $this->color = $color; return $this; } /** * @internal */ public function toArray(): array { return [ 'type' => $this->type->value, 'width' => $this->width, 'height' => $this->height, 'color' => $this->color, // Ajout de la couleur au tableau de sérialisation ]; } /** * @param array{ type: value-of, width: positive-int, height: positive-int, color?: string } * &(array{ url: non-empty-string } * |array{ html: non-empty-string } * |array{ name: non-empty-string }) $data * * @internal */ public static function fromArray(array $data): static { return match ($data['type']) { IconType::Url->value => UrlIcon::fromArray($data), IconType::Svg->value => SvgIcon::fromArray($data), IconType::UxIcon->value => UxIcon::fromArray($data), default => throw new InvalidArgumentException(\sprintf('Invalid icon type %s.', $data['type'])), }; } } - - - - - - - - - - - - - - - - - - - - - Work for me when I do: `$iconOfTechnician = Icon::ux('ri:taxi-wifi-fill')->width(24)->height(24)->color('#0029D2');` Now on a map, thanks to a foreach loop, each Entity has a different color, this works very well. If someone can try and make the pull request themselves... it might help others. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants