Skip to content

[Map] Add Rectangle support #2845

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 4 commits into from
Jun 22, 2025
Merged
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
10 changes: 10 additions & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# CHANGELOG

## 2.29

- Add support for creating `Rectangle` by passing two `Point` instances to the `Rectangle` constructor, e.g.:
```php
$map->addRectangle(new Rectangle(
southWest: new Point(48.856613, 2.352222), // Paris
northEast: new Point(48.51238 2.21080) // Gare de Lyon (Paris)
));
```

## 2.28

- Add support for creating `Circle` by passing a `Point` and a radius (in meters) to the `Circle` constructor, e.g.:
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
@@ -56,6 +56,14 @@ export type CircleDefinition<CircleOptions, InfoWindowOptions> = WithIdentifier<
rawOptions?: CircleOptions;
extra: Record<string, unknown>;
}>;
export type RectangleDefinition<RectangleOptions, InfoWindowOptions> = WithIdentifier<{
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
southWest: Point;
northEast: Point;
title: string | null;
rawOptions?: RectangleOptions;
extra: Record<string, unknown>;
}>;
export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
@@ -66,7 +74,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, CircleOptions, Circle> extends Controller<HTMLElement> {
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline, CircleOptions, Circle, RectangleOptions, Rectangle> extends Controller<HTMLElement> {
static values: {
providerOptions: ObjectConstructor;
center: ObjectConstructor;
@@ -76,6 +84,7 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
polygons: ArrayConstructor;
polylines: ArrayConstructor;
circles: ArrayConstructor;
rectangles: ArrayConstructor;
options: ObjectConstructor;
};
centerValue: Point | null;
@@ -85,6 +94,7 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
circlesValue: Array<CircleDefinition<CircleOptions, InfoWindowOptions>>;
rectanglesValue: Array<RectangleDefinition<RectangleOptions, InfoWindowOptions>>;
optionsValue: MapOptions;
hasCenterValue: boolean;
hasZoomValue: boolean;
@@ -93,30 +103,34 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
hasPolygonsValue: boolean;
hasPolylinesValue: boolean;
hasCirclesValue: boolean;
hasRectanglesValue: 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 rectangles: globalThis.Map<string, Rectangle>;
protected infoWindows: Array<InfoWindow>;
private isConnected;
private createMarker;
private createPolygon;
private createPolyline;
private createCircle;
private createRectangle;
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
connect(): void;
createInfoWindow({ definition, element, }: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline | Circle;
element: Marker | Polygon | Polyline | Circle | Rectangle;
}): InfoWindow;
abstract centerValueChanged(): void;
abstract zoomValueChanged(): void;
markersValueChanged(): void;
polygonsValueChanged(): void;
polylinesValueChanged(): void;
circlesValueChanged(): void;
rectanglesValueChanged(): void;
protected abstract doCreateMap({ center, zoom, options, }: {
center: Point | null;
zoom: number | null;
@@ -139,9 +153,13 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
definition: CircleDefinition<CircleOptions, InfoWindowOptions>;
}): Circle;
protected abstract doRemoveCircle(circle: Circle): void;
protected abstract doCreateRectangle({ definition, }: {
definition: RectangleDefinition<RectangleOptions, InfoWindowOptions>;
}): Rectangle;
protected abstract doRemoveRectangle(rectangle: Rectangle): void;
protected abstract doCreateInfoWindow({ definition, element, }: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline | Circle;
element: Marker | Polygon | Polyline | Circle | Rectangle;
}): InfoWindow;
protected abstract doCreateIcon({ definition, element, }: {
definition: Icon;
11 changes: 11 additions & 0 deletions src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@ class default_1 extends Controller {
this.polygons = new Map();
this.polylines = new Map();
this.circles = new Map();
this.rectangles = new Map();
this.infoWindows = [];
this.isConnected = false;
}
@@ -22,6 +23,7 @@ class default_1 extends Controller {
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.createRectangle = this.createDrawingFactory('rectangle', this.rectangles, this.doCreateRectangle.bind(this));
this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
zoom: this.hasZoomValue ? this.zoomValue : null,
@@ -31,6 +33,7 @@ class default_1 extends Controller {
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
this.circlesValue.forEach((definition) => this.createCircle({ definition }));
this.rectanglesValue.forEach((definition) => this.createRectangle({ definition }));
if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
@@ -40,6 +43,7 @@ class default_1 extends Controller {
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
circles: [...this.circles.values()],
rectangles: [...this.rectangles.values()],
infoWindows: this.infoWindows,
});
this.isConnected = true;
@@ -78,6 +82,12 @@ class default_1 extends Controller {
}
this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle);
}
rectanglesValueChanged() {
if (!this.isConnected) {
return;
}
this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle);
}
createDrawingFactory(type, draws, factory) {
const eventBefore = `${type}:before-create`;
const eventAfter = `${type}:after-create`;
@@ -115,6 +125,7 @@ default_1.values = {
polygons: Array,
polylines: Array,
circles: Array,
rectangles: Array,
options: Object,
};

70 changes: 66 additions & 4 deletions src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
@@ -98,6 +98,24 @@ export type CircleDefinition<CircleOptions, InfoWindowOptions> = WithIdentifier<
extra: Record<string, unknown>;
}>;

export type RectangleDefinition<RectangleOptions, InfoWindowOptions> = WithIdentifier<{
infoWindow?: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
southWest: Point;
northEast: Point;
title: string | null;
/**
* Raw options passed to the rectangle constructor, specific to the map provider (e.g.: `L.rectangle()` for Leaflet).
*/
rawOptions?: RectangleOptions;
/**
* 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:rectangle:before-create`
* - `ux:map:rectangle:after-create`
*/
extra: Record<string, unknown>;
}>;

export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
@@ -136,6 +154,8 @@ export default abstract class<
Polyline,
CircleOptions,
Circle,
RectangleOptions,
Rectangle,
> extends Controller<HTMLElement> {
static values = {
providerOptions: Object,
@@ -146,6 +166,7 @@ export default abstract class<
polygons: Array,
polylines: Array,
circles: Array,
rectangles: Array,
options: Object,
};

@@ -156,6 +177,7 @@ export default abstract class<
declare polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
declare polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
declare circlesValue: Array<CircleDefinition<CircleOptions, InfoWindowOptions>>;
declare rectanglesValue: Array<RectangleDefinition<RectangleOptions, InfoWindowOptions>>;
declare optionsValue: MapOptions;

declare hasCenterValue: boolean;
@@ -165,13 +187,15 @@ export default abstract class<
declare hasPolygonsValue: boolean;
declare hasPolylinesValue: boolean;
declare hasCirclesValue: boolean;
declare hasRectanglesValue: 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 rectangles = new Map<Identifier, Rectangle>();
protected infoWindows: Array<InfoWindow> = [];

private isConnected = false;
@@ -187,6 +211,9 @@ export default abstract class<
private createCircle: ({
definition,
}: { definition: CircleDefinition<CircleOptions, InfoWindowOptions> }) => Circle;
private createRectangle: ({
definition,
}: { definition: RectangleDefinition<RectangleOptions, InfoWindowOptions> }) => Rectangle;

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

@@ -199,6 +226,11 @@ export default abstract class<
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.createRectangle = this.createDrawingFactory(
'rectangle',
this.rectangles,
this.doCreateRectangle.bind(this)
);

this.map = this.doCreateMap({
center: this.hasCenterValue ? this.centerValue : null,
@@ -209,6 +241,7 @@ export default abstract class<
this.polygonsValue.forEach((definition) => this.createPolygon({ definition }));
this.polylinesValue.forEach((definition) => this.createPolyline({ definition }));
this.circlesValue.forEach((definition) => this.createCircle({ definition }));
this.rectanglesValue.forEach((definition) => this.createRectangle({ definition }));

if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
@@ -220,6 +253,7 @@ export default abstract class<
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
circles: [...this.circles.values()],
rectangles: [...this.rectangles.values()],
infoWindows: this.infoWindows,
});

@@ -232,7 +266,7 @@ export default abstract class<
element,
}: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline | Circle;
element: Marker | Polygon | Polyline | Circle | Rectangle;
}): InfoWindow {
this.dispatchEvent('info-window:before-create', { definition, element });
const infoWindow = this.doCreateInfoWindow({ definition, element });
@@ -286,6 +320,14 @@ export default abstract class<
this.onDrawChanged(this.circles, this.circlesValue, this.createCircle, this.doRemoveCircle);
}

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

this.onDrawChanged(this.rectangles, this.rectanglesValue, this.createRectangle, this.doRemoveRectangle);
}

//endregion

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

protected abstract doRemoveCircle(circle: Circle): void;

protected abstract doCreateRectangle({
definition,
}: {
definition: RectangleDefinition<RectangleOptions, InfoWindowOptions>;
}): Rectangle;

protected abstract doRemoveRectangle(rectangle: Rectangle): void;

protected abstract doCreateInfoWindow({
definition,
element,
}: {
definition: InfoWindowWithoutPositionDefinition<InfoWindowOptions>;
element: Marker | Polygon | Polyline | Circle;
element: Marker | Polygon | Polyline | Circle | Rectangle;
}): InfoWindow;
protected abstract doCreateIcon({
definition,
@@ -369,15 +419,21 @@ export default abstract class<
draws: typeof this.circles,
factory: typeof this.doCreateCircle
): typeof this.doCreateCircle;
private createDrawingFactory(
type: 'rectangle',
draws: typeof this.rectangles,
factory: typeof this.doCreateRectangle
): typeof this.doCreateRectangle;
private createDrawingFactory<
Factory extends
| typeof this.doCreateMarker
| typeof this.doCreatePolygon
| typeof this.doCreatePolyline
| typeof this.doCreateCircle,
| typeof this.doCreateCircle
| typeof this.doCreateRectangle,
Draw extends ReturnType<Factory>,
>(
type: 'marker' | 'polygon' | 'polyline' | 'circle',
type: 'marker' | 'polygon' | 'polyline' | 'circle' | 'rectangle',
draws: globalThis.Map<WithIdentifier<any>, Draw>,
factory: Factory
): Factory {
@@ -421,6 +477,12 @@ export default abstract class<
factory: typeof this.createCircle,
remover: typeof this.doRemoveCircle
): void;
private onDrawChanged(
draws: typeof this.rectangles,
newDrawDefinitions: typeof this.rectanglesValue,
factory: typeof this.createRectangle,
remover: typeof this.doRemoveRectangle
): void;
private onDrawChanged<Draw, DrawDefinition extends WithIdentifier<Record<string, unknown>>>(
draws: globalThis.Map<WithIdentifier<any>, Draw>,
newDrawDefinitions: Array<DrawDefinition>,
Loading