Skip to content

[Map] Add Circle support #2838

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 1 commit into from
Jun 13, 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
9 changes: 9 additions & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# CHANGELOG

## 2.28

- Add support for creating `Circle` by passing a `Point` and a radius (in meters) to the `Circle` constructor, e.g.:
```php
$map->addCircle(new Circle(
center: new Point(48.856613, 2.352222), // Paris
radius: 5_000 // 5km
));
```
## 2.27

- The `fitBoundsToMarkers` option is not overridden anymore when using the `Map` LiveComponent, but now respects the value you defined.
Expand Down
24 changes: 21 additions & 3 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = WithIdentif
rawOptions?: PolylineOptions;
extra: Record<string, unknown>;
}>;
export type CircleDefinition<CircleOptions, InfoWindowOptions> = WithIdentifier<{
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
center: Point;
radius: number;
title: string | null;
rawOptions?: CircleOptions;
extra: Record<string, unknown>;
}>;
export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
Expand All @@ -58,7 +66,7 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
extra: Record<string, unknown>;
};
export type InfoWindowWithoutPositionDefinition<InfoWindowOptions> = Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline> extends Controller<HTMLElement> {
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline, CircleOptions, Circle> extends Controller<HTMLElement> {
static values: {
providerOptions: ObjectConstructor;
center: ObjectConstructor;
Expand All @@ -67,6 +75,7 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
markers: ArrayConstructor;
polygons: ArrayConstructor;
polylines: ArrayConstructor;
circles: ArrayConstructor;
options: ObjectConstructor;
};
centerValue: Point | null;
Expand All @@ -75,34 +84,39 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
circlesValue: Array<CircleDefinition<CircleOptions, InfoWindowOptions>>;
optionsValue: MapOptions;
hasCenterValue: boolean;
hasZoomValue: boolean;
hasFitBoundsToMarkersValue: boolean;
hasMarkersValue: boolean;
hasPolygonsValue: boolean;
hasPolylinesValue: boolean;
hasCirclesValue: boolean;
hasOptionsValue: boolean;
protected map: Map;
protected markers: globalThis.Map<string, Marker>;
protected polygons: globalThis.Map<string, Polygon>;
protected polylines: globalThis.Map<string, Polyline>;
protected circles: globalThis.Map<string, Circle>;
protected infoWindows: Array<InfoWindow>;
private isConnected;
private createMarker;
private createPolygon;
private createPolyline;
private createCircle;
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
connect(): void;
createInfoWindow({ definition, element, }: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
element: Marker | Polygon | Polyline | Circle;
}): InfoWindow;
abstract centerValueChanged(): void;
abstract zoomValueChanged(): void;
markersValueChanged(): void;
polygonsValueChanged(): void;
polylinesValueChanged(): void;
circlesValueChanged(): void;
protected abstract doCreateMap({ center, zoom, options, }: {
center: Point | null;
zoom: number | null;
Expand All @@ -121,9 +135,13 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>;
}): Polyline;
protected abstract doRemovePolyline(polyline: Polyline): void;
protected abstract doCreateCircle({ definition, }: {
definition: CircleDefinition<CircleOptions, InfoWindowOptions>;
}): Circle;
protected abstract doRemoveCircle(circle: Circle): void;
protected abstract doCreateInfoWindow({ definition, element, }: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
element: Marker | Polygon | Polyline | Circle;
}): InfoWindow;
protected abstract doCreateIcon({ definition, element, }: {
definition: Icon;
Expand Down
11 changes: 11 additions & 0 deletions src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class default_1 extends Controller {
this.markers = new Map();
this.polygons = new Map();
this.polylines = new Map();
this.circles = new Map();
this.infoWindows = [];
this.isConnected = false;
}
Expand All @@ -20,6 +21,7 @@ class default_1 extends Controller {
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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this));
this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
Expand All @@ -28,6 +30,7 @@ class default_1 extends Controller {
this.markersValue.forEach((definition) => this.createMarker({ definition }));
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
this.circlesValue.forEach((definition) => this.createCircle({ definition }));
if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
Expand All @@ -36,6 +39,7 @@ class default_1 extends Controller {
markers: [...this.markers.values()],
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
circles: [...this.circles.values()],
infoWindows: this.infoWindows,
});
this.isConnected = true;
Expand Down Expand Up @@ -68,6 +72,12 @@ class default_1 extends Controller {
}
this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
}
circlesValueChanged() {
if (!this.isConnected) {
return;
}
this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle);
}
createDrawingFactory(type, draws, factory) {
const eventBefore = `${type}:before-create`;
const eventAfter = `${type}:after-create`;
Expand Down Expand Up @@ -104,6 +114,7 @@ default_1.values = {
markers: Array,
polygons: Array,
polylines: Array,
circles: Array,
options: Object,
};

Expand Down
69 changes: 65 additions & 4 deletions src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = WithIdentif
extra: Record<string, unknown>;
}>;

export type CircleDefinition<CircleOptions, InfoWindowOptions> = WithIdentifier<{
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
center: Point;
radius: number;
title: string | null;
/**
* Raw options passed to the circle constructor, specific to the map provider (e.g.: `L.circle()` for Leaflet).
*/
rawOptions?: CircleOptions;
/**
* Extra data defined by the developer.
* They are not directly used by the Stimulus controller, but they can be used by the developer with event listeners:
* - `ux:map:circle:before-create`
* - `ux:map:circle:after-create`
*/
extra: Record<string, unknown>;
}>;

export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
Expand Down Expand Up @@ -116,6 +134,8 @@ export default abstract class<
Polygon,
PolylineOptions,
Polyline,
CircleOptions,
Circle,
> extends Controller<HTMLElement> {
static values = {
providerOptions: Object,
Expand All @@ -125,6 +145,7 @@ export default abstract class<
markers: Array,
polygons: Array,
polylines: Array,
circles: Array,
options: Object,
};

Expand All @@ -134,6 +155,7 @@ export default abstract class<
declare markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
declare polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
declare polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
declare circlesValue: Array<CircleDefinition<CircleOptions, InfoWindowOptions>>;
declare optionsValue: MapOptions;

declare hasCenterValue: boolean;
Expand All @@ -142,12 +164,14 @@ export default abstract class<
declare hasMarkersValue: boolean;
declare hasPolygonsValue: boolean;
declare hasPolylinesValue: boolean;
declare hasCirclesValue: boolean;
declare hasOptionsValue: boolean;

protected map: Map;
protected markers = new Map<Identifier, Marker>();
protected polygons = new Map<Identifier, Polygon>();
protected polylines = new Map<Identifier, Polyline>();
protected circles = new Map<Identifier, Circle>();
protected infoWindows: Array<InfoWindow> = [];

private isConnected = false;
Expand All @@ -160,6 +184,9 @@ export default abstract class<
private createPolyline: ({
definition,
}: { definition: PolylineDefinition<PolylineOptions, InfoWindowOptions> }) => Polyline;
private createCircle: ({
definition,
}: { definition: CircleDefinition<CircleOptions, InfoWindowOptions> }) => Circle;

protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;

Expand All @@ -171,6 +198,7 @@ export default abstract class<
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.createCircle = this.createDrawingFactory('circle', this.circles, this.doCreateCircle.bind(this));

this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
Expand All @@ -180,6 +208,7 @@ export default abstract class<
this.markersValue.forEach((definition) => this.createMarker({ definition }));
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
this.circlesValue.forEach((definition) => this.createCircle({ definition }));

if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
Expand All @@ -190,6 +219,7 @@ export default abstract class<
markers: [...this.markers.values()],
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
circles: [...this.circles.values()],
infoWindows: this.infoWindows,
});

Expand All @@ -202,7 +232,7 @@ export default abstract class<
element,
}: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
element: Marker | Polygon | Polyline | Circle;
}): InfoWindow {
this.dispatchEvent('info-window:before-create', { definition, element });
const infoWindow = this.doCreateInfoWindow({ definition, element });
Expand Down Expand Up @@ -248,6 +278,14 @@ export default abstract class<
this.onDrawChanged(this.polylines, this.polylinesValue, this.createPolyline, this.doRemovePolyline);
}

public circlesValueChanged(): void {
if (!this.isConnected) {
return;
}

this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle);
}

//endregion

//region Abstract factory methods to be implemented by the concrete classes, they are specific to the map provider
Expand Down Expand Up @@ -285,12 +323,20 @@ export default abstract class<

protected abstract doRemovePolyline(polyline: Polyline): void;

protected abstract doCreateCircle({
definition,
}: {
definition: CircleDefinition<CircleOptions, InfoWindowOptions>;
}): Circle;

protected abstract doRemoveCircle(circle: Circle): void;

protected abstract doCreateInfoWindow({
definition,
element,
}: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline;
element: Marker | Polygon | Polyline | Circle;
}): InfoWindow;
protected abstract doCreateIcon({
definition,
Expand Down Expand Up @@ -318,11 +364,20 @@ export default abstract class<
draws: typeof this.polylines,
factory: typeof this.doCreatePolyline
): typeof this.doCreatePolyline;
private createDrawingFactory(
type: 'circle',
draws: typeof this.circles,
factory: typeof this.doCreateCircle
): typeof this.doCreateCircle;
private createDrawingFactory<
Factory extends typeof this.doCreateMarker | typeof this.doCreatePolygon | typeof this.doCreatePolyline,
Factory extends
| typeof this.doCreateMarker
| typeof this.doCreatePolygon
| typeof this.doCreatePolyline
| typeof this.doCreateCircle,
Draw extends ReturnType<Factory>,
>(
type: 'marker' | 'polygon' | 'polyline',
type: 'marker' | 'polygon' | 'polyline' | 'circle',
draws: globalThis.Map<WithIdentifier<any>, Draw>,
factory: Factory
): Factory {
Expand Down Expand Up @@ -360,6 +415,12 @@ export default abstract class<
factory: typeof this.createPolyline,
remover: typeof this.doRemovePolyline
): void;
private onDrawChanged(
draws: typeof this.circles,
newDrawDefinitions: typeof this.circlesValue,
factory: typeof this.createCircle,
remover: typeof this.doRemoveCircle
): void;
private onDrawChanged<Draw, DrawDefinition extends WithIdentifier<Record<string, unknown>>>(
draws: globalThis.Map<WithIdentifier<any>, Draw>,
newDrawDefinitions: Array<DrawDefinition>,
Expand Down
Loading