Search code examples
reactjsnext.jsleafletopenstreetmappolyline

Indicating one way trail direction with arrows in leaflet and OSM


I am currently working on a trail mapping Next.js react app where I fetch data from OpenStreetMap and illustrate trails on a map using leaflet polyline. Some trails have the tag oneway= 'yes' which I would like to represent visually using arrows on the polyline itself, that way it would be easy to tell which direction each trail went.

I have searched and tried many different avenues to try and replicate this feature in my app, using leaflet plugins such as leaflet-polylineDecorator or leaflet-arrowheads. I even tried rendering a bunch of markers and rotating them to point towards the next node, but I didn't manage to make it work and it seemed too complicated a solution for something so simple.

I am looking at implementing a simple and lightweight solution to avoid slowing the app down. Is there an easy solution which I am missing?


Solution

  • You can relatively easy adding decoration with leaflet.polylineDecorator.

    You'll need to create a decorator for each feature that is oneway = 'yes'. I recommend to add the decorators to a separate L.FeatureGroup (instead to the map directly) so you can easily remove, show or hide them depending on the zoom level.

    Here is an example (using vanilla JavaScript and a test GeoJSON with polylines and a property oneway; it will require some adaptation to your dataset and for Next.js):

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
    
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>StackOverflow Question 79028922 - Trail Directions</title>
    
        <link rel="stylesheet" href="leaflet.css" />
        <script src="leaflet.js"></script>
    
        <script src="leaflet.polylineDecorator.js"></script>
    
        <style>
            body {
                margin: 0;
            }
    
            #map {
                height: 100vh;
            }
        </style>
    
    </head>
    
    <body>
    
        <div id="map"></div>
    
        <script>
    
            const coordinate = [46.948056, 7.4475];
    
            const map = L.map('map').setView(coordinate, 15);
    
            L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
                maxZoom: 22,
                // add link to attribution, omitted in this code example
                // due to width limit, i.e., avoid horizontal scrolling
                attribution: '&copy; OpenStreetMap',
            }).addTo(map);
    
            fetch('trails.geojson')
                .then(response => response.json())
                .then(trails => {
    
                    const trailLayer = L.geoJSON(trails, {
                        style: (feature) => {
                            const isOneWay = feature.properties['oneway'] == 'yes';
                            return {
                                color: isOneWay ? '#a14242' : '#160042',
                            };
                        }, 
                    }).addTo(map);
    
                    // create decorators
                    const decorators = trailLayer.getLayers()
                        .filter(layer => layer.feature.properties['oneway'] == 'yes')
                        .map(layer => {
    
                            // swap lat/lon (needed as GeoJSON uses a different order)
                            const coordinates = layer.feature.geometry.coordinates
                                .map(c => [c[1], c[0]]);
    
                            // create the decorator for the trail
                            return L.polylineDecorator(L.polyline(coordinates), {
                                patterns: [{ 
                                    offset: '5%', 
                                    repeat: '50px', 
                                    symbol: L.Symbol.arrowHead({ 
                                        pixelSize: 10, 
                                        polygon: false, 
                                        pathOptions: { 
                                            stroke: true, 
                                            color: '#a14242' 
                                        } 
                                    }) 
                                }]
                            });
    
                        });
    
                    const featureGroup = L.featureGroup(decorators, {}).addTo(map);
    
                    map.on('zoom', event => {
                        featureGroup.clearLayers();
                        if (map.getZoom() >= 15) {
                            decorators.forEach(decorator => {
                                featureGroup.addLayer(decorator);
                            });
                        }
                    });
    
                });
    
        </script>
    
    </body>
    
    </html>
    

    The file trails.geojson can be found here.

    I also uploaded the code to a GitHub repository here and a demo can be found here.


    I think using a library such as leaflet.polylineDecorator or leaflet-arrowheads is probably the most lightweight solution as you would need to manipulate the SVG paths manually otherwise.


    Addendum: In leaflet.polylineDecorator's README.md there is a section recommending two light-weight alternatives for simpler cases. Leaflet.TextPath could be worth to be investigated.