Skip to content

Commit 01a1fa0

Browse files
committed
feature #2162 [Map] Adding polygons to google and leaflet + info window (rrr63)
This PR was merged into the 2.x branch. Discussion ---------- [Map] Adding polygons to google and leaflet + info window | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | Issues | | License | MIT Hello, In my job i use to work a lot with maps, so i would love to work with symfony ux in next projects. But i have to work with polygons, so i tried to implement it to the ux-map. I added polygons on the Map component, and made it work on both bridge : google and leaflet. I added a window popup on click also, the current version of window popup worked only with markers. I had to rename `createInfoWindow` to `createInfoWindowMarker`. With that i could add a `createInfoWindowPolygon`. The only difference with the infoWindow from marker is that his anchor will be the event cursor position, because the area of the polygon could be very large and we want a window on the click position. You can try by adding a simple polygon on your map : ```php ->addPolygon(new Polygon( points: [ new Point(45.7402741, 3.191185), new Point(45.7314078, 3.1903267), new Point(45.7307488, 3.208952), new Point(45.7402741, 3.2112694) ], rawOptions:[ 'fillColor' => 'green', 'color' => 'green', ], infoWindow: new InfoWindow( headerContent: '<b>Polygon</b>', content: 'I am a polygon' ) )) ``` ![smallgreenrectangle](https://github.com/user-attachments/assets/09282c03-dc7a-499c-b489-44bf56816cb4) Here a full example that works with leaflet and google that show some of french regions via an api call ```php $polygons = json_decode(file_get_contents('api_url'), true); $polygons = $polygons['features']; $googleOptions = (new GoogleOptions()) ->mapId('mapId') ; $map = (new Map('default', $googleOptions)) ->center(new Point(45.8079318, 3)) ->zoom(10) ; foreach ($polygons as $polygon) { $points = []; $title = $polygon['properties']['nom']; foreach ($polygon['geometry']['coordinates'][0] as $point) { if (count($point) == 2) { $points[] = new Point($point[1], $point[0]); } } $map->addPolygon(new Polygon( points: $points, rawOptions:[ 'fillColor' => 'red', 'color' => 'red', ], infoWindow: new InfoWindow( headerContent: '<b>'.$title.'</b>', content: 'The French region of '.$title.'.' ) )); } ``` ![googleregions](https://github.com/user-attachments/assets/511ec6d5-5ab1-4feb-b58f-6b5542e28091) ![leafletregions](https://github.com/user-attachments/assets/f68d18b3-8399-40a9-979f-db039a5298ec) I added tests for polygons and polygon info window I hope I did it right, i listen for any suggestions or edits in the PR Commits ------- 2f63db7 [Map] Add support for Polygons
2 parents 48c5fa1 + 2f63db7 commit 01a1fa0

23 files changed

+605
-132
lines changed

src/Map/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Add `<twig:ux:map />` Twig component
1010
- The importmap entry `@symfony/ux-map/abstract-map-controller` can be removed
1111
from your importmap, it is no longer needed.
12+
- Add `Polygon` support
1213

1314
## 2.19
1415

src/Map/assets/dist/abstract_map_controller.d.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ export type Point = {
33
lat: number;
44
lng: number;
55
};
6-
export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
6+
export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
77
center: Point | null;
88
zoom: number | null;
99
fitBoundsToMarkers: boolean;
1010
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
11+
polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
1112
options: Options;
1213
};
1314
export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
@@ -17,6 +18,13 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
1718
rawOptions?: MarkerOptions;
1819
extra: Record<string, unknown>;
1920
};
21+
export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
22+
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
23+
points: Array<Point>;
24+
title: string | null;
25+
rawOptions?: PolygonOptions;
26+
extra: Record<string, unknown>;
27+
};
2028
export type InfoWindowDefinition<InfoWindowOptions> = {
2129
headerContent: string | null;
2230
content: string | null;
@@ -26,30 +34,36 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
2634
rawOptions?: InfoWindowOptions;
2735
extra: Record<string, unknown>;
2836
};
29-
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow> extends Controller<HTMLElement> {
37+
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
3038
static values: {
3139
providerOptions: ObjectConstructor;
3240
view: ObjectConstructor;
3341
};
34-
viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
42+
viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
3543
protected map: Map;
3644
protected markers: Array<Marker>;
3745
protected infoWindows: Array<InfoWindow>;
46+
protected polygons: Array<Polygon>;
3847
connect(): void;
3948
protected abstract doCreateMap({ center, zoom, options, }: {
4049
center: Point | null;
4150
zoom: number | null;
4251
options: MapOptions;
4352
}): Map;
4453
createMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
54+
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
4555
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
46-
protected createInfoWindow({ definition, marker, }: {
47-
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
48-
marker: Marker;
56+
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
57+
protected createInfoWindow({ definition, element, }: {
58+
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
59+
element: Marker | Polygon;
4960
}): InfoWindow;
50-
protected abstract doCreateInfoWindow({ definition, marker, }: {
61+
protected abstract doCreateInfoWindow({ definition, element, }: {
5162
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
52-
marker: Marker;
63+
element: Marker;
64+
} | {
65+
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
66+
element: Polygon;
5367
}): InfoWindow;
5468
protected abstract doFitBoundsToMarkers(): void;
5569
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;

src/Map/assets/dist/abstract_map_controller.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ class default_1 extends Controller {
55
super(...arguments);
66
this.markers = [];
77
this.infoWindows = [];
8+
this.polygons = [];
89
}
910
connect() {
10-
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
11+
const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
1112
this.dispatchEvent('pre-connect', { options });
1213
this.map = this.doCreateMap({ center, zoom, options });
1314
markers.forEach((marker) => this.createMarker(marker));
15+
polygons.forEach((polygon) => this.createPolygon(polygon));
1416
if (fitBoundsToMarkers) {
1517
this.doFitBoundsToMarkers();
1618
}
1719
this.dispatchEvent('connect', {
1820
map: this.map,
1921
markers: this.markers,
22+
polygons: this.polygons,
2023
infoWindows: this.infoWindows,
2124
});
2225
}
@@ -27,10 +30,17 @@ class default_1 extends Controller {
2730
this.markers.push(marker);
2831
return marker;
2932
}
30-
createInfoWindow({ definition, marker, }) {
31-
this.dispatchEvent('info-window:before-create', { definition, marker });
32-
const infoWindow = this.doCreateInfoWindow({ definition, marker });
33-
this.dispatchEvent('info-window:after-create', { infoWindow, marker });
33+
createPolygon(definition) {
34+
this.dispatchEvent('polygon:before-create', { definition });
35+
const polygon = this.doCreatePolygon(definition);
36+
this.dispatchEvent('polygon:after-create', { polygon });
37+
this.polygons.push(polygon);
38+
return polygon;
39+
}
40+
createInfoWindow({ definition, element, }) {
41+
this.dispatchEvent('info-window:before-create', { definition, element });
42+
const infoWindow = this.doCreateInfoWindow({ definition, element });
43+
this.dispatchEvent('info-window:after-create', { infoWindow, element });
3444
this.infoWindows.push(infoWindow);
3545
return infoWindow;
3646
}

src/Map/assets/src/abstract_map_controller.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Controller } from '@hotwired/stimulus';
22

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

5-
export type MapView<Options, MarkerOptions, InfoWindowOptions> = {
5+
export type MapView<Options, MarkerOptions, InfoWindowOptions, PolygonOptions> = {
66
center: Point | null;
77
zoom: number | null;
88
fitBoundsToMarkers: boolean;
99
markers: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
10+
polygons: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
1011
options: Options;
1112
};
1213

@@ -27,6 +28,14 @@ export type MarkerDefinition<MarkerOptions, InfoWindowOptions> = {
2728
extra: Record<string, unknown>;
2829
};
2930

31+
export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
32+
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
33+
points: Array<Point>;
34+
title: string | null;
35+
rawOptions?: PolygonOptions;
36+
extra: Record<string, unknown>;
37+
};
38+
3039
export type InfoWindowDefinition<InfoWindowOptions> = {
3140
headerContent: string | null;
3241
content: string | null;
@@ -54,34 +63,40 @@ export default abstract class<
5463
Marker,
5564
InfoWindowOptions,
5665
InfoWindow,
66+
PolygonOptions,
67+
Polygon,
5768
> extends Controller<HTMLElement> {
5869
static values = {
5970
providerOptions: Object,
6071
view: Object,
6172
};
6273

63-
declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions>;
74+
declare viewValue: MapView<MapOptions, MarkerOptions, InfoWindowOptions, PolygonOptions>;
6475

6576
protected map: Map;
6677
protected markers: Array<Marker> = [];
6778
protected infoWindows: Array<InfoWindow> = [];
79+
protected polygons: Array<Polygon> = [];
6880

6981
connect() {
70-
const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
82+
const { center, zoom, options, markers, polygons, fitBoundsToMarkers } = this.viewValue;
7183

7284
this.dispatchEvent('pre-connect', { options });
7385

7486
this.map = this.doCreateMap({ center, zoom, options });
7587

7688
markers.forEach((marker) => this.createMarker(marker));
7789

90+
polygons.forEach((polygon) => this.createPolygon(polygon));
91+
7892
if (fitBoundsToMarkers) {
7993
this.doFitBoundsToMarkers();
8094
}
8195

8296
this.dispatchEvent('connect', {
8397
map: this.map,
8498
markers: this.markers,
99+
polygons: this.polygons,
85100
infoWindows: this.infoWindows,
86101
});
87102
}
@@ -106,18 +121,29 @@ export default abstract class<
106121
return marker;
107122
}
108123

124+
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon {
125+
this.dispatchEvent('polygon:before-create', { definition });
126+
const polygon = this.doCreatePolygon(definition);
127+
this.dispatchEvent('polygon:after-create', { polygon });
128+
this.polygons.push(polygon);
129+
return polygon;
130+
}
131+
109132
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
133+
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
110134

111135
protected createInfoWindow({
112136
definition,
113-
marker,
137+
element,
114138
}: {
115-
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
116-
marker: Marker;
139+
definition:
140+
| MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']
141+
| PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
142+
element: Marker | Polygon;
117143
}): InfoWindow {
118-
this.dispatchEvent('info-window:before-create', { definition, marker });
119-
const infoWindow = this.doCreateInfoWindow({ definition, marker });
120-
this.dispatchEvent('info-window:after-create', { infoWindow, marker });
144+
this.dispatchEvent('info-window:before-create', { definition, element });
145+
const infoWindow = this.doCreateInfoWindow({ definition, element });
146+
this.dispatchEvent('info-window:after-create', { infoWindow, element });
121147

122148
this.infoWindows.push(infoWindow);
123149

@@ -126,11 +152,16 @@ export default abstract class<
126152

127153
protected abstract doCreateInfoWindow({
128154
definition,
129-
marker,
130-
}: {
131-
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
132-
marker: Marker;
133-
}): InfoWindow;
155+
element,
156+
}:
157+
| {
158+
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
159+
element: Marker;
160+
}
161+
| {
162+
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
163+
element: Polygon;
164+
}): InfoWindow;
134165

135166
protected abstract doFitBoundsToMarkers(): void;
136167

src/Map/assets/test/abstract_map_controller.test.ts

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,28 @@ class MyMapController extends AbstractMapController {
2020
const marker = { marker: 'marker', title: definition.title };
2121

2222
if (definition.infoWindow) {
23-
this.createInfoWindow({ definition: definition.infoWindow, marker });
23+
this.createInfoWindow({ definition: definition.infoWindow, element: marker });
2424
}
2525

2626
return marker;
2727
}
2828

29-
doCreateInfoWindow({ definition, marker }) {
30-
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: marker.title };
29+
doCreatePolygon(definition) {
30+
const polygon = { polygon: 'polygon', title: definition.title };
31+
32+
if (definition.infoWindow) {
33+
this.createInfoWindow({ definition: definition.infoWindow, element: polygon });
34+
}
35+
return polygon;
36+
}
37+
38+
doCreateInfoWindow({ definition, element }) {
39+
if (element.marker) {
40+
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title };
41+
}
42+
if (element.polygon) {
43+
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title };
44+
}
3145
}
3246

3347
doFitBoundsToMarkers() {
@@ -47,20 +61,69 @@ describe('AbstractMapController', () => {
4761
beforeEach(() => {
4862
container = mountDOM(`
4963
<div
50-
data-testid="map"
51-
data-controller="map"
52-
style="height&#x3A;&#x20;700px&#x3B;&#x20;margin&#x3A;&#x20;10px"
53-
data-map-provider-options-value="&#x7B;&#x7D;"
54-
data-map-view-value="&#x7B;&quot;center&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;zoom&quot;&#x3A;4,&quot;fitBoundsToMarkers&quot;&#x3A;true,&quot;options&quot;&#x3A;&#x7B;&#x7D;,&quot;markers&quot;&#x3A;&#x5B;&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;48.8566,&quot;lng&quot;&#x3A;2.3522&#x7D;,&quot;title&quot;&#x3A;&quot;Paris&quot;,&quot;infoWindow&quot;&#x3A;null&#x7D;,&#x7B;&quot;position&quot;&#x3A;&#x7B;&quot;lat&quot;&#x3A;45.764,&quot;lng&quot;&#x3A;4.8357&#x7D;,&quot;title&quot;&#x3A;&quot;Lyon&quot;,&quot;infoWindow&quot;&#x3A;&#x7B;&quot;headerContent&quot;&#x3A;&quot;&lt;b&gt;Lyon&lt;&#x5C;&#x2F;b&gt;&quot;,&quot;content&quot;&#x3A;&quot;The&#x20;French&#x20;town&#x20;in&#x20;the&#x20;historic&#x20;Rh&#x5C;u00f4ne-Alpes&#x20;region,&#x20;located&#x20;at&#x20;the&#x20;junction&#x20;of&#x20;the&#x20;Rh&#x5C;u00f4ne&#x20;and&#x20;Sa&#x5C;u00f4ne&#x20;rivers.&quot;,&quot;position&quot;&#x3A;null,&quot;opened&quot;&#x3A;false,&quot;autoClose&quot;&#x3A;true&#x7D;&#x7D;&#x5D;&#x7D;"
55-
></div>
64+
data-testid="map"
65+
data-controller="map"
66+
style="height: 700px; margin: 10px;"
67+
data-map-provider-options-value="{}"
68+
data-map-view-value='{
69+
"center": { "lat": 48.8566, "lng": 2.3522 },
70+
"zoom": 4,
71+
"fitBoundsToMarkers": true,
72+
"options": {},
73+
"markers": [
74+
{
75+
"position": { "lat": 48.8566, "lng": 2.3522 },
76+
"title": "Paris",
77+
"infoWindow": null
78+
},
79+
{
80+
"position": { "lat": 45.764, "lng": 4.8357 },
81+
"title": "Lyon",
82+
"infoWindow": {
83+
"headerContent": "<b>Lyon</b>",
84+
"content": "The French town in the historic Rhône-Alpes region, located at the junction of the Rhône and Saône rivers.",
85+
"position": null,
86+
"opened": false,
87+
"autoClose": true
88+
}
89+
}
90+
],
91+
"polygons": [
92+
{
93+
"coordinates": [
94+
{ "lat": 48.858844, "lng": 2.294351 },
95+
{ "lat": 48.853, "lng": 2.3499 },
96+
{ "lat": 48.8566, "lng": 2.3522 }
97+
],
98+
"title": "Polygon 1",
99+
"infoWindow": null
100+
},
101+
{
102+
"coordinates": [
103+
{ "lat": 45.764043, "lng": 4.835659 },
104+
{ "lat": 45.750000, "lng": 4.850000 },
105+
{ "lat": 45.770000, "lng": 4.820000 }
106+
],
107+
"title": "Polygon 2",
108+
"infoWindow": {
109+
"headerContent": "<b>Polygon 2</b>",
110+
"content": "A polygon around Lyon with some additional info.",
111+
"position": null,
112+
"opened": false,
113+
"autoClose": true
114+
}
115+
}
116+
]
117+
}'>
118+
</div>
56119
`);
57120
});
58121

59122
afterEach(() => {
60123
clearDOM();
61124
});
62125

63-
it('connect and create map, marker and info window', async () => {
126+
it('connect and create map, marker, polygon and info window', async () => {
64127
const div = getByTestId(container, 'map');
65128
expect(div).not.toHaveClass('connected');
66129

@@ -73,12 +136,21 @@ describe('AbstractMapController', () => {
73136
{ marker: 'marker', title: 'Paris' },
74137
{ marker: 'marker', title: 'Lyon' },
75138
]);
139+
expect(controller.polygons).toEqual([
140+
{ polygon: 'polygon', title: 'Polygon 1' },
141+
{ polygon: 'polygon', title: 'Polygon 2' },
142+
]);
76143
expect(controller.infoWindows).toEqual([
77144
{
78145
headerContent: '<b>Lyon</b>',
79146
infoWindow: 'infoWindow',
80147
marker: 'Lyon',
81148
},
149+
{
150+
headerContent: '<b>Polygon 2</b>',
151+
infoWindow: 'infoWindow',
152+
polygon: 'Polygon 2',
153+
},
82154
]);
83155
});
84156
});

0 commit comments

Comments
 (0)