Skip to content

Commit 2f63db7

Browse files
rrr63Kocal
authored andcommitted
[Map] Add support for Polygons
1 parent 48c5fa1 commit 2f63db7

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)