Skip to main content
APIs 9 min read

Create a multi-stop route for your fleet

Create a multi-stop route for your fleet

One of the best things about being a developer (outside of code reviews of course) is knowing that you can use your development skills to help solve problems. There isn't a tool for what you need? Build your own! Of course, that assumes an infinite amount of time and actual ability, but let's just assume that isn't an issue. Today's demo is a perfect example of that in action. I recently had a problem that wasn't immediately solvable by an existing tool, so I used our platform to get it done. So what was the problem?

I wanted to vote early, something many people across the country are already doing. In my area, I had three different polling places available to me for early voting. However, I wasn't sure which one was best. What I wanted to do was go to a map, plot my house, and then plot all three locations so I could figure out what was going to work best for me. Not just in terms of location but how long the drive is, what else is around them in case I could knock out a few more errands, and so forth.

From what I could see though, no mapping tool really fit that need. Most mapping tools making it easy to map from point A to B, but I couldn't find one that would let me map from A to B1, A to B2, and so forth. (And I swear, I looked for a long time too. Maybe even up to five minutes of intense Internet searching.)

I thought it would be fun to try to build a demo of this using our Maps product. I'd use our JavaScript Maps for rendering and our Geocoding and Routing to work with my input. I decided for a simple interface. I prompt for your starting location and then have fields for one to four destinations.

You can enter each field using "regular" addresses which then get geocoded into a precise location. Here's an example (and no, this isn't my home):

Each of the destinations has a marker that you can click for more information. For my initial demo, just the total driving time:

You can play with this demo yourself (https://cfjedimaster.github.io/heredemos/mapsjs/multilocations/) and the complete source may be found here: https://github.com/cfjedimaster/heredemos/tree/master/mapsjs/multilocations

I won't go over every line of code, but let's take a look at how it was built. For my layout I used a CSS grid to create the left "panel" and a place for the map.

Copied
        .container {
    display: grid;
    grid-template-columns: 20% 80%;
}

#panel {
    background-color: bisque;
    padding: 8px;
}

#map {
    height: 100vh;
    width: 100%;
}
  

The panel itself is simple HTML tied to JavaScript to handle when values change.

Copied
        <div id="panel">
    <h2>Multi Router</h2>

    <p>
    Enter a starting address and then enter your muliple addresses. The tool 
    will tell you routing information from your starting position to each of them.
    </p>

    <p>
    <label for="startingAddress">Starting Address:</label><br/>
    <input type="text" id="startingAddress">
    </p>

    <p>
    <label for="dest1">Destination 1</label><br/>
    <input type="text" id="dest1" class="destField" disabled>
    </p>
    <p>
    <label for="dest2">Destination 2</label><br/>
    <input type="text" id="dest2" class="destField" disabled>
    </p>
    <p>
    <label for="dest3">Destination 3</label><br/>
    <input type="text" id="dest3" class="destField" disabled>
    </p>
    <p>
    <label for="dest4">Destination 4</label><br/>
    <input type="text" id="dest4" class="destField" disabled>
    </p>

    <p id="credit">
    Icons made by <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> from
    <a href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
    </p>

</div>
  

Notice that the destination fields are all disabled. They will get enabled once a starting address is added. Now let's tackle the JavaScript. I'm going to assume some familiarity with our JavaScript maps, but here's boilerplate code that simply drops a map on a web page:

Copied
        platform = new H.service.Platform({
    'apikey': KEY
});

// Obtain the default map types from the platform object:
let defaultLayers = platform.createDefaultLayers();

map = new H.Map(
    document.getElementById('map'),
    defaultLayers.vector.normal.map,
    {
        zoom: 5,
        center: { lat: 39.83, lng: -98.58 },
        pixelRatio: window.devicePixelRatio || 1,
        padding: {top: 50, left: 50, bottom: 50, right: 50}
    }
);


let behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

// Create the default UI:
ui = H.ui.UI.createDefault(map, defaultLayers);
  

The only thing I'll call out here is my center which came from me Googling for the center of America. The padding value is interesting. It doesn't actually add a padding around the map itself, ie a white or transparent border. Rather, it provides some padding when working with UI items. So for example, later we're going to center the map around a set of markers and this padding will ensure that all the markers are visible and not on the "edge" of the view port.

Along with setting up the map, there's some additional prep work we do. First, I create a group that will store our markers and set up some custom icons:

Copied
        markerGroup = new H.map.Group();
map.addObject(markerGroup);

homeIcon = new H.map.Icon('home.png');
destIcon = new H.map.Icon('marker.png');
  

Next, I create an instance of the routing and geocoding service:

Copied
        router = platform.getRoutingService(null,8);
geocoder = platform.getSearchService();
  

These objects provide wrappers to our services and make it a bit easier to use in JavaScript applications. Finally, I do a bit of DOM work - getting handles to my form fields and adding some event listeners:

Copied
        startingAddress = document.querySelector('#startingAddress');
startingAddress.addEventListener('change', doStartingLocation);

destFields = document.querySelectorAll('.destField');
destFields.forEach(d => {
    d.addEventListener('change', doDestination);
});
  

The first thing I built was handling input in the starting address field. We have to take the input, geocode it, and then add a marker. First, here's the code handling changes to the text field:

Copied
        async function doStartingLocation() {
    console.log('address change', startingAddress.value);
    if(!startingAddress.value) {
        disableDestinationFields();
        return;
    } else enableDestinationFields();
    homePos = await geocode(startingAddress.value);
    if(homePos) addHomeMarker(homePos);
}
  

I do a few basic things here. I have logic to either disable or enable the destination fields based on an input. I'm not going to share that function as it just does the DOM changes. If you did have a value, I then geocode it:

Copied
        async function geocode(s) {
    let params = {
        q:s
    };

    return new Promise((resolve, reject) => {
        geocoder.geocode(
            params,
            r => {
                resolve(r.items[0].position);
            },
            e => reject(e)
        );
    });
}
  

This is an example of using the "service helpers" in the MapsJS library. The last bit handles adding the home marker:

Copied
        function addHomeMarker(pos) {
    if(homeMarker) markerGroup.removeObject(homeMarker);
    homeMarker = new H.map.Marker({lat:pos.lat, lng:pos.lng}, {icon:homeIcon});
    homeMarker.setData("You are HERE")
    markerGroup.addObject(homeMarker);
    map.setCenter(homeMarker.getGeometry());
}
  

This function handles removing any previous marker if it exists. It then adds the marker and centers the map.

The next set of functionality handles destinations. I've got one event handler for all the destination fields. The handler is responsible for geocoding the input, adding a marker, and then drawing a route. Here's the main handler:

Copied
        async function doDestination(e) {
    let id = e.target.id;
    if(!e.target.value) return;
    let pos = await geocode(e.target.value);

    let routeRequestParams = {
        routingMode: 'fast',
        transportMode: 'car',
        origin: homePos.lat+','+homePos.lng, 
        destination: pos.lat+','+pos.lng,  
        return: 'polyline,turnByTurnActions,actions,instructions,travelSummary'
    };

    let route = await getRoute(routeRequestParams);
    let totalTime = formatTime(route.sections[0].travelSummary.duration);

    addDestMarker(pos, id, `
<p>
    <strong>Destination:</strong> ${e.target.value}<br/>
    <strong>Driving Time:</strong> ${totalTime}
</p>`);

    addRouteShapeToMap(route, id);
}
  

It's using the same geocode wrapper I had before. It then calls out to a routing wrapper:

Copied
        async function getRoute(p) { 

    let params = {
        origin:p.origin, 
        destination:p.destination,
        transportMode:p.transportMode,
        routingMode:p.routingMode,
        return:p.return,
    }

    return new Promise((resolve, reject) => {
        router.calculateRoute(
            params, 
            r => {
                resolve(r.routes[0]);
            }, 
            e => reject(e)
        );
    });

}
  

We get the total driving time from the route result and use it to create a marker. I abstracted that part out into addDestMarker:

Copied
        function addDestMarker(pos, id, label) {
    if(destMarkers[id]) markerGroup.removeObject(destMarkers[id]);
    destMarkers[id] = new H.map.Marker({lat:pos.lat, lng:pos.lng}, {icon:destIcon});
    destMarkers[id].setData(`<div class="bubble">${label}</div>`);

    destMarkers[id].addEventListener('tap', e => {
        if(bubble) ui.removeBubble(bubble);
        bubble = new H.ui.InfoBubble(e.target.getGeometry(), {
            content:e.target.getData()
        });
        ui.addBubble(bubble);
    });
    markerGroup.addObject(destMarkers[id]);
}
  

This function uses a window level object, destMarkers, as a way to remember a marker and it's associated destination field. This is what let's us remove the marker made for the second destination, for example, when it changes while leaving the rest alone. An event listener is added that shows the text data passed to it. The final bit, addRouteShapeToMap, is code I got right from our docs that add the route API polygon shape to a map:

Copied
        function addRouteShapeToMap(route,id) {

    // remove existing route
    if(destRoutes[id]) {
        destRoutes[id].removeAll();
        markerGroup.removeObject(destRoutes[id]);
    }

    destRoutes[id] = new H.map.Group();

    route.sections.forEach((section) => {
        // decode LineString from the flexible polyline
        let linestring = H.geo.LineString.fromFlexiblePolyline(section.polyline);

        // Create a polyline to display the route:
        let polyline = new H.map.Polyline(linestring, {
            style: {
                lineWidth: 4,
                strokeColor: 'rgba(0, 128, 255, 0.7)'
            }
        });

        destRoutes[id].addObject(polyline);

      });

    markerGroup.addObject(destRoutes[id]);
    map.getViewModel().setLookAtData({bounds: markerGroup.getBoundingBox()});

}
  

There's only two modifications here. First, recognizing and removing a previous route for that particular field and then updating the map viewport. (That improvement, amongst others, came from my amazing teammate, Shruti!) And that's it. Again, you can check out the repository and demo to take it for a spin.

Raymond Camden

Raymond Camden

Have your say

Sign up for our newsletter

Why sign up:

  • Latest offers and discounts
  • Tailored content delivered weekly
  • Exclusive events
  • One click to unsubscribe