Search code examples
javascriptleafletgpx

Drawing GPX tracks with shadows using Leaflet.js and Leaflet GPX


I am using Leaflet.js and Leaflet GPX to display a number of GPX tracks on a map. The tracks should have a white shadow so that they stand out better from the map. To do this, each track is added twice to the map, first as a shadow (wider line in white) and then as an actual track (thinner line). The order in which the elements are added to the map should actually be retained by Leaflet.

const trackColors = ['#FF6600', '#FF0066', '#6600FF', '#0066FF'];

// Create map
let map = L.map ('map');

// Create tile layer
L.tileLayer ('https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png').addTo (map);

// Add GPX tracks
for (let i = 0; i < tracks.length; i++)
{
  // Create white track shadow, 8 pixels wide
  let trackShadow = new L.GPX (tracks [i],
  {
    async: true,
    marker_options:
    {
      startIconUrl: null,
      endIconUrl:   null,
      shadowUrl:    null
    },
    parseElements: ['track', 'route'],
    polyline_options:
    {
      color: '#FFFFFF',
      opacity: 0.6,
      weight: 8,
      lineCap: 'round'
    }
  }).addTo (map);

  // Create colored track, 4 pixels wide
  let track = new L.GPX (tracks [i],
  {
    async: true,
    marker_options:
    {
      startIconUrl: null,
      endIconUrl:   null,
      shadowUrl:    null
    },
    parseElements: ['track', 'route'],
    polyline_options:
    {
      color:   trackColors [i % trackColors.length],
      opacity: 1.0,
      weight:  4,
      lineCap: 'round'
    }
  }).addTo (map);
}

Unfortunately, it sometimes happens that the track shadow is drawn OVER the actual track, which means that the elements are automatically rearranged within Leaflet.

  • Is it possible to switch off the sorting of elements?
  • Is there perhaps an event in which I can manually reorder all tracks using bringToBack() and bringToFront() before the first drawing?

Solution

  • Since you're loading the *.gpx files asynchronously, and you're doing it twice (once for the shadow, and the second time for the original track), you're most likely running into a race condition. That would explain for this part of your question:

    it sometimes happens that the track shadow is drawn OVER the actual track

    You can resolve the issue with loading individual tracks just once, and then applying the data afterwards. For example, like in the following code. Be sure to read the comments.

    const trackColors = ['#FF6600', '#FF0066', '#6600FF', '#0066FF'];
    /* this is new - since I didn't have access to your original GPX files, I'm using these - found via Google search */
    const tracks = [
        'https://assets.codepen.io/2511086/Mendick+Hill-compressed.gpx',
        'https://raw.githubusercontent.com/gps-touring/sample-gpx/master/BrittanyJura/Newhaven_Brighton.gpx',
        'https://raw.githubusercontent.com/gps-touring/sample-gpx/master/BrittanyJura/Southampton_Portsmouth.gpx'
    ];
    // Create map
    let map = L.map ('map');
    
    // Create tile layer
    L.tileLayer ('https://{s}.tile.openstreetmap.de/{z}/{x}/{y}.png').addTo (map);
    
    /* this is new - we need to load the gpx data asynchronously, and to wait for each of them before adding them to the map */
    async function loadGPXData(url) {
        try {
            const response = await fetch(url);
            if (!response.ok) {
                // I'm throwing errors, but you can opt for a silent fail - you can return an empty string
                // and check in the other function, where you're declaring const gpxData
                // If it's an empty string, just continue. Or, you can both do that, and fill up
                // an error string to be displayed in an alert, to inform the user that some
                // of the tracks were not loaded to the map
                throw new Error('Failed to fetch GPX data');
            }
            const gpxData = await response.text();
            return gpxData;
        } catch (error) {
            // The same as above - you can return an empty string, or something prefaced with
            // "error- <actual error>"
            // or you can codify all of your responses to be JSONs with {"error":boolState,"data":data}
            // structure, to have a consistent logic for processing it later on
            console.error('Error loading GPX data:', error);
            throw error;
        }
    }
    
    /* this is new - we have to use the async function, since within it, we're awaiting the gpxData */
    // Since the tracks are loaded asynchronously, and that could take a while, perhaps you could also 
    // inform your users with an overlaying DIV that loading is in progress, and then hide it once
    // everything is done loading (either successfully or not)
    // This isn't present in the sample code I provided, but it shouldn't be too difficult to implement
    // You would just be showing and hiding the DIV
    async function addGPXTracksToMap(tracks) {
        for (let i = 0; i < tracks.length; i++) {
            try {
                const gpxData = await loadGPXData(tracks[i]);
    
                /* this is new - instead of tracks[i], like in your original code, I'm using the retrieved data */
                // Create white track shadow, 8 pixels wide
                let trackShadow = new L.GPX(gpxData, {
                    parseElements: ['track', 'route'],
                    polyline_options: {
                        color: '#FFFFFF',
                        opacity: 0.6,
                        weight: 8,
                        lineCap: 'round'
                    }
                }).addTo(map);
    
                /* this is new - same as above, gpxData, instead of tracks[i]*/
                // Create colored track, 4 pixels wide
                let track = new L.GPX(gpxData, {
                    parseElements: ['track', 'route'],
                    polyline_options: {
                        color: trackColors[i % trackColors.length],
                        opacity: 1.0,
                        weight: 4,
                        lineCap: 'round'
                    }
                }).addTo(map);
            } catch (error) {
                console.error('Error loading GPX data:', error);
            }
        }
    }
    
    addGPXTracksToMap(tracks);
    
    map.setView([55.745, -3.384], 14);
    #map {
      height: 500px;
    }
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" integrity="sha512-h9FcoyWjHcOcmEVkxOfTLnmZFWIH0iZhZT1H2TbOq55xssQGEJHEaIm+PgoUaZbRvQTNTluNOEfb1ZRy6D3BOw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" integrity="sha512-puJW3E/qXDqYp9IfhAI54BJEaWIfloJ7JWs7OeD5i6ruC9JZL1gERT1wjtwXFlh7CjE7ZJ+/vcRZRkIYIb6p4g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-gpx/1.7.0/gpx.js" integrity="sha512-FatQYmF8k/KuvOgmwfRGRqlyzbG13etWz1GOO+JO6YQyhGgw5tVl9ihC9RS8S6iiS18CZAnZBvUoKHlGI6BTPQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <div id="map"></div>