Skip to content

Commit e95c0cd

Browse files
committed
feature #2340 [Map] add polyline support (sblondeau)
This PR was merged into the 2.x branch. Discussion ---------- [Map] add polyline support | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Issues | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead --> | License | MIT Add polyline support to Map for Leaflet and GoogleMap (useful e.g. for itinerary drawing) ```php public function index(): Response { $map = (new Map('default')) ->center(new Point(45.7534031, 4.8295061)) ->zoom(6) ->addPolyline( new Polyline( title: 'my title', points: [ new Point(48.8566, 2.3522), new Point(45.7640, 4.8357), new Point(43.2965, 5.3698), ] ) ) ; return $this->render('hom/index.html.twig', [ 'map' => $map, ]);; } ``` ![image](https://github.com/user-attachments/assets/d49631ee-fbea-4baa-835a-0dfad4fbbbc2) Commits ------- 754fd35 [Map] Add support for Polyline
2 parents d161d8d + 754fd35 commit e95c0cd

25 files changed

+632
-55
lines changed

src/Map/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add method `Symfony\UX\Map\Renderer\AbstractRenderer::tapOptions()`, to allow Renderer to modify options before rendering a Map.
66
- Add `ux_map.google_maps.default_map_id` configuration to set the Google ``Map ID``
77
- Add `ComponentWithMapTrait` to ease maps integration in [Live Components](https://symfony.com/bundles/ux-live-component/current/index.html)
8+
- Add `Polyline` support
89

910
## 2.20
1011

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
1919
rawOptions?: PolygonOptions;
2020
extra: Record<string, unknown>;
2121
};
22+
export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
23+
'@id': string;
24+
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
25+
points: Array<Point>;
26+
title: string | null;
27+
rawOptions?: PolylineOptions;
28+
extra: Record<string, unknown>;
29+
};
2230
export type InfoWindowDefinition<InfoWindowOptions> = {
2331
headerContent: string | null;
2432
content: string | null;
@@ -28,26 +36,29 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
2836
rawOptions?: InfoWindowOptions;
2937
extra: Record<string, unknown>;
3038
};
31-
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
39+
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline> extends Controller<HTMLElement> {
3240
static values: {
3341
providerOptions: ObjectConstructor;
3442
center: ObjectConstructor;
3543
zoom: NumberConstructor;
3644
fitBoundsToMarkers: BooleanConstructor;
3745
markers: ArrayConstructor;
3846
polygons: ArrayConstructor;
47+
polylines: ArrayConstructor;
3948
options: ObjectConstructor;
4049
};
4150
centerValue: Point | null;
4251
zoomValue: number | null;
4352
fitBoundsToMarkersValue: boolean;
4453
markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
4554
polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
55+
polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
4656
optionsValue: MapOptions;
4757
protected map: Map;
4858
protected markers: globalThis.Map<any, any>;
4959
protected infoWindows: Array<InfoWindow>;
5060
protected polygons: globalThis.Map<any, any>;
61+
protected polylines: globalThis.Map<any, any>;
5162
connect(): void;
5263
protected abstract doCreateMap({ center, zoom, options, }: {
5364
center: Point | null;
@@ -58,22 +69,36 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
5869
protected abstract removeMarker(marker: Marker): void;
5970
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
6071
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
72+
protected abstract removePolygon(polygon: Polygon): void;
6173
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
74+
createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
75+
protected abstract removePolyline(polyline: Polyline): void;
76+
protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
6277
protected createInfoWindow({ definition, element, }: {
63-
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
64-
element: Marker | Polygon;
78+
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
79+
element: Marker;
80+
} | {
81+
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
82+
element: Polygon;
83+
} | {
84+
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
85+
element: Polyline;
6586
}): InfoWindow;
6687
protected abstract doCreateInfoWindow({ definition, element, }: {
6788
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
6889
element: Marker;
6990
} | {
7091
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
7192
element: Polygon;
93+
} | {
94+
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
95+
element: Polyline;
7296
}): InfoWindow;
7397
protected abstract doFitBoundsToMarkers(): void;
7498
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
7599
abstract centerValueChanged(): void;
76100
abstract zoomValueChanged(): void;
77101
markersValueChanged(): void;
78102
polygonsValueChanged(): void;
103+
polylinesValueChanged(): void;
79104
}

src/Map/assets/dist/abstract_map_controller.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ class default_1 extends Controller {
66
this.markers = new Map();
77
this.infoWindows = [];
88
this.polygons = new Map();
9+
this.polylines = new Map();
910
}
1011
connect() {
1112
const options = this.optionsValue;
1213
this.dispatchEvent('pre-connect', { options });
1314
this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
1415
this.markersValue.forEach((marker) => this.createMarker(marker));
1516
this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
17+
this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
1618
if (this.fitBoundsToMarkersValue) {
1719
this.doFitBoundsToMarkers();
1820
}
1921
this.dispatchEvent('connect', {
2022
map: this.map,
2123
markers: [...this.markers.values()],
2224
polygons: [...this.polygons.values()],
25+
polylines: [...this.polylines.values()],
2326
infoWindows: this.infoWindows,
2427
});
2528
}
@@ -39,6 +42,14 @@ class default_1 extends Controller {
3942
this.polygons.set(definition['@id'], polygon);
4043
return polygon;
4144
}
45+
createPolyline(definition) {
46+
this.dispatchEvent('polyline:before-create', { definition });
47+
const polyline = this.doCreatePolyline(definition);
48+
this.dispatchEvent('polyline:after-create', { polyline });
49+
polyline['@id'] = definition['@id'];
50+
this.polylines.set(definition['@id'], polyline);
51+
return polyline;
52+
}
4253
createInfoWindow({ definition, element, }) {
4354
this.dispatchEvent('info-window:before-create', { definition, element });
4455
const infoWindow = this.doCreateInfoWindow({ definition, element });
@@ -71,7 +82,7 @@ class default_1 extends Controller {
7182
}
7283
this.polygons.forEach((polygon) => {
7384
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
74-
polygon.remove();
85+
this.removePolygon(polygon);
7586
this.polygons.delete(polygon['@id']);
7687
}
7788
});
@@ -81,6 +92,22 @@ class default_1 extends Controller {
8192
}
8293
});
8394
}
95+
polylinesValueChanged() {
96+
if (!this.map) {
97+
return;
98+
}
99+
this.polylines.forEach((polyline) => {
100+
if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
101+
this.removePolyline(polyline);
102+
this.polylines.delete(polyline['@id']);
103+
}
104+
});
105+
this.polylinesValue.forEach((polyline) => {
106+
if (!this.polylines.has(polyline['@id'])) {
107+
this.createPolyline(polyline);
108+
}
109+
});
110+
}
84111
}
85112
default_1.values = {
86113
providerOptions: Object,
@@ -89,6 +116,7 @@ default_1.values = {
89116
fitBoundsToMarkers: Boolean,
90117
markers: Array,
91118
polygons: Array,
119+
polylines: Array,
92120
options: Object,
93121
};
94122

src/Map/assets/src/abstract_map_controller.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,15 @@ export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
2929
extra: Record<string, unknown>;
3030
};
3131

32+
export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
33+
'@id': string;
34+
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
35+
points: Array<Point>;
36+
title: string | null;
37+
rawOptions?: PolylineOptions;
38+
extra: Record<string, unknown>;
39+
};
40+
3241
export type InfoWindowDefinition<InfoWindowOptions> = {
3342
headerContent: string | null;
3443
content: string | null;
@@ -58,6 +67,8 @@ export default abstract class<
5867
InfoWindow,
5968
PolygonOptions,
6069
Polygon,
70+
PolylineOptions,
71+
Polyline,
6172
> extends Controller<HTMLElement> {
6273
static values = {
6374
providerOptions: Object,
@@ -66,6 +77,7 @@ export default abstract class<
6677
fitBoundsToMarkers: Boolean,
6778
markers: Array,
6879
polygons: Array,
80+
polylines: Array,
6981
options: Object,
7082
};
7183

@@ -74,12 +86,14 @@ export default abstract class<
7486
declare fitBoundsToMarkersValue: boolean;
7587
declare markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
7688
declare polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
89+
declare polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
7790
declare optionsValue: MapOptions;
7891

7992
protected map: Map;
8093
protected markers = new Map<Marker>();
8194
protected infoWindows: Array<InfoWindow> = [];
8295
protected polygons = new Map<Polygon>();
96+
protected polylines = new Map<Polyline>();
8397

8498
connect() {
8599
const options = this.optionsValue;
@@ -92,6 +106,8 @@ export default abstract class<
92106

93107
this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
94108

109+
this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
110+
95111
if (this.fitBoundsToMarkersValue) {
96112
this.doFitBoundsToMarkers();
97113
}
@@ -100,6 +116,7 @@ export default abstract class<
100116
map: this.map,
101117
markers: [...this.markers.values()],
102118
polygons: [...this.polygons.values()],
119+
polylines: [...this.polylines.values()],
103120
infoWindows: this.infoWindows,
104121
});
105122
}
@@ -142,17 +159,36 @@ export default abstract class<
142159
return polygon;
143160
}
144161

162+
protected abstract removePolygon(polygon: Polygon): void;
163+
145164
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
146165

166+
public createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline {
167+
this.dispatchEvent('polyline:before-create', { definition });
168+
const polyline = this.doCreatePolyline(definition);
169+
this.dispatchEvent('polyline:after-create', { polyline });
170+
171+
polyline['@id'] = definition['@id'];
172+
173+
this.polylines.set(definition['@id'], polyline);
174+
175+
return polyline;
176+
}
177+
178+
protected abstract removePolyline(polyline: Polyline): void;
179+
180+
protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
181+
147182
protected createInfoWindow({
148183
definition,
149184
element,
150-
}: {
151-
definition:
152-
| MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']
153-
| PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
154-
element: Marker | Polygon;
155-
}): InfoWindow {
185+
}:
186+
| { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
187+
| { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
188+
| {
189+
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
190+
element: Polyline;
191+
}): InfoWindow {
156192
this.dispatchEvent('info-window:before-create', { definition, element });
157193
const infoWindow = this.doCreateInfoWindow({ definition, element });
158194
this.dispatchEvent('info-window:after-create', { infoWindow, element });
@@ -166,13 +202,11 @@ export default abstract class<
166202
definition,
167203
element,
168204
}:
205+
| { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
206+
| { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
169207
| {
170-
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
171-
element: Marker;
172-
}
173-
| {
174-
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
175-
element: Polygon;
208+
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
209+
element: Polyline;
176210
}): InfoWindow;
177211

178212
protected abstract doFitBoundsToMarkers(): void;
@@ -213,7 +247,7 @@ export default abstract class<
213247

214248
this.polygons.forEach((polygon) => {
215249
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
216-
polygon.remove();
250+
this.removePolygon(polygon);
217251
this.polygons.delete(polygon['@id']);
218252
}
219253
});
@@ -224,4 +258,23 @@ export default abstract class<
224258
}
225259
});
226260
}
261+
262+
public polylinesValueChanged(): void {
263+
if (!this.map) {
264+
return;
265+
}
266+
267+
this.polylines.forEach((polyline) => {
268+
if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
269+
this.removePolyline(polyline);
270+
this.polylines.delete(polyline['@id']);
271+
}
272+
});
273+
274+
this.polylinesValue.forEach((polyline) => {
275+
if (!this.polylines.has(polyline['@id'])) {
276+
this.createPolyline(polyline);
277+
}
278+
});
279+
}
227280
}

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,25 @@ class MyMapController extends AbstractMapController {
3535
return polygon;
3636
}
3737

38+
doCreatePolyline(definition) {
39+
const polyline = { polyline: 'polyline', title: definition.title };
40+
41+
if (definition.infoWindow) {
42+
this.createInfoWindow({ definition: definition.infoWindow, element: polyline });
43+
}
44+
return polyline;
45+
}
46+
3847
doCreateInfoWindow({ definition, element }) {
3948
if (element.marker) {
4049
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title };
4150
}
4251
if (element.polygon) {
4352
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title };
4453
}
54+
if (element.polyline) {
55+
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polyline: element.title };
56+
}
4557
}
4658

4759
doFitBoundsToMarkers() {
@@ -70,6 +82,7 @@ describe('AbstractMapController', () => {
7082
data-map-options-value="{}"
7183
data-map-markers-value="[{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Paris&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:&quot;Paris&quot;,&quot;content&quot;:null,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;a69f13edd2e571f3&quot;},{&quot;position&quot;:{&quot;lat&quot;:45.75,&quot;lng&quot;:4.85},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:&quot;Lyon&quot;,&quot;content&quot;:null,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;cb9c1a30d562694b&quot;},{&quot;position&quot;:{&quot;lat&quot;:43.6047,&quot;lng&quot;:1.4442},&quot;title&quot;:&quot;Toulouse&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:&quot;Toulouse&quot;,&quot;content&quot;:null,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;e6b3acef1325fb52&quot;}]"
7284
data-map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:45.75,&quot;lng&quot;:4.85},{&quot;lat&quot;:43.6047,&quot;lng&quot;:1.4442}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;228ae6f5c1b17cfd&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.4442,&quot;lng&quot;:43.6047},{&quot;lat&quot;:4.85,&quot;lng&quot;:45.75},{&quot;lat&quot;:2.3522,&quot;lng&quot;:48.8566}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:&quot;Polygon&quot;,&quot;content&quot;:null,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:{&quot;foo&quot;:&quot;bar&quot;}},&quot;extra&quot;:{&quot;fillColor&quot;:&quot;#ff0000&quot;},&quot;@id&quot;:&quot;9874334e4e8caa16&quot;}]"
85+
data-map-polylines-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.1173,&quot;lng&quot;:-1.6778},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.2082,&quot;lng&quot;:16.3738}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:&quot;Polyline&quot;,&quot;content&quot;:null,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:{&quot;foo&quot;:&quot;bar&quot;}},&quot;extra&quot;:{&quot;strokeColor&quot;:&quot;#ff0000&quot;},&quot;@id&quot;:&quot;0fa955da866c7720&quot;}]"
7386
style="height: 600px"
7487
></div>
7588
`);
@@ -79,7 +92,7 @@ describe('AbstractMapController', () => {
7992
clearDOM();
8093
});
8194

82-
it('connect and create map, marker, polygon and info window', async () => {
95+
it('connect and create map, marker, polygon, polyline and info window', async () => {
8396
const div = getByTestId(container, 'map');
8497
expect(div).not.toHaveClass('connected');
8598

@@ -101,6 +114,9 @@ describe('AbstractMapController', () => {
101114
['9874334e4e8caa16', { '@id': '9874334e4e8caa16', polygon: 'polygon', title: null }],
102115
])
103116
);
117+
expect(controller.polylines).toEqual(
118+
new Map([['0fa955da866c7720', { '@id': '0fa955da866c7720', polyline: 'polyline', title: null }]])
119+
);
104120
expect(controller.infoWindows).toEqual([
105121
{
106122
headerContent: 'Paris',
@@ -122,6 +138,11 @@ describe('AbstractMapController', () => {
122138
infoWindow: 'infoWindow',
123139
polygon: null,
124140
},
141+
{
142+
headerContent: 'Polyline',
143+
infoWindow: 'infoWindow',
144+
polyline: null,
145+
},
125146
]);
126147
});
127148
});

0 commit comments

Comments
 (0)