Repository: symfony/ux
Pull request #2340 (2024-11-04 21:19:29)

[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)



WebMamba: Hey, thanks a lot for this PR @sblondeau! Could you please add more info about this new feature to your PR description? Some screenshots, details and what you trying to achieve! Thannnnnks 😁
sblondeau: Hello, I tried a lot of things but some tests (js ans php) failed and I do not understand why :( 
If someone has some time to investigate... 
Thanks :-)
Kocal: Hi @sblondeau, where are stuck?

Please ensure to rebase your branch from the latest `2.x` quite often, but also try to run some yarn scripts to easy your development:
```shell
yarn lint
yarn format
yarn workspaces foreach -Rp --from '{src/Map/assets,src/Map/src/Bridge/Google/assets,src/Map/src/Bridge/Leaflet/assets}' run build
yarn workspaces foreach -Rp --from '{src/Map/assets,src/Map/src/Bridge/Google/assets,src/Map/src/Bridge/Leaflet/assets}' run test
```

Also, please start to squash your commits, we don't want to see commits [`1517fe1` (#2340)](https://github.com/symfony/ux/pull/2340/commits/1517fe1c303cabfe913236b8fe470a37062b0223) like this because it will _spam_ the history for nothing (meaning, harder to blame if necessary).

Thanks!
sblondeau: @Kocal thanks for your help with the yarn command (it is not easy to guess them ;-) )
My code works loclaly but I do not understand how to correctly launch tests. I have a failing test with GoogleMap. Sorry if my question sounds stupid but I do not understand how to correctly contribute with this part, should I work in ux map repo or in the bridge repo ? It is frustrating to be stuck so close to the end ;-) Thanks for your help
Kocal: > @Kocal thanks for your help with the yarn command (it is not easy to guess them ;-) )

Well, in fact
```
yarn workspaces foreach -Rp --from '{src/Map/assets,src/Map/src/Bridge/Google/assets,src/Map/src/Bridge/Leaflet/assets}' run build
``` 
is simply a shortcut to:
```
(cd src/Map/assets && yarn build)
(cd src/Map/src/Bridge/Google/assets && yarn build)
(cd src/Map/src/Bridge/Leaflet/assets && yarn build)
``` 
😛 


> My code works loclaly but I do not understand how to correctly launch tests. I have a failing test with GoogleMap. Sorry if my question sounds stupid but I do not understand how to correctly contribute with this part, should I work in ux map repo or in the bridge repo ?

Yes! You should work in the UX repository, `symfony/ux-*` are supposed to be read-only, but it looks like it is not the case. 

To run JS tests for Google, you can run `cd src/Map/src/Bridge/Google/assets && yarn test`.

Keep up the good work! But... I predict you will have a lot of conflicts when #2385 will be merged :P 


Kocal: With the merge of stuff, there are now plenty of conflicts 😅 
I'll take it from here :)

Patch:
diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md
index df7d6551..cdad7ae9 100644
--- a/src/Map/CHANGELOG.md
+++ b/src/Map/CHANGELOG.md
@@ -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
 
diff --git a/src/Map/assets/dist/abstract_map_controller.d.ts b/src/Map/assets/dist/abstract_map_controller.d.ts
index 382050b9..7bfd5325 100644
--- a/src/Map/assets/dist/abstract_map_controller.d.ts
+++ b/src/Map/assets/dist/abstract_map_controller.d.ts
@@ -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;
@@ -28,7 +36,7 @@ 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;
@@ -36,6 +44,7 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
         fitBoundsToMarkers: BooleanConstructor;
         markers: ArrayConstructor;
         polygons: ArrayConstructor;
+        polylines: ArrayConstructor;
         options: ObjectConstructor;
     };
     centerValue: Point | null;
@@ -43,11 +52,13 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
     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;
@@ -58,10 +69,20 @@ 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'];
@@ -69,6 +90,9 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
     } | {
         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;
@@ -76,4 +100,5 @@ export default abstract class<MapOptions, Map, MarkerOptions, Marker, InfoWindow
     abstract zoomValueChanged(): void;
     markersValueChanged(): void;
     polygonsValueChanged(): void;
+    polylinesValueChanged(): void;
 }
diff --git a/src/Map/assets/dist/abstract_map_controller.js b/src/Map/assets/dist/abstract_map_controller.js
index a4936565..9381817e 100644
--- a/src/Map/assets/dist/abstract_map_controller.js
+++ b/src/Map/assets/dist/abstract_map_controller.js
@@ -6,6 +6,7 @@ class default_1 extends Controller {
         this.markers = new Map();
         this.infoWindows = [];
         this.polygons = new Map();
+        this.polylines = new Map();
     }
     connect() {
         const options = this.optionsValue;
@@ -13,6 +14,7 @@ class default_1 extends Controller {
         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();
         }
@@ -20,6 +22,7 @@ class default_1 extends Controller {
             map: this.map,
             markers: [...this.markers.values()],
             polygons: [...this.polygons.values()],
+            polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
     }
@@ -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 });
@@ -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']);
             }
         });
@@ -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,
@@ -89,6 +116,7 @@ default_1.values = {
     fitBoundsToMarkers: Boolean,
     markers: Array,
     polygons: Array,
+    polylines: Array,
     options: Object,
 };
 
diff --git a/src/Map/assets/src/abstract_map_controller.ts b/src/Map/assets/src/abstract_map_controller.ts
index 2d95f64a..a1f9de68 100644
--- a/src/Map/assets/src/abstract_map_controller.ts
+++ b/src/Map/assets/src/abstract_map_controller.ts
@@ -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;
@@ -58,6 +67,8 @@ export default abstract class<
     InfoWindow,
     PolygonOptions,
     Polygon,
+    PolylineOptions,
+    Polyline,
 > extends Controller<HTMLElement> {
     static values = {
         providerOptions: Object,
@@ -66,6 +77,7 @@ export default abstract class<
         fitBoundsToMarkers: Boolean,
         markers: Array,
         polygons: Array,
+        polylines: Array,
         options: Object,
     };
 
@@ -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;
@@ -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();
         }
@@ -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,
         });
     }
@@ -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 });
@@ -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;
@@ -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']);
             }
         });
@@ -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);
+            }
+        });
+    }
 }
diff --git a/src/Map/doc/index.rst b/src/Map/doc/index.rst
index 8274a99a..0801f95f 100644
--- a/src/Map/doc/index.rst
+++ b/src/Map/doc/index.rst
@@ -153,6 +153,24 @@ You can also add Polygons, which represents an area enclosed by a series of ``Po
         ),
     ));
 
+Add Polylines
+~~~~~~~~~~~~~
+
+You can add Polylines, which represents a path made by a series of ``Point`` instances::
+
+    $myMap->addPolyline(new Polyline(
+        points: [
+            new Point(48.8566, 2.3522),
+            new Point(45.7640, 4.8357),
+            new Point(43.2965, 5.3698),
+            new Point(44.8378, -0.5792),
+        ],
+        infoWindow: new InfoWindow(
+            content: 'A line passing through Paris, Lyon, Marseille, Bordeaux',
+        ),
+    ));
+
+
 Render a map
 ------------
 
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
index 110c2ff7..2ce6fcc0 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
-export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon> {
+export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElementOptions, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindowOptions, google.maps.InfoWindow, google.maps.PolygonOptions, google.maps.Polygon, google.maps.PolylineOptions, google.maps.Polyline> {
     providerOptionsValue: Pick<LoaderOptions, 'apiKey' | 'id' | 'language' | 'region' | 'nonce' | 'retries' | 'url' | 'version' | 'libraries'>;
     connect(): Promise<void>;
     protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
@@ -14,12 +14,18 @@ export default class extends AbstractMapController<MapOptions, google.maps.Map,
     protected doCreateMarker(definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>): google.maps.marker.AdvancedMarkerElement;
     protected removeMarker(marker: google.maps.marker.AdvancedMarkerElement): void;
     protected doCreatePolygon(definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>): google.maps.Polygon;
+    protected removePolygon(polygon: google.maps.Polygon): void;
+    protected doCreatePolyline(definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>): google.maps.Polyline;
+    protected removePolyline(polyline: google.maps.Polyline): void;
     protected doCreateInfoWindow({ definition, element, }: {
         definition: MarkerDefinition<google.maps.marker.AdvancedMarkerElementOptions, google.maps.InfoWindowOptions>['infoWindow'];
         element: google.maps.marker.AdvancedMarkerElement;
     } | {
         definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
         element: google.maps.Polygon;
+    } | {
+        definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>['infoWindow'];
+        element: google.maps.Polyline;
     }): google.maps.InfoWindow;
     private createTextOrElement;
     private closeInfoWindowsExcept;
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
index 4b199e56..4307c67a 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -7,6 +7,7 @@ class default_1 extends Controller {
         this.markers = new Map();
         this.infoWindows = [];
         this.polygons = new Map();
+        this.polylines = new Map();
     }
     connect() {
         const options = this.optionsValue;
@@ -14,6 +15,7 @@ class default_1 extends Controller {
         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();
         }
@@ -21,6 +23,7 @@ class default_1 extends Controller {
             map: this.map,
             markers: [...this.markers.values()],
             polygons: [...this.polygons.values()],
+            polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
     }
@@ -40,6 +43,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 });
@@ -72,7 +83,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']);
             }
         });
@@ -82,6 +93,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,
@@ -90,6 +117,7 @@ default_1.values = {
     fitBoundsToMarkers: Boolean,
     markers: Array,
     polygons: Array,
+    polylines: Array,
     options: Object,
 };
 
@@ -166,6 +194,27 @@ class map_controller extends default_1 {
         }
         return polygon;
     }
+    removePolygon(polygon) {
+        polygon.setMap(null);
+    }
+    doCreatePolyline(definition) {
+        const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
+        const polyline = new _google.maps.Polyline({
+            ...rawOptions,
+            path: points,
+            map: this.map,
+        });
+        if (title) {
+            polyline.set('title', title);
+        }
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polyline });
+        }
+        return polyline;
+    }
+    removePolyline(polyline) {
+        polyline.setMap(null);
+    }
     doCreateInfoWindow({ definition, element, }) {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
         const infoWindow = new _google.maps.InfoWindow({
diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
index 6aa9ff8b..df5e843c 100644
--- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
@@ -8,7 +8,7 @@
  */
 
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 import { Loader } from '@googlemaps/js-api-loader';
 
@@ -38,7 +38,9 @@ export default class extends AbstractMapController<
     google.maps.InfoWindowOptions,
     google.maps.InfoWindow,
     google.maps.PolygonOptions,
-    google.maps.Polygon
+    google.maps.Polygon,
+    google.maps.PolylineOptions,
+    google.maps.Polyline
 > {
     declare providerOptionsValue: Pick<
         LoaderOptions,
@@ -153,6 +155,36 @@ export default class extends AbstractMapController<
         return polygon;
     }
 
+    protected removePolygon(polygon: google.maps.Polygon) {
+        polygon.setMap(null);
+    }
+
+    protected doCreatePolyline(
+        definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>
+    ): google.maps.Polyline {
+        const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
+
+        const polyline = new _google.maps.Polyline({
+            ...rawOptions,
+            path: points,
+            map: this.map,
+        });
+
+        if (title) {
+            polyline.set('title', title);
+        }
+
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polyline });
+        }
+
+        return polyline;
+    }
+
+    protected removePolyline(polyline: google.maps.Polyline): void {
+        polyline.setMap(null);
+    }
+
     protected doCreateInfoWindow({
         definition,
         element,
@@ -167,6 +199,10 @@ export default class extends AbstractMapController<
         | {
               definition: PolygonDefinition<google.maps.Polygon, google.maps.InfoWindowOptions>['infoWindow'];
               element: google.maps.Polygon;
+          }
+        | {
+              definition: PolylineDefinition<google.maps.Polyline, google.maps.InfoWindowOptions>['infoWindow'];
+              element: google.maps.Polyline;
           }): google.maps.InfoWindow {
         const { headerContent, content, extra, rawOptions = {}, ...otherOptions } = definition;
 
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
index 1caf28c1..ccca2608 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
@@ -1,8 +1,8 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
+import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolylineOptions as PolygonOptions, PolylineOptions } from 'leaflet';
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: {
         url: string;
@@ -10,7 +10,7 @@ type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
         options: Record<string, unknown>;
     };
 };
-export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, PopupOptions, typeof L.Popup, PolygonOptions, typeof L.Polygon> {
+export default class extends AbstractMapController<MapOptions, typeof L.Map, MarkerOptions, typeof L.Marker, PopupOptions, typeof L.Popup, PolygonOptions, typeof L.Polygon, PolylineOptions, typeof L.Polyline> {
     connect(): void;
     protected dispatchEvent(name: string, payload?: Record<string, unknown>): void;
     protected doCreateMap({ center, zoom, options, }: {
@@ -21,12 +21,18 @@ export default class extends AbstractMapController<MapOptions, typeof L.Map, Mar
     protected doCreateMarker(definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>): L.Marker;
     protected removeMarker(marker: L.Marker): void;
     protected doCreatePolygon(definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>): L.Polygon;
+    protected removePolygon(polygon: L.Polygon): void;
+    protected doCreatePolyline(definition: PolylineDefinition): L.Polyline;
+    protected removePolyline(polyline: L.Polyline): void;
     protected doCreateInfoWindow({ definition, element, }: {
-        definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>;
+        definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>['infoWindow'];
         element: L.Marker;
     } | {
-        definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>;
+        definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>['infoWindow'];
         element: L.Polygon;
+    } | {
+        definition: PolylineDefinition<typeof L.Polyline, typeof L.Popup>['infoWindow'];
+        element: L.Polyline;
     }): L.Popup;
     protected doFitBoundsToMarkers(): void;
     centerValueChanged(): void;
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
index cdbad0f8..088c2706 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -8,6 +8,7 @@ class default_1 extends Controller {
         this.markers = new Map();
         this.infoWindows = [];
         this.polygons = new Map();
+        this.polylines = new Map();
     }
     connect() {
         const options = this.optionsValue;
@@ -15,6 +16,7 @@ class default_1 extends Controller {
         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();
         }
@@ -22,6 +24,7 @@ class default_1 extends Controller {
             map: this.map,
             markers: [...this.markers.values()],
             polygons: [...this.polygons.values()],
+            polylines: [...this.polylines.values()],
             infoWindows: this.infoWindows,
         });
     }
@@ -41,6 +44,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 });
@@ -73,7 +84,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']);
             }
         });
@@ -83,6 +94,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,
@@ -91,6 +118,7 @@ default_1.values = {
     fitBoundsToMarkers: Boolean,
     markers: Array,
     polygons: Array,
+    polylines: Array,
     options: Object,
 };
 
@@ -148,6 +176,23 @@ class map_controller extends default_1 {
         }
         return polygon;
     }
+    removePolygon(polygon) {
+        polygon.remove();
+    }
+    doCreatePolyline(definition) {
+        const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
+        const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
+        if (title) {
+            polyline.bindPopup(title);
+        }
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polyline });
+        }
+        return polyline;
+    }
+    removePolyline(polyline) {
+        polyline.remove();
+    }
     doCreateInfoWindow({ definition, element, }) {
         const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
         element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
index eb392d21..86cc334c 100644
--- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
@@ -1,8 +1,14 @@
 import AbstractMapController from '@symfony/ux-map';
-import type { Point, MarkerDefinition, PolygonDefinition } from '@symfony/ux-map';
+import type { Point, MarkerDefinition, PolygonDefinition, PolylineDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
-import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions, PolygonOptions } from 'leaflet';
+import type {
+    MapOptions as LeafletMapOptions,
+    MarkerOptions,
+    PopupOptions,
+    PolylineOptions as PolygonOptions,
+    PolylineOptions,
+} from 'leaflet';
 
 type MapOptions = Pick<LeafletMapOptions, 'center' | 'zoom'> & {
     tileLayer: { url: string; attribution: string; options: Record<string, unknown> };
@@ -16,7 +22,9 @@ export default class extends AbstractMapController<
     PopupOptions,
     typeof L.Popup,
     PolygonOptions,
-    typeof L.Polygon
+    typeof L.Polygon,
+    PolylineOptions,
+    typeof L.Polyline
 > {
     connect(): void {
         L.Marker.prototype.options.icon = L.divIcon({
@@ -91,12 +99,43 @@ export default class extends AbstractMapController<
         return polygon;
     }
 
+    protected removePolygon(polygon: L.Polygon) {
+        polygon.remove();
+    }
+
+    protected doCreatePolyline(definition: PolylineDefinition): L.Polyline {
+        const { '@id': _id, points, title, infoWindow, rawOptions = {} } = definition;
+
+        const polyline = L.polyline(points, { ...rawOptions }).addTo(this.map);
+
+        if (title) {
+            polyline.bindPopup(title);
+        }
+
+        if (infoWindow) {
+            this.createInfoWindow({ definition: infoWindow, element: polyline });
+        }
+
+        return polyline;
+    }
+
+    protected removePolyline(polyline: L.Polyline): void {
+        polyline.remove();
+    }
+
     protected doCreateInfoWindow({
         definition,
         element,
     }:
-        | { definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>; element: L.Marker }
-        | { definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>; element: L.Polygon }): L.Popup {
+        | {
+              definition: MarkerDefinition<typeof L.Marker, typeof L.Popup>['infoWindow'];
+              element: L.Marker;
+          }
+        | { definition: PolygonDefinition<typeof L.Polygon, typeof L.Popup>['infoWindow']; element: L.Polygon }
+        | {
+              definition: PolylineDefinition<typeof L.Polyline, typeof L.Popup>['infoWindow'];
+              element: L.Polyline;
+          }): L.Popup {
         const { headerContent, content, rawOptions = {}, ...otherOptions } = definition;
 
         element.bindPopup([headerContent, content].filter((x) => x).join('<br>'), { ...otherOptions, ...rawOptions });
diff --git a/src/Map/src/Map.php b/src/Map/src/Map.php
index 374b7e1a..529f8af8 100644
--- a/src/Map/src/Map.php
+++ b/src/Map/src/Map.php
@@ -35,6 +35,11 @@ final class Map
          * @var array<Polygon>
          */
         private array $polygons = [],
+
+        /**
+         * @var array<Polyline>
+         */
+        private array $polylines = [],
     ) {
     }
 
@@ -95,6 +100,13 @@ final class Map
         return $this;
     }
 
+    public function addPolyline(Polyline $polyline): self
+    {
+        $this->polylines[] = $polyline;
+
+        return $this;
+    }
+
     public function toArray(): array
     {
         if (!$this->fitBoundsToMarkers) {
@@ -114,6 +126,7 @@ final class Map
             'options' => $this->options ? MapOptionsNormalizer::normalize($this->options) : [],
             'markers' => array_map(static fn (Marker $marker) => $marker->toArray(), $this->markers),
             'polygons' => array_map(static fn (Polygon $polygon) => $polygon->toArray(), $this->polygons),
+            'polylines' => array_map(static fn (Polyline $polyline) => $polyline->toArray(), $this->polylines),
         ];
     }
 
@@ -123,6 +136,7 @@ final class Map
      *     zoom?: float,
      *     markers?: list<array>,
      *     polygons?: list<array>,
+     *     polylines?: list<array>,
      *     fitBoundsToMarkers?: bool,
      *     options?: array<string, mixed>,
      * } $map
@@ -157,6 +171,12 @@ final class Map
         }
         $map['polygons'] = array_map(Polygon::fromArray(...), $map['polygons']);
 
+        $map['polylines'] ??= [];
+        if (!\is_array($map['polylines'])) {
+            throw new InvalidArgumentException('The "polylines" parameter must be an array.');
+        }
+        $map['polylines'] = array_map(Polyline::fromArray(...), $map['polylines']);
+
         return new self(...$map);
     }
 }
diff --git a/src/Map/src/Marker.php b/src/Map/src/Marker.php
index f14082f9..54d61ebe 100644
--- a/src/Map/src/Marker.php
+++ b/src/Map/src/Marker.php
@@ -37,7 +37,7 @@ final readonly class Marker
      *     position: array{lat: float, lng: float},
      *     title: string|null,
      *     infoWindow: array<string, mixed>|null,
-     *     extra: object,
+     *     extra: array,
      * }
      */
     public function toArray(): array
@@ -55,7 +55,7 @@ final readonly class Marker
      *     position: array{lat: float, lng: float},
      *     title: string|null,
      *     infoWindow: array<string, mixed>|null,
-     *     extra: object,
+     *     extra: array,
      * } $marker
      *
      * @internal
diff --git a/src/Map/src/Polygon.php b/src/Map/src/Polygon.php
index 7dd4cc60..d32594ad 100644
--- a/src/Map/src/Polygon.php
+++ b/src/Map/src/Polygon.php
@@ -33,6 +33,13 @@ final readonly class Polygon
 
     /**
      * Convert the polygon to an array representation.
+     *
+     * @return array{
+     *     points: array<array{lat: float, lng: float}>,
+     *     title: string|null,
+     *     infoWindow: array<string, mixed>|null,
+     *     extra: array,
+     * }
      */
     public function toArray(): array
     {
@@ -49,7 +56,7 @@ final readonly class Polygon
      *     points: array<array{lat: float, lng: float}>,
      *     title: string|null,
      *     infoWindow: array<string, mixed>|null,
-     *     extra: object,
+     *     extra: array,
      * } $polygon
      *
      * @internal
diff --git a/src/Map/src/Polyline.php b/src/Map/src/Polyline.php
new file mode 100644
index 00000000..46302136
--- /dev/null
+++ b/src/Map/src/Polyline.php
@@ -0,0 +1,77 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\UX\Map;
+
+use Symfony\UX\Map\Exception\InvalidArgumentException;
+
+/**
+ * Represents a polyline on a map.
+ *
+ * @author [Sylvain Blondeau]
+ */
+final readonly class Polyline
+{
+    /**
+     * @param array<string, mixed> $extra Extra data, can be used by the developer to store additional information and use them later JavaScript side
+     */
+    public function __construct(
+        private array $points,
+        private ?string $title = null,
+        private ?InfoWindow $infoWindow = null,
+        private array $extra = [],
+    ) {
+    }
+
+    /**
+     * Convert the polyline to an array representation.
+     *
+     * @return array{
+     *     points: array<array{lat: float, lng: float}>,
+     *     title: string|null,
+     *     infoWindow: array<string, mixed>|null,
+     *     extra: array,
+     * }
+     */
+    public function toArray(): array
+    {
+        return [
+            'points' => array_map(fn (Point $point) => $point->toArray(), $this->points),
+            'title' => $this->title,
+            'infoWindow' => $this->infoWindow?->toArray(),
+            'extra' => $this->extra,
+        ];
+    }
+
+    /**
+     * @param array{
+     *     points: array<array{lat: float, lng: float}>,
+     *     title: string|null,
+     *     infoWindow: array<string, mixed>|null,
+     *     extra: array,
+     * } $polyline
+     *
+     * @internal
+     */
+    public static function fromArray(array $polyline): self
+    {
+        if (!isset($polyline['points'])) {
+            throw new InvalidArgumentException('The "points" parameter is required.');
+        }
+        $polyline['points'] = array_map(Point::fromArray(...), $polyline['points']);
+
+        if (isset($polyline['infoWindow'])) {
+            $polyline['infoWindow'] = InfoWindow::fromArray($polyline['infoWindow']);
+        }
+
+        return new self(...$polyline);
+    }
+}
diff --git a/src/Map/src/Renderer/AbstractRenderer.php b/src/Map/src/Renderer/AbstractRenderer.php
index b79d2e2c..794b08d6 100644
--- a/src/Map/src/Renderer/AbstractRenderer.php
+++ b/src/Map/src/Renderer/AbstractRenderer.php
@@ -84,7 +84,7 @@ abstract readonly class AbstractRenderer implements RendererInterface
 
     private function getMapAttributes(Map $map): array
     {
-        $computeId = fn (array $array) => hash('xxh3', json_encode($array, JSON_THROW_ON_ERROR));
+        $computeId = fn (array $array) => hash('xxh3', json_encode($array, \JSON_THROW_ON_ERROR));
 
         $attrs = $map->toArray();
 
@@ -96,6 +96,10 @@ abstract readonly class AbstractRenderer implements RendererInterface
             $attrs['polygons'][$key]['@id'] = $computeId($polygon);
         }
 
+        foreach ($attrs['polylines'] as $key => $polyline) {
+            $attrs['polylines'][$key]['@id'] = $computeId($polyline);
+        }
+
         return $attrs;
     }
 }
diff --git a/src/Map/src/Twig/MapRuntime.php b/src/Map/src/Twig/MapRuntime.php
index 8b9a1ca2..a729e46e 100644
--- a/src/Map/src/Twig/MapRuntime.php
+++ b/src/Map/src/Twig/MapRuntime.php
@@ -15,6 +15,7 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
 use Symfony\UX\Map\Polygon;
+use Symfony\UX\Map\Polyline;
 use Symfony\UX\Map\Renderer\RendererInterface;
 use Twig\Extension\RuntimeExtensionInterface;
 
@@ -34,12 +35,14 @@ final class MapRuntime implements RuntimeExtensionInterface
      * @param array<string, mixed> $attributes
      * @param array<string, mixed> $markers
      * @param array<string, mixed> $polygons
+     * @param array<string, mixed> $polylines
      */
     public function renderMap(
         ?Map $map = null,
         array $attributes = [],
         ?array $markers = null,
         ?array $polygons = null,
+        ?array $polylines = null,
         ?array $center = null,
         ?float $zoom = null,
     ): string {
@@ -55,8 +58,11 @@ final class MapRuntime implements RuntimeExtensionInterface
         foreach ($markers ?? [] as $marker) {
             $map->addMarker(Marker::fromArray($marker));
         }
-        foreach ($polygons ?? [] as $polygons) {
-            $map->addPolygon(Polygon::fromArray($polygons));
+        foreach ($polygons ?? [] as $polygon) {
+            $map->addPolygon(Polygon::fromArray($polygon));
+        }
+        foreach ($polylines ?? [] as $polyline) {
+            $map->addPolyline(Polyline::fromArray($polyline));
         }
         if (null !== $center) {
             $map->center(Point::fromArray($center));
@@ -70,7 +76,7 @@ final class MapRuntime implements RuntimeExtensionInterface
 
     public function render(array $args = []): string
     {
-        $map = array_intersect_key($args, ['map' => 0, 'markers' => 0, 'polygons' => 0, 'center' => 1, 'zoom' => 2]);
+        $map = array_intersect_key($args, ['map' => 0, 'markers' => 1, 'polygons' => 2, 'polylines' => 3, 'center' => 4, 'zoom' => 5]);
         $attributes = array_diff_key($args, $map);
 
         return $this->renderMap(...$map, attributes: $attributes);
diff --git a/src/Map/src/Twig/UXMapComponent.php b/src/Map/src/Twig/UXMapComponent.php
index 39e362b3..4c167420 100644
--- a/src/Map/src/Twig/UXMapComponent.php
+++ b/src/Map/src/Twig/UXMapComponent.php
@@ -14,6 +14,7 @@ namespace Symfony\UX\Map\Twig;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
 use Symfony\UX\Map\Polygon;
+use Symfony\UX\Map\Polyline;
 
 /**
  * @author Simon André <smn.andre@gmail.com>
@@ -35,4 +36,9 @@ final class UXMapComponent
      * @var Polygon[]
      */
     public array $polygons;
+
+    /**
+     * @var Polyline[]
+     */
+    public array $polylines;
 }


Test patch:
diff --git a/src/Map/assets/test/abstract_map_controller.test.ts b/src/Map/assets/test/abstract_map_controller.test.ts
index 118241f0..b3d2623a 100644
--- a/src/Map/assets/test/abstract_map_controller.test.ts
+++ b/src/Map/assets/test/abstract_map_controller.test.ts
@@ -35,6 +35,15 @@ 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 };
@@ -42,6 +51,9 @@ class MyMapController extends AbstractMapController {
         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() {
@@ -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>
         `);
@@ -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');
 
@@ -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',
@@ -122,6 +138,11 @@ describe('AbstractMapController', () => {
                 infoWindow: 'infoWindow',
                 polygon: null,
             },
+            {
+                headerContent: 'Polyline',
+                infoWindow: 'infoWindow',
+                polyline: null,
+            },
         ]);
     });
 });
diff --git a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
index 76118c05..edee670c 100644
--- a/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Google/assets/test/map_controller.test.ts
@@ -46,6 +46,7 @@ describe('GoogleMapsController', () => {
               data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" 
               data-symfony--ux-google-map--map-markers-value="[]" 
               data-symfony--ux-google-map--map-polygons-value="[]" 
+              data-symfony--ux-google-map--map-polylines-value="[]" 
               style="height: 600px"
           ></div>
         `);
diff --git a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
index c64045c1..d8695096 100644
--- a/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
+++ b/src/Map/src/Bridge/Google/tests/GoogleRendererTest.php
@@ -18,6 +18,7 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
 use Symfony\UX\Map\Polygon;
+use Symfony\UX\Map\Polyline;
 use Symfony\UX\Map\Test\RendererTestCase;
 use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
 
@@ -30,26 +31,26 @@ class GoogleRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map, with minimum options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
         ];
 
         yield 'with every options' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;id&quot;:&quot;gmap&quot;,&quot;language&quot;:&quot;fr&quot;,&quot;region&quot;:&quot;FR&quot;,&quot;nonce&quot;:&quot;abcd&quot;,&quot;retries&quot;:10,&quot;url&quot;:&quot;https:\/\/maps.googleapis.com\/maps\/api\/js&quot;,&quot;version&quot;:&quot;quarterly&quot;,&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;id&quot;:&quot;gmap&quot;,&quot;language&quot;:&quot;fr&quot;,&quot;region&quot;:&quot;FR&quot;,&quot;nonce&quot;:&quot;abcd&quot;,&quot;retries&quot;:10,&quot;url&quot;:&quot;https:\/\/maps.googleapis.com\/maps\/api\/js&quot;,&quot;version&quot;:&quot;quarterly&quot;,&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key', id: 'gmap', language: 'fr', region: 'FR', nonce: 'abcd', retries: 10, url: 'https://maps.googleapis.com/maps/api/js', version: 'quarterly'),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]" class="map"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--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;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;abd45a2c703af97a&quot;},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;219637cbd8e62ea6&quot;}]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--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;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;abd45a2c703af97a&quot;},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;219637cbd8e62ea6&quot;}]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
@@ -57,15 +58,23 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'with polygons and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->addPolygon(new Polygon(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)]))
                 ->addPolygon(new Polygon(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))),
         ];
 
+        yield 'with polylines and infoWindows' => [
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]"></div>',
+            'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
+            'map' => (clone $map)
+                ->addPolyline(new Polyline(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)]))
+                ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))),
+        ];
+
         yield 'with controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
@@ -77,7 +86,7 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'without controls enabled' => [
-            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:null,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), apiKey: 'api_key'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(
@@ -89,18 +98,18 @@ class GoogleRendererTest extends RendererTestCase
         ];
 
         yield 'with default map id' => [
-            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;DefaultMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;DefaultMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'),
             'map' => (clone $map),
         ];
         yield 'with default map id, when passing options (except the "mapId")' => [
-            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;DefaultMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;DefaultMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'),
             'map' => (clone $map)
                 ->options(new GoogleOptions()),
         ];
         yield 'with default map id overridden by option "mapId"' => [
-            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;CustomMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]"></div>',
+            'expected_renderer' => '<div data-controller="symfony--ux-google-map--map" data-symfony--ux-google-map--map-provider-options-value="{&quot;apiKey&quot;:&quot;my_api_key&quot;}" data-symfony--ux-google-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-google-map--map-zoom-value="12" data-symfony--ux-google-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-google-map--map-options-value="{&quot;mapId&quot;:&quot;CustomMapId&quot;,&quot;gestureHandling&quot;:&quot;auto&quot;,&quot;backgroundColor&quot;:null,&quot;disableDoubleClickZoom&quot;:false,&quot;zoomControlOptions&quot;:{&quot;position&quot;:22},&quot;mapTypeControlOptions&quot;:{&quot;mapTypeIds&quot;:[],&quot;position&quot;:14,&quot;style&quot;:0},&quot;streetViewControlOptions&quot;:{&quot;position&quot;:22},&quot;fullscreenControlOptions&quot;:{&quot;position&quot;:20},&quot;@provider&quot;:&quot;google&quot;}" data-symfony--ux-google-map--map-markers-value="[]" data-symfony--ux-google-map--map-polygons-value="[]" data-symfony--ux-google-map--map-polylines-value="[]"></div>',
             'renderer' => new GoogleRenderer(new StimulusHelper(null), 'my_api_key', defaultMapId: 'DefaultMapId'),
             'map' => (clone $map)
                 ->options(new GoogleOptions(mapId: 'CustomMapId')),
diff --git a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
index c5378eaf..379eb603 100644
--- a/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/test/map_controller.test.ts
@@ -46,6 +46,7 @@ describe('LeafletController', () => {
               data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\\/\\/tile.openstreetmap.org\\/{z}\\/{x}\\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\\&quot;https:\\/\\/www.openstreetmap.org\\/copyright\\&quot;&gt;OpenStreetMap&lt;\\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" 
               data-symfony--ux-leaflet-map--map-markers-value="[]" 
               data-symfony--ux-leaflet-map--map-polygons-value="[]" 
+              data-symfony--ux-leaflet-map--map-polylines-value="[]" 
               style="height: 600px"
           ></div>
 
diff --git a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
index b7e1394e..81d53160 100644
--- a/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
+++ b/src/Map/src/Bridge/Leaflet/tests/LeafletRendererTest.php
@@ -17,6 +17,7 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
 use Symfony\UX\Map\Polygon;
+use Symfony\UX\Map\Polyline;
 use Symfony\UX\Map\Test\RendererTestCase;
 use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
 
@@ -29,20 +30,20 @@ class LeafletRendererTest extends RendererTestCase
             ->zoom(12);
 
         yield 'simple map' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
         ];
 
         yield 'with custom attributes' => [
-            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" class="map"></div>',
+            'expected_render' => '<div data-controller="my-custom-controller symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]" class="map"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => $map,
             'attributes' => ['data-controller' => 'my-custom-controller', 'class' => 'map'],
         ];
 
         yield 'with markers and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--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;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;abd45a2c703af97a&quot;},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;219637cbd8e62ea6&quot;}]" data-symfony--ux-leaflet-map--map-polygons-value="[]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--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;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;abd45a2c703af97a&quot;},{&quot;position&quot;:{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},&quot;title&quot;:&quot;Lyon&quot;,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Lyon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;219637cbd8e62ea6&quot;}]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[]"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => (clone $map)
                 ->addMarker(new Marker(new Point(48.8566, 2.3522), 'Paris'))
@@ -50,11 +51,19 @@ class LeafletRendererTest extends RendererTestCase
         ];
 
         yield 'with polygons and infoWindows' => [
-            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]"></div>',
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]" data-symfony--ux-leaflet-map--map-polylines-value="[]"></div>',
             'renderer' => new LeafletRenderer(new StimulusHelper(null)),
             'map' => (clone $map)
                 ->addPolygon(new Polygon(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)]))
                 ->addPolygon(new Polygon(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))),
         ];
+
+        yield 'with polylines and infoWindows' => [
+            'expected_render' => '<div data-controller="symfony--ux-leaflet-map--map" data-symfony--ux-leaflet-map--map-provider-options-value="{}" data-symfony--ux-leaflet-map--map-center-value="{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}" data-symfony--ux-leaflet-map--map-zoom-value="12" data-symfony--ux-leaflet-map--map-fit-bounds-to-markers-value="false" data-symfony--ux-leaflet-map--map-options-value="{&quot;tileLayer&quot;:{&quot;url&quot;:&quot;https:\/\/tile.openstreetmap.org\/{z}\/{x}\/{y}.png&quot;,&quot;attribution&quot;:&quot;\u00a9 &lt;a href=\&quot;https:\/\/www.openstreetmap.org\/copyright\&quot;&gt;OpenStreetMap&lt;\/a&gt;&quot;,&quot;options&quot;:[]},&quot;@provider&quot;:&quot;leaflet&quot;}" data-symfony--ux-leaflet-map--map-markers-value="[]" data-symfony--ux-leaflet-map--map-polygons-value="[]" data-symfony--ux-leaflet-map--map-polylines-value="[{&quot;points&quot;:[{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522},{&quot;lat&quot;:48.8566,&quot;lng&quot;:2.3522}],&quot;title&quot;:null,&quot;infoWindow&quot;:null,&quot;extra&quot;:[],&quot;@id&quot;:&quot;2dc7aa290e8fc88a&quot;},{&quot;points&quot;:[{&quot;lat&quot;:1.1,&quot;lng&quot;:2.2},{&quot;lat&quot;:3.3,&quot;lng&quot;:4.4},{&quot;lat&quot;:5.5,&quot;lng&quot;:6.6}],&quot;title&quot;:null,&quot;infoWindow&quot;:{&quot;headerContent&quot;:null,&quot;content&quot;:&quot;Polygon&quot;,&quot;position&quot;:null,&quot;opened&quot;:false,&quot;autoClose&quot;:true,&quot;extra&quot;:[]},&quot;extra&quot;:[],&quot;@id&quot;:&quot;d6cab193e60e5faf&quot;}]"></div>',
+            'renderer' => new LeafletRenderer(new StimulusHelper(null)),
+            'map' => (clone $map)
+                ->addPolyline(new Polyline(points: [new Point(48.8566, 2.3522), new Point(48.8566, 2.3522), new Point(48.8566, 2.3522)]))
+                ->addPolyline(new Polyline(points: [new Point(1.1, 2.2), new Point(3.3, 4.4), new Point(5.5, 6.6)], infoWindow: new InfoWindow(content: 'Polygon'))),
+        ];
     }
 }
diff --git a/src/Map/tests/MapFactoryTest.php b/src/Map/tests/MapFactoryTest.php
index bdf737cb..25194376 100644
--- a/src/Map/tests/MapFactoryTest.php
+++ b/src/Map/tests/MapFactoryTest.php
@@ -53,6 +53,13 @@ class MapFactoryTest extends TestCase
         $this->assertSame($array['polygons'][0]['title'], $polygons[0]['title']);
         $this->assertSame($array['polygons'][0]['infoWindow']['headerContent'], $polygons[0]['infoWindow']['headerContent']);
         $this->assertSame($array['polygons'][0]['infoWindow']['content'], $polygons[0]['infoWindow']['content']);
+
+        $this->assertCount(1, $polylines = $map->toArray()['polylines']);
+        $this->assertEquals($array['polylines'][0]['points'], $polylines[0]['points']);
+        $this->assertEquals($array['polylines'][0]['points'], $polylines[0]['points']);
+        $this->assertSame($array['polylines'][0]['title'], $polylines[0]['title']);
+        $this->assertSame($array['polylines'][0]['infoWindow']['headerContent'], $polylines[0]['infoWindow']['headerContent']);
+        $this->assertSame($array['polylines'][0]['infoWindow']['content'], $polylines[0]['infoWindow']['content']);
     }
 
     public function testToArrayFromArray(): void
@@ -154,6 +161,30 @@ class MapFactoryTest extends TestCase
         Map::fromArray($array);
     }
 
+    public function testFromArrayWithInvalidPolylines(): void
+    {
+        $array = self::createMapArray();
+        $array['polylines'] = 'invalid';
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The "polylines" parameter must be an array.');
+        Map::fromArray($array);
+    }
+
+    public function testFromArrayWithInvalidPolyline(): void
+    {
+        $array = self::createMapArray();
+        $array['polylines'] = [
+            [
+                'invalid',
+            ],
+        ];
+
+        $this->expectException(\InvalidArgumentException::class);
+        $this->expectExceptionMessage('The "points" parameter is required.');
+        Map::fromArray($array);
+    }
+
     private static function createMapArray(): array
     {
         return [
@@ -198,6 +229,29 @@ class MapFactoryTest extends TestCase
                     ],
                 ],
             ],
+            'polylines' => [
+                [
+                    'points' => [
+                        [
+                            'lat' => 48.858844,
+                            'lng' => 2.294351,
+                        ],
+                        [
+                            'lat' => 48.853,
+                            'lng' => 2.3499,
+                        ],
+                        [
+                            'lat' => 48.8566,
+                            'lng' => 2.3522,
+                        ],
+                    ],
+                    'title' => 'Polyline 1',
+                    'infoWindow' => [
+                        'headerContent' => 'Polyline 1',
+                        'content' => 'Polyline 1',
+                    ],
+                ],
+            ],
         ];
     }
 }
diff --git a/src/Map/tests/MapTest.php b/src/Map/tests/MapTest.php
index 7445dbc8..b176e0d9 100644
--- a/src/Map/tests/MapTest.php
+++ b/src/Map/tests/MapTest.php
@@ -18,6 +18,7 @@ use Symfony\UX\Map\Map;
 use Symfony\UX\Map\Marker;
 use Symfony\UX\Map\Point;
 use Symfony\UX\Map\Polygon;
+use Symfony\UX\Map\Polyline;
 
 class MapTest extends TestCase
 {
@@ -66,6 +67,7 @@ class MapTest extends TestCase
             'options' => $array['options'],
             'markers' => [],
             'polygons' => [],
+            'polylines' => [],
         ], $array);
     }
 
@@ -85,6 +87,7 @@ class MapTest extends TestCase
             'options' => $array['options'],
             'markers' => [],
             'polygons' => [],
+            'polylines' => [],
         ], $array);
     }
 
@@ -136,6 +139,30 @@ class MapTest extends TestCase
                     autoClose: true,
                 ),
             ))
+            ->addPolyline(new Polyline(
+                points: [
+                    new Point(48.858844, 2.294351),
+                    new Point(48.853, 2.3499),
+                    new Point(48.8566, 2.3522),
+                ],
+                title: 'Polyline 1',
+                infoWindow: null,
+            ))
+            ->addPolyline(new Polyline(
+                points: [
+                    new Point(45.764043, 4.835659),
+                    new Point(45.75, 4.85),
+                    new Point(45.77, 4.82),
+                ],
+                title: 'Polyline 2',
+                infoWindow: new InfoWindow(
+                    headerContent: '<b>Polyline 2</b>',
+                    content: 'A polyline around Lyon with some additional info.',
+                    position: new Point(45.764, 4.8357),
+                    opened: true,
+                    autoClose: true,
+                ),
+            ))
         ;
 
         self::assertEquals([
@@ -217,6 +244,35 @@ class MapTest extends TestCase
                     'extra' => [],
                 ],
             ],
+            'polylines' => [
+                [
+                    'points' => [
+                        ['lat' => 48.858844, 'lng' => 2.294351],
+                        ['lat' => 48.853, 'lng' => 2.3499],
+                        ['lat' => 48.8566, 'lng' => 2.3522],
+                    ],
+                    'title' => 'Polyline 1',
+                    'infoWindow' => null,
+                    'extra' => [],
+                ],
+                [
+                    'points' => [
+                        ['lat' => 45.764043, 'lng' => 4.835659],
+                        ['lat' => 45.75, 'lng' => 4.85],
+                        ['lat' => 45.77, 'lng' => 4.82],
+                    ],
+                    'title' => 'Polyline 2',
+                    'infoWindow' => [
+                        'headerContent' => '<b>Polyline 2</b>',
+                        'content' => 'A polyline around Lyon with some additional info.',
+                        'position' => ['lat' => 45.764, 'lng' => 4.8357],
+                        'opened' => true,
+                        'autoClose' => true,
+                        'extra' => [],
+                    ],
+                    'extra' => [],
+                ],
+            ],
         ], $map->toArray());
     }
 }