Skip to content

Make a Web Mapping App with Routing and Place Search: Tutorial

“We’ve got a long way to go and a short time to get there”; an excerpt from Jerry Reed’s classic record “Eastbound and Down”. Within those lyrics resides a deeper story of managing time and distance across the roads of America. There were no data hubs in Jerry’s days of trucking and fleet management. There were no ways of saving time beyond those that could be thought up on a whim.

The infrastructure now is much different of course. In today’s world, knowing the road that lies ahead is essential for business efficiency across several industries. Fleet management; retail logistics; sales teams that move around the country; local governments; and road maintenance crews are just a few examples of organizations who benefit from tightly managing their travels.

ThinkGeo Cloud Routing is a great way to inject that much-needed data into your own apps, as it offers really fast route calculation for North America (U.S., Canada and Mexico) plus much of Central America. So, let’s see how we can use the ThinkGeo Cloud to build a routing web app that lets us discover the world around our route, and gives us access to important point-of-interest data that can fuel our trips.

Before we get started with the code, let’s grab a ThinkGeo Cloud API key, which we’ll use to authenticate our requests to the ThinkGeo Cloud API. To get your own API key, you’ll need to sign up for a free ThinkGeo Cloud account at https://cloud.thinkgeo.com which takes just a few seconds. If you get stuck here, ThinkGeo has a handy tutorial video that can get you jump-started.

Now let’s do this.

1. Setting up the background map

Before we can display routes, we need a base map to provide context, and it’s pretty easy to set one up with ThinkGeo Cloud Maps. The first thing you’ll want to do in your web app is reference the ThinkGeo Cloud JavaScript libraries and CSS from their CDN. (These packages are also available on NPM if you prefer.)

https://cdn.thinkgeo.com/vectormap-js/3.0.0/vectormap.js
https://cdn.thinkgeo.com/vectormap-js/3.0.0/vectormap.css
https://cdn.thinkgeo.com/vectormap-icons/3.0.0/webfontloader.js
https://cdn.thinkgeo.com/cloudclient-js/1.0.8/thinkgeocloudclient.js

In your HTML page, add an element for the map to live in. We’ve just inlined some CSS here to make the map fill the screen.

<div id="map" style="position:relative; width:100%; height:100%;"></div>

Now we can create our map using ThinkGeo’s VectorMap.JS library. The map’s base layer will use the ThinkGeo Cloud Maps Vector Tile service to display a detailed street map using modern vector-based map data.

// Define your ThinkGeo Cloud API key here
const apiKey = 'your-thinkgeo-cloud-api-key';

// Create the map's base layer
const mapBaseLayer = new ol.mapsuite.VectorTileLayer('https://cdn.thinkgeo.com/worldstreets-styles/3.0.0/light.json', {
    apiKey: apiKey,
    layerName: 'map-light'
});

Next, let’s define a default view for the map when it starts up. We’ll zoom in to Dallas, TX as an example.

const view = new ol.View({
    center: ol.proj.fromLonLat([-96.751, 32.7498]),
    progressiveZoom: true,
    zoom: 10,
    minZoom: 2,
    maxZoom: 19
});

Finally, we’ll load a custom set of point-of-interest icons for our base map. The icon loader has an event that fires when loading is complete, and we’ll have this event call our initializeMap method when it does.

// Load the base map icon set and initialize the map when done
WebFont.load({
    custom: {
        families: ["vectormap-icons"],
        urls: ["https://cdn.thinkgeo.com/vectormap-icons/3.0.0/vectormap-icons.css"]
    },
    active: initializeMap
});

Now let’s take a look at our base map. So far so good, right?

2. Calculating a route from A to B

Since this is a routing app we’re building, next we’re going to add a start and end point to the map, and have the ThinkGeo Cloud calculate the shortest route between them. For brevity, we’ll use a hard-coded start and end point in the Dallas area, but you can use any latitude and longitude coordinates you wish (or gather them from map interaction).

First we need to define what our A and B points will look like. You can use any icon image you want instead of the sample ones from ThinkGeo’s servers.

// Set up styles for icon points
const styles = {
    start: new ol.style.Style({
        image: new ol.style.Icon({
            anchor: [0.5, 0.9],
            anchorXUnits: 'fraction',
            anchorYUnits: 'fraction',
            opacity: 1,
            crossOrigin: 'Anonymous',
            src: 'https://samples.thinkgeo.com/cloud/example/image/end-point-letter/end-point-a.png'
        })
    }),
    end: new ol.style.Style({
        image: new ol.style.Icon({
            anchor: [0.5, 0.9],
            anchorXUnits: 'fraction',
            anchorYUnits: 'fraction',
            opacity: 1,
            crossOrigin: 'Anonymous',
            src: 'https://samples.thinkgeo.com/cloud/example/image/end-point-letter/end-point-b.png'
        })
    }),
}

Now to set the coordinates for these points and turn them into vector features for our map:

// Show the route start and end points on the map.
let vectorSource = new ol.source.Vector();
let startFeature, endFeature;
let startPoint = [-97.1598, 32.7599];
let endPoint = [-96.3213, 32.7203];
const showRouteEndpoints = () => {
    startFeature = new ol.Feature({
        geometry: new ol.geom.Point(ol.proj.fromLonLat(startPoint)),
        name: 'start'
    });
    endFeature = new ol.Feature({
        geometry: new ol.geom.Point(ol.proj.fromLonLat(endPoint)),
        name: 'end'
    });
    
    startFeature.setStyle(styles.start);
    endFeature.setStyle(styles.end);
    vectorSource.addFeature(startFeature);
    vectorSource.addFeature(endFeature);
}

You might have noticed that we defined a vectorSource variable but didn’t use it yet. Go back to your initializeMap method and insert the following snippet at the end of it:

// Add a map layer for showing the start and end points.
map.addLayer(new ol.layer.Vector({source:vectorSource}));
showRouteEndpoints();

Finally, let’s find a route from A to B, using the ThinkGeo Cloud routing client. Add the following code to your JavaScript:

// Generate the route.
const routingClient = new tg.RoutingClient(apiKey);
const performRouting = () => {
    const lineGeom = new ol.geom.LineString(
        [ol.proj.fromLonLat(startPoint), ol.proj.fromLonLat(endPoint)]
    );
  
    // Remove any existing route from the map before recalculating.
    vectorSource.getFeatures().some(feature => {
        if (feature.get('name') === 'line') {
            vectorSource.removeFeature(feature);
            return true;
        }
    })
  
    const handleRoutingResponse = (status, response) => {
        if (status === 200) {
            // Draw the route line on the map.
            let drivingLine = drawDrivingLine(response.data);
        }
    }
    
    const waypoints = [
        { x: startPoint[0], y: startPoint[1] },
        { x: endPoint[0], y: endPoint[1] }
    ];
  
    routingClient.getRoute(waypoints, handleRoutingResponse);
}

// Draw a line on the map following the route.
const drawDrivingLine = (res) => {
    // Reproject result from Decimal Degrees (lat/long) to Web Mercator 
    // (our map's coordinate system)
    const drivingLine = (new ol.format.WKT()).readFeature(res.routes[0].geometry, {
        dataProjection: 'EPSG:4326',
        featureProjection: 'EPSG:3857'
    });
    drivingLine.setStyle(new ol.style.Style({
        stroke: new ol.style.Stroke({
            width: 6,
            color: [34, 109, 214, 0.9]
        })
    }));
    drivingLine.set('name', 'line');
    vectorSource.addFeature(drivingLine);
  
    return drivingLine;
}

And finally, to kick off the process automatically on page load, call the performRouting method at the end of your initializeMap routine:

performRouting();

Refresh your browser now, and you’ll see a blue line connecting point A to B along the shortest available route!

3. Finding points of interest along your route

Knowing the fastest way to get from place to place is great, but what if you want to know what you might encounter along your way? Maybe you’re looking for hotels to stop at, restaurants to grab a bite to eat, or gas stations to keep your car’s tank topped up. We can easily add that extra dimension to our app by using the ThinkGeo Cloud Reverse Geocoding service. Reverse geocoding is the process of taking a set of spatial coordinates and converting them into the nearest address or point of interest.

We’ll start by setting up a menu that lets the user choose what type of places they want to search for — hotels, restaurants, or gas stations. While we’re at it, let’s add a button that will regenerate the results when clicked. We can place this new markup in our HTML page after our map container element:

<div style="position: absolute; left: 1em; top: 1em; width: 260px; border: 1px solid #666; background: #fff; padding: 10px">
    <label>Find Along Route:</label>
    <select id="place-type">
        <option value="hotel,motel" tag="hotel">Hotels</option>
        <option value="bbq,cafe,fast_food,food_court,restaurant" tag="restaurant" selected>Restaurants</option>
        <option value="fuel" tag="gas-station">Gas Stations</option>
    </select>
    <button id="btn-get-route" onclick="performRouting()">
        Go
    </button>
</div>

Next, add the following code to your JavaScript. The searchPlaces method takes the route we received from the ThinkGeo Cloud and submits it as a reverse geocode query. The ThinkGeo Cloud Reverse Geocoding service will find and return all places of the types we specify within a certain proximity to our drive (200 meters, in this example).

// Find points of interest along our route.
let placeSource = new ol.source.Vector();
let reverseGeocodingClient = new tg.ReverseGeocodingClient(apiKey);
const searchPlaces = (drivingLine) => {

    // Remove any points of interest already on the map.
    placeSource.clear();
    
    // Prepare request parameters.
    const placeType = document.querySelector('#place-type').selectedOptions[0];
    const lineWkt = (new ol.format.WKT()).writeGeometry(drivingLine.getGeometry());
    
    const handleReverseGeocodeResponse = (status, response) => {
        if (status === 200) {
            // Draw the returned points of interest on our map.
            showPlaces(response.data, placeType.innerText);
        }
    }
    
    // Perform the reverse geocode.
    reverseGeocodingClient.searchPlaceAdvanced({
        wkt: lineWkt,
        srid: 3857,
        locationTypes: placeType.value,
        maxResults: 100,
        searchRadius: 200  // Search within 200 meters of route.
    }, handleReverseGeocodeResponse);
}

Below this, we’re going to add a method called showPlaces that will take the response from the ThinkGeo Cloud Reverse Geocoder and use it to create points on the map. Each point will have a pin icon with a symbol that represents its type; for example, a bed icon for hotels.

const showPlaces = (res, placeType) => {
    for (let i = 0; i < res.nearbyLocations.length; i++) {
        // For each place in the reverse geocode result,
        // construct a map feature and a popup bubble.
        let place = res.nearbyLocations[i].data;
        const title = place.locationName ? `<strong>${place.locationName}</strong> ` : '';
        const content = `<div>${title}<small>` +
                        `(${place.locationType})</small><br/>` +
                        `${place.address.substring(place.address.indexOf(',') + 1)},` +
                        `${place.address.lastIndexOf(',')}</div>`
        const placeFeature = new ol.Feature({
            geometry: new ol.geom.Point([place.locationPoint.pointX, place.locationPoint.pointY]),
            content: content,
            name: 'place'
        });
        
        // Give the map point for each place an icon specific
        // to its type (hotel, restaurant or gas station).
        let style;
        switch (placeType) {
            case 'Hotels':
                style = new ol.style.Style({
                    image: new ol.style.Icon({
                        anchor: [0.5, 1],
                        anchorXUnits: 'fraction',
                        anchorYUnits: 'fraction',
                        opacity: 1,
                        crossOrigin: "Anonymous",
                        src: 'https://samples.thinkgeo.com/cloud/example/image/place-icons/hotel.png',
                        imgSize: [32, 32]
                    }),
                    zIndex: 2
                });
                break;
            case 'Restaurants':
                style = new ol.style.Style({
                    image: new ol.style.Icon({
                        anchor: [0.5, 1],
                        anchorXUnits: 'fraction',
                        anchorYUnits: 'fraction',
                        opacity: 1,
                        crossOrigin: "Anonymous",
                        src: 'https://samples.thinkgeo.com/cloud/example/image/place-icons/restaurant.png',
                        imgSize: [32, 32]
                    }),
                    zIndex: 2
                });
                break;
            case 'Gas Stations':
                style = new ol.style.Style({
                    image: new ol.style.Icon({
                        anchor: [0.5, 1],
                        anchorXUnits: 'fraction',
                        anchorYUnits: 'fraction',
                        opacity: 1,
                        crossOrigin: "Anonymous",
                        src: 'https://samples.thinkgeo.com/cloud/example/image/place-icons/fuel.png',
                        imgSize: [32, 32]
                    }),
                    zIndex: 2
                });
                break;
        }
        
        placeFeature.setStyle(style);
        placeSource.addFeature(placeFeature);
    }
}

Lastly, back in our initializeMap method, we need to add the layer that will hold our points of interest. Do this between the calls to showRouteEndpoints() and performRouting().

// Add a map layer for the points of interest along the route.
 map.addLayer(new ol.layer.Vector({source:placeSource}));

And voila…if we reload our map now, we’ll see all the restaurants within 200 meters of our route! This is great.

4. Displaying place details in a popup bubble

One last thing to make this really useful: we need a way to show the user the details for each place along their route, such as its name and address. The features we added to our map have this information already embedded, we just need to display it. One way to do that would be to show it in a popup bubble when you click on one of the points, so let’s implement that now.

To make the popups work, we first need to insert a new HTML element inside of the map container on our web page, like this:

<div id="popup" class="ol-popup">
    <a href="#" id="popup-closer" class="ol-popup-closer"></a>
    <div id="popup-content"></div>
</div>

To make this look right, we’ll need to add the following CSS. These styles will make the popup look like a speech balloon, and give it an “X” button that can be clicked to make it disappear.

/* Make the popup look like a speech balloon */
.ol-popup {
  position: absolute;
  background-color: #fff;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
  padding: 5px;
  border-radius: 5px;
  border: 1px solid #ccc;
  bottom: 12px;
  left: -50px;
  min-width: 280px;
  margin-bottom: 30px;
}

/* Add the bubble's "pointer" */
.ol-popup:after,
.ol-popup:before {
  top: 100%;
  border: solid transparent;
  content: " ";
  height: 0;
  width: 0;
  position: absolute;
  pointer-events: none;
}
.ol-popup:after {
  border-top-color: white;
  border-width: 10px;
  left: 48px;
  margin-left: -10px;
}
.ol-popup:before {
  border-top-color: #ccc;
  border-width: 11px;
  left: 48px;
  margin-left: -11px;
}

/* Text styles for the popup content */
#popup-content {
  line-height: 20px;
  font-size: 12px;
}

#popup-content small {
  font-size: 12px;
}

/* Style the popup's 'X' button */
.ol-popup-closer {
  text-decoration: none;
  position: absolute;
  top: 2px;
  right: 8px;
  color: black;
}

.ol-popup-closer:after {
  content: "✖";
}

Finally, we need to add some JavaScript to our initializeMap method that makes these popups work. Before the call to performRouting(), we’ll create a map overlay for our popup and hook up the events to show and hide it.

// Create a popup overlay that will show the name and address 
// of each point we find along our route.
const container = document.getElementById('popup');
const content = document.getElementById('popup-content');
const closer = document.getElementById('popup-closer');

popup = new ol.Overlay({
    element: container,
    autoPan: true,
    autoPanAnimation: {
        duration: 250
    }
});
map.addOverlay(popup);

// Show the popup when clicking a point.
map.on('click', function (evt) {
    var feature = map.forEachFeatureAtPixel(evt.pixel,
        function (feature) {
            return feature;
        }, {
            layerFilter: (layer) => {
                return !(layer instanceof ol.mapsuite.VectorTileLayer)
            }
        });
    if (feature && feature.get('name') === 'place') {
        var coordinates = feature.getGeometry().getCoordinates();
        popup.setPosition(coordinates);
        content.innerHTML = feature.get('content');
    }
});

// Close the popup when clicking on its 'X'.
closer.onclick = function (e) {
    e.preventDefault();
    popup.setPosition(undefined);
    closer.blur();
    return false;
};

And that’s all there is to it. Now, if you click on a point of interest on your map, a popup bubble will open and display the name and address of that point. (If the ThinkGeo Cloud service doesn’t know the name of the place, it will show its type instead.) Here’s how it looks:

Go ahead and experiment with the finished product below.

See the Pen Flat minion by ThinkGeo (@thinkgeocodepen) on CodePen
1

Taking it further

The simple map we’ve built has fully-functional routing and place search capabilities, but there’s lots of room for enhancements to the user experience if we really wanted to make this a useful application. Some ideas:

  • Allow the user to set their own start and end points for the route, e.g. by clicking on the map or dragging the points around.
  • You could hook up to a geocoding service like ThinkGeo’s Cloud Geocoder to allow users to search for real-world addresses to use as their start and end points.
  • Give the user more control over the point-of-interest search, including filtering the results by name, choosing the proximity to the route, setting the maximum number of results, and so on.
  • Support adding waypoints to the route, and then optimize your route to hit a series of points in the most efficient order.
  • Allow a choice of map styles. In addition to this typical look and feel, ThinkGeo’s Cloud Maps have a dark design as well as a hybrid mode with aerial imagery, and you can completely customize the base map style if you want (it’s just defined a JSON file).

In fact, ThinkGeo has built a somewhat more advanced version of this tutorial map that already includes many of these features. You can experiment with it and see the code behind it at their Places Along Route sample.

There are lots of potential applications for routing and reverse geocoding that you can put through their paces. Whether it’s for a commercial product, an internal enterprise application or a side project, fast and accurate routing is a great feature to have. ThinkGeo has lots of cloud mapping services under one roof, so check them out to see what all you can do.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *