Skip to content

Commit

Permalink
feature #2340 [Map] add polyline support (sblondeau)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
smnandre committed Nov 23, 2024
2 parents d161d8d + 754fd35 commit e95c0cd
Show file tree
Hide file tree
Showing 25 changed files with 632 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/Map/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Add method `Symfony\UX\Map\Renderer\AbstractRenderer::tapOptions()`, to allow Renderer to modify options before rendering a Map.
- Add `ux_map.google_maps.default_map_id` configuration to set the Google ``Map ID``
- Add `ComponentWithMapTrait` to ease maps integration in [Live Components](https://symfony.com/bundles/ux-live-component/current/index.html)
- Add `Polyline` support

## 2.20

Expand Down
31 changes: 28 additions & 3 deletions src/Map/assets/dist/abstract_map_controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
rawOptions?: PolygonOptions;
extra: Record<string, unknown>;
};
export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
'@id': string;
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
points: Array<Point>;
title: string | null;
rawOptions?: PolylineOptions;
extra: Record<string, unknown>;
};
export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
Expand All @@ -28,26 +36,29 @@ export type InfoWindowDefinition<InfoWindowOptions> = {
rawOptions?: InfoWindowOptions;
extra: Record<string, unknown>;
};
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon> extends Controller<HTMLElement> {
export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindowOptions, InfoWindow, PolygonOptions, Polygon, PolylineOptions, Polyline> extends Controller<HTMLElement> {
static values: {
providerOptions: ObjectConstructor;
center: ObjectConstructor;
zoom: NumberConstructor;
fitBoundsToMarkers: BooleanConstructor;
markers: ArrayConstructor;
polygons: ArrayConstructor;
polylines: ArrayConstructor;
options: ObjectConstructor;
};
centerValue: Point | null;
zoomValue: number | null;
fitBoundsToMarkersValue: boolean;
markersValue: Array<MarkerDefinition<MarkerOptions, InfoWindowOptions>>;
polygonsValue: Array<PolygonDefinition<PolygonOptions, InfoWindowOptions>>;
polylinesValue: Array<PolylineDefinition<PolylineOptions, InfoWindowOptions>>;
optionsValue: MapOptions;
protected map: Map;
protected markers: globalThis.Map<any, any>;
protected infoWindows: Array<InfoWindow>;
protected polygons: globalThis.Map<any, any>;
protected polylines: globalThis.Map<any, any>;
connect(): void;
protected abstract doCreateMap({ center, zoom, options, }: {
center: Point | null;
Expand All @@ -58,22 +69,36 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
protected abstract removeMarker(marker: Marker): void;
protected abstract doCreateMarker(definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>): Marker;
createPolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
protected abstract removePolygon(polygon: Polygon): void;
protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;
createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
protected abstract removePolyline(polyline: Polyline): void;
protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;
protected createInfoWindow({ definition, element, }: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'] | PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
element: Marker | Polygon;
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
element: Marker;
} | {
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
element: Polygon;
} | {
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
element: Polyline;
}): InfoWindow;
protected abstract doCreateInfoWindow({ definition, element, }: {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
element: Marker;
} | {
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
element: Polygon;
} | {
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
element: Polyline;
}): InfoWindow;
protected abstract doFitBoundsToMarkers(): void;
protected abstract dispatchEvent(name: string, payload: Record<string, unknown>): void;
abstract centerValueChanged(): void;
abstract zoomValueChanged(): void;
markersValueChanged(): void;
polygonsValueChanged(): void;
polylinesValueChanged(): void;
}
30 changes: 29 additions & 1 deletion src/Map/assets/dist/abstract_map_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ class default_1 extends Controller {
this.markers = new Map();
this.infoWindows = [];
this.polygons = new Map();
this.polylines = new Map();
}
connect() {
const options = this.optionsValue;
this.dispatchEvent('pre-connect', { options });
this.map = this.doCreateMap({ center: this.centerValue, zoom: this.zoomValue, options });
this.markersValue.forEach((marker) => this.createMarker(marker));
this.polygonsValue.forEach((polygon) => this.createPolygon(polygon));
this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));
if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
this.dispatchEvent('connect', {
map: this.map,
markers: [...this.markers.values()],
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
infoWindows: this.infoWindows,
});
}
Expand All @@ -39,6 +42,14 @@ class default_1 extends Controller {
this.polygons.set(definition['@id'], polygon);
return polygon;
}
createPolyline(definition) {
this.dispatchEvent('polyline:before-create', { definition });
const polyline = this.doCreatePolyline(definition);
this.dispatchEvent('polyline:after-create', { polyline });
polyline['@id'] = definition['@id'];
this.polylines.set(definition['@id'], polyline);
return polyline;
}
createInfoWindow({ definition, element, }) {
this.dispatchEvent('info-window:before-create', { definition, element });
const infoWindow = this.doCreateInfoWindow({ definition, element });
Expand Down Expand Up @@ -71,7 +82,7 @@ class default_1 extends Controller {
}
this.polygons.forEach((polygon) => {
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
polygon.remove();
this.removePolygon(polygon);
this.polygons.delete(polygon['@id']);
}
});
Expand All @@ -81,6 +92,22 @@ class default_1 extends Controller {
}
});
}
polylinesValueChanged() {
if (!this.map) {
return;
}
this.polylines.forEach((polyline) => {
if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
this.removePolyline(polyline);
this.polylines.delete(polyline['@id']);
}
});
this.polylinesValue.forEach((polyline) => {
if (!this.polylines.has(polyline['@id'])) {
this.createPolyline(polyline);
}
});
}
}
default_1.values = {
providerOptions: Object,
Expand All @@ -89,6 +116,7 @@ default_1.values = {
fitBoundsToMarkers: Boolean,
markers: Array,
polygons: Array,
polylines: Array,
options: Object,
};

Expand Down
79 changes: 66 additions & 13 deletions src/Map/assets/src/abstract_map_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ export type PolygonDefinition<PolygonOptions, InfoWindowOptions> = {
extra: Record<string, unknown>;
};

export type PolylineDefinition<PolylineOptions, InfoWindowOptions> = {
'@id': string;
infoWindow?: Omit<InfoWindowDefinition<InfoWindowOptions>, 'position'>;
points: Array<Point>;
title: string | null;
rawOptions?: PolylineOptions;
extra: Record<string, unknown>;
};

export type InfoWindowDefinition<InfoWindowOptions> = {
headerContent: string | null;
content: string | null;
Expand Down Expand Up @@ -58,6 +67,8 @@ export default abstract class<
InfoWindow,
PolygonOptions,
Polygon,
PolylineOptions,
Polyline,
> extends Controller<HTMLElement> {
static values = {
providerOptions: Object,
Expand All @@ -66,6 +77,7 @@ export default abstract class<
fitBoundsToMarkers: Boolean,
markers: Array,
polygons: Array,
polylines: Array,
options: Object,
};

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

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

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

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

this.polylinesValue.forEach((polyline) => this.createPolyline(polyline));

if (this.fitBoundsToMarkersValue) {
this.doFitBoundsToMarkers();
}
Expand All @@ -100,6 +116,7 @@ export default abstract class<
map: this.map,
markers: [...this.markers.values()],
polygons: [...this.polygons.values()],
polylines: [...this.polylines.values()],
infoWindows: this.infoWindows,
});
}
Expand Down Expand Up @@ -142,17 +159,36 @@ export default abstract class<
return polygon;
}

protected abstract removePolygon(polygon: Polygon): void;

protected abstract doCreatePolygon(definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>): Polygon;

public createPolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline {
this.dispatchEvent('polyline:before-create', { definition });
const polyline = this.doCreatePolyline(definition);
this.dispatchEvent('polyline:after-create', { polyline });

polyline['@id'] = definition['@id'];

this.polylines.set(definition['@id'], polyline);

return polyline;
}

protected abstract removePolyline(polyline: Polyline): void;

protected abstract doCreatePolyline(definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>): Polyline;

protected createInfoWindow({
definition,
element,
}: {
definition:
| MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']
| PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
element: Marker | Polygon;
}): InfoWindow {
}:
| { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
| { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
| {
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
element: Polyline;
}): InfoWindow {
this.dispatchEvent('info-window:before-create', { definition, element });
const infoWindow = this.doCreateInfoWindow({ definition, element });
this.dispatchEvent('info-window:after-create', { infoWindow, element });
Expand All @@ -166,13 +202,11 @@ export default abstract class<
definition,
element,
}:
| { definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow']; element: Marker }
| { definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow']; element: Polygon }
| {
definition: MarkerDefinition<MarkerOptions, InfoWindowOptions>['infoWindow'];
element: Marker;
}
| {
definition: PolygonDefinition<PolygonOptions, InfoWindowOptions>['infoWindow'];
element: Polygon;
definition: PolylineDefinition<PolylineOptions, InfoWindowOptions>['infoWindow'];
element: Polyline;
}): InfoWindow;

protected abstract doFitBoundsToMarkers(): void;
Expand Down Expand Up @@ -213,7 +247,7 @@ export default abstract class<

this.polygons.forEach((polygon) => {
if (!this.polygonsValue.find((p) => p['@id'] === polygon['@id'])) {
polygon.remove();
this.removePolygon(polygon);
this.polygons.delete(polygon['@id']);
}
});
Expand All @@ -224,4 +258,23 @@ export default abstract class<
}
});
}

public polylinesValueChanged(): void {
if (!this.map) {
return;
}

this.polylines.forEach((polyline) => {
if (!this.polylinesValue.find((p) => p['@id'] === polyline['@id'])) {
this.removePolyline(polyline);
this.polylines.delete(polyline['@id']);
}
});

this.polylinesValue.forEach((polyline) => {
if (!this.polylines.has(polyline['@id'])) {
this.createPolyline(polyline);
}
});
}
}
23 changes: 22 additions & 1 deletion src/Map/assets/test/abstract_map_controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,25 @@ class MyMapController extends AbstractMapController {
return polygon;
}

doCreatePolyline(definition) {
const polyline = { polyline: 'polyline', title: definition.title };

if (definition.infoWindow) {
this.createInfoWindow({ definition: definition.infoWindow, element: polyline });
}
return polyline;
}

doCreateInfoWindow({ definition, element }) {
if (element.marker) {
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, marker: element.title };
}
if (element.polygon) {
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polygon: element.title };
}
if (element.polyline) {
return { infoWindow: 'infoWindow', headerContent: definition.headerContent, polyline: element.title };
}
}

doFitBoundsToMarkers() {
Expand Down Expand Up @@ -70,6 +82,7 @@ describe('AbstractMapController', () => {
data-map-options-value="{}"
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;}]"
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;}]"
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;}]"
style="height: 600px"
></div>
`);
Expand All @@ -79,7 +92,7 @@ describe('AbstractMapController', () => {
clearDOM();
});

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

Expand All @@ -101,6 +114,9 @@ describe('AbstractMapController', () => {
['9874334e4e8caa16', { '@id': '9874334e4e8caa16', polygon: 'polygon', title: null }],
])
);
expect(controller.polylines).toEqual(
new Map([['0fa955da866c7720', { '@id': '0fa955da866c7720', polyline: 'polyline', title: null }]])
);
expect(controller.infoWindows).toEqual([
{
headerContent: 'Paris',
Expand All @@ -122,6 +138,11 @@ describe('AbstractMapController', () => {
infoWindow: 'infoWindow',
polygon: null,
},
{
headerContent: 'Polyline',
infoWindow: 'infoWindow',
polyline: null,
},
]);
});
});
Loading

0 comments on commit e95c0cd

Please sign in to comment.