Enable draggable directions

Allowing users to interact with the default routing by adding waypoints and drag markers can improve the efficiency and accuracy of navigation.

For example, draggable directions on a map can be a valuable tool for logistics managers in the warehouse industry. By customizing the routes based on specific delivery truck needs, you can ensure that deliveries are made efficiently and timely.

In addition, by dragging markers on the map, users can customize their route to include specific streets or areas they want to pass through. This level of personalization enables users to create a route that is tailored to their preferences and needs.

This tutorial demonstrates how use the Maps API for JavaScript to plot markers for the origin and destination on the map and then calculate and display the fastest route between those markers. The tutorial then demonstrates how to dynamically recalculate the default route if a user adds or removes a waypoint marker or changes the location of any of the existing markers.

Before you begin

Create a basic HTML page as a container for the JavaScript code rendering the map.

  1. Within the HTML file, ensure that the <head> element references the required library components, as shown in the following example:

     <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-core.js"></script>
     <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-service.js"></script>
     <script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js"></script>
     <script type="text/javascript" charset="utf-8" src="https://js.api.here.com/v3/3.1/mapsjs-ui.js" ></script>
     <link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
    
  2. Add a CSS style to the HTML page. For example, you can use the following basic style that sets the map viewport to 100% height and width of the enclosing container:

    body {width: 100%; height: 100%; position: absolute; margin: 0px; padding: 0px; overflow: hidden;} 
    #map {position:absolute; top:0; bottom:0; width:100%; height: 100%;}
    

Create a base map

In the HTML container that you created, add JavaScript code that uses the Maps API for JavaScript to display the map and enable map interactions.

  1. Within the <script> element of the HTML file that you created, paste or reference the following code snippet:

     // Initiate and authenticate your connection to the HERE platform:
     const platform = new H.service.Platform({
         'apikey': 'Your API key'
     });
    
     // Obtain the default map types from the platform object:
     const defaultLayers = platform.createDefaultLayers();
    
     // Instantiate (and display) a map:
     var map = new H.Map(
         document.getElementById("map"),
         // Center the map on Great Britain with the zoom level of 6:
         defaultLayers.vector.normal.map, {
             zoom: 6,
             center: {
                 lat: 53.480759,
                 lng: -2.242631
             }
         });
    
     // MapEvents enables the event system.
     // The behavior variable implements default interactions for pan/zoom (also on mobile touch environments).
     const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
    
     // Enable dynamic resizing of the map, based on the current size of the enclosing cntainer
     window.addEventListener('resize', () => map.getViewPort().resize());
    
     // Create the default UI:
     var ui = H.ui.UI.createDefault(map, defaultLayers, );
    
  2. In the code snippet, replace the value of the apikey variable with your own API key.

Result: The resulting HTML file renders a base map centered on Great Britain, with the zoom level of 6, as shown in the following example:

Base map
Figure 1. Base map

Plot the origin and destination points on the map

On your map, add markers that represent the origin and destination locations.

  1. Create the getMarkerIcon() function that applies custom SVG style to each marker:

     /**
      * Returns an instance of H.map.Icon to style the markers
      * @param {number|string} id An identifier that will be displayed as marker label
      *
      * @return {H.map.Icon}
      */
     function getMarkerIcon(id) {
         const svgCircle = `<svg width="30" height="30" version="1.1" xmlns="http://www.w3.org/2000/svg">
                               <g id="marker">
                                 <circle cx="15" cy="15" r="10" fill="#0099D8" stroke="#0099D8" stroke-width="4" />
                                 <text x="50%" y="50%" text-anchor="middle" fill="#FFFFFF" font-family="Arial, sans-serif" font-size="12px" dy=".3em">${id}</text>
                               </g></svg>`;
         return new H.map.Icon(svgCircle, {
             anchor: {
                 x: 10,
                 y: 10
             }
         });
     }
    

    The style produces a marker that is a blue circle with text in the center that corresponds to the marker ID.

  2. Create the addMarker() function that adds an object marker to the map and applies the custom SVG style:

     /**
      * Create an instance of H.map.Marker and add it to the map
      *
      * @param {object} position  An object with 'lat' and 'lng' properties defining the position of the marker
      * @param {string|number} id An identifier that will be displayed as marker label
      * @return {H.map.Marker} The instance of the marker that was created
      */
     function addMarker(position, id) {
         const marker = new H.map.Marker(position, {
             data: {
                 id
             },
             icon: getMarkerIcon(id),
         });
    
         map.addObject(marker);
         return marker;
     }
    
  3. Define the coordinates for the origin and destination markers, as shown in the following example:

     const origin = {
         lat: 55.953251,
         lng: -3.188267
     }; // Edinburgh
     const destination = {
         lat: 51.507797,
         lng: -0.128604
     }; // London
    
  4. Call the addMarker() function to plot the origin and destination markers on the map, at the locations that you specified, with different IDs:

     const originMarker = addMarker(origin, 'A');
     const destinationMarker = addMarker(destination, 'B');
    

Result: The map now shows static markers for the origin and destination coordinates, as shown in the following figure:

Base map with the origin and destination markers
Figure 2. Base map with the origin and destination markers

For more information about markers, see see Marker Objects.

Calculate the fastest route between the origin and destination points

Use the HERE Routing API v8 to determine the fastest route between the orign and destination points.

  1. Specify the routing parameters:

     const routingParams = {
         'origin': `${origin.lat},${origin.lng}`,
         'destination': `${destination.lat},${destination.lng}`,
         'transportMode': 'car',
         'return': 'polyline'
     };
    

    where:

    • origin is a string representing the latitude and longitude of the starting point of the route.
    • destination is a string representing the latitude and longitude of the destination of the route.
    • transpotMode is a string specifying the mode of transportation to be used for the route. In this example, it is set to car, which means the route will be optimized for car travel.
    • return is a string indicating the format of the response from the API. In this example, it is set to polyline, which means the response is a string representing the route as a polyline.
  2. Initialize H.service.RoutingService8 object:

     const router = platform.getRoutingService(null, 8);
    
  3. Initialize an empty variable for holding the polyline that represents the calculated route on the map:

     let routePolyline;
    
  4. Define the routeResponseHandler() function to use it as the callback function for the calculateRoute method of the H.service.RoutingService8 object:

     /**
      * Handler for the H.service.RoutingService8#calculateRoute call
      *
      * @param {object} response The response object returned by calculateRoute method
      */
     function routeResponseHandler(response) {
         const sections = response.routes[0].sections;
         const lineStrings = [];
         sections.forEach((section) => {
             // convert Flexible Polyline encoded string to geometry
             lineStrings.push(H.geo.LineString.fromFlexiblePolyline(section.polyline));
         });
         const multiLineString = new H.geo.MultiLineString(lineStrings);
         const bounds = multiLineString.getBoundingBox();
    
         // Create the polyline for the route
         if (routePolyline) {
             // If the routePolyline we just set the new geometry
             routePolyline.setGeometry(multiLineString);
         } else {
             // routePolyline is not yet defined, instantiate a new H.map.Polyline
             routePolyline = new H.map.Polyline(multiLineString, {
                 style: {
                     lineWidth: 5
                 }
             });
         }
    
         // Add the polyline to the map
         map.addObject(routePolyline);
     }
    
  5. Define the updateRoute function to call the routing service with the defined parameters:

     function updateRoute() {
         router.calculateRoute(routingParams, routeResponseHandler, console.error);
     }
    

    The calculateRoute method determines the fastest route between two or more points based on the parameters specified in routingParams. When the route is calculated, the routeResponseHandler() function is called with the response object as a parameter.

  6. Call the updateRoute function to display the route between the origin and destination points:

     updateRoute();
    

Result: The map displays the fastest route to get from Edinburgh to London by car, as shown in the following figure:

The fastest route between the origin and destination points as a polyline
Figure 3. The fastest route between the origin and destination points as a polyline

For more information about routing, see HERE Routing API v8.

Make markers draggable

Allow users to edit the initial origin and destination points by dragging the corresponding markers to new locations. You can configure your map to dynamically recalculate the route as marker positions change.

  1. Inside the addMarker function, enable dragging for marker objects:

    1. In the marker variable, set the volatile configuration option of the H.map.Marker object to true to enable smooth dragging.
    2. Set the marker.draggable property to true.

      See the updated function definition for reference:

      function addMarker(position, id) {
       const marker = new H.map.Marker(position, {
           data: {
               id
           },
           icon: getMarkerIcon(id),
           // Enable smooth dragging
           volatility: true
       });
       // Enable draggable markers
       marker.draggable = true;
      
       map.addObject(marker);
       return marker;
      }
      
  2. Add a map event listener for the dragstart event:

     /**
      * Listen to the dragstart and store the relevant position information of the marker
      */
     map.addEventListener('dragstart', function(ev) {
         const target = ev.target;
         const pointer = ev.currentPointer;
         if (target instanceof H.map.Marker) {
             // Disable the default draggability of the underlying map
             behavior.disable(H.mapevents.Behavior.Feature.PANNING);
    
             var targetPosition = map.geoToScreen(target.getGeometry());
             // Calculate the offset between mouse and target's position
             // when starting to drag a marker object
             target['offset'] = new H.math.Point(
                 pointer.viewportX - targetPosition.x, pointer.viewportY - targetPosition.y);
         }
     }, false);
    

    This event triggers when a user starts dragging a marker on the map. The function in this event listener checks if the target of the event is a marker object. If true, the function disables the default draggability of the underlying map and calculates the offset between the mouse and the target position when starting to drag the marker object. The offset is used to calculate the new position of the marker as it is dragged across the map.

  3. Add a map event listener for the drag event:

     /**
      * Listen to the drag event and move the position of the marker as necessary
      */
     map.addEventListener('drag', function(ev) {
         const target = ev.target;
         const pointer = ev.currentPointer;
         if (target instanceof H.map.Marker) {
             target.setGeometry(
                 map.screenToGeo(pointer.viewportX - target['offset'].x, pointer.viewportY - target['offset'].y)
             );
         }
     }, false);
    

    The function in this event listener updates the position of the marker on the map as the marker is being dragged based on the current position of the pointer.

  4. Add a map event listener for the dragend event:

     /**
      * Listen to the dragend and update the route
      */
     map.addEventListener('dragend', function(ev) {
         const target = ev.target;
         if (target instanceof H.map.Marker) {
             // re-enable the default draggability of the underlying map
             // when dragging has completed
             behavior.enable(H.mapevents.Behavior.Feature.PANNING);
             const coords = target.getGeometry();
             const markerId = target.getData().id;
    
             // Update the routing params `origin` and `destination` properties
             // in case we dragging either the origin or the destination marker
             if (markerId === 'A') {
                 routingParams.origin = `${coords.lat},${coords.lng}`;
             } else if (markerId === 'B') {
                 routingParams.destination = `${coords.lat},${coords.lng}`;
             }
    
             updateRoute();
         }
     }, false);
    

    The dragend event is defined to trigger the updateRoute function to dynamically recalculate and display the new route, based on the updated marker coordinates.

Result: You can now move origin and destination markers to new locations and the map dynamically recalculates the fastest route, as shown in the following figure:

Draggable markers and dynamic route recalculation
Figure 4. Draggable markers and dynamic route recalculation

Add or remove waypoints

You can further increase map interactivity by enabling users to add or remove draggable waypoint markers. This functionality provides greater flexibility and efficiency in route planning.

  1. Initialize an empty array for markers that correspond route waypoints:

     // This array holds instances of H.map.Marker representing the route waypoints
     const waypoints = []
    
  2. In the routing logic, update the routingParams variable to include the new via parameter, as shown in the following example:

     const routingParams = {
       'origin':      `${origin.lat},${origin.lng}`,
       'destination': `${destination.lat},${destination.lng}`,
       // defines multiple waypoints
       'via': new H.service.Url.MultiValueQueryParameter(waypoints),
       'transportMode': 'car',
       'return': 'polyline'
     };
    

    The MultiValueQueryParameter creates a string parameter holding coordinates for multiple waypoints between the origin and destination points. Then, this string is appended to the HERE Routing API query.

  3. Adjust the updateRoute() function to include the waypoints, as shown in the following example:

     function updateRoute() {
         // Add waypoints the route must pass through
         routingParams.via = new H.service.Url.MultiValueQueryParameter(
             waypoints.map(wp => `${wp.getGeometry().lat},${wp.getGeometry().lng}`));
    
         router.calculateRoute(routingParams, routeResponseHandler, console.error);
     }
    

    The via parameter adds additional waypoints the route must pass through between the origin and destination locations.

  4. Add a map event listener for the tap event:

     /**
      * Listen to the tap event to add a new waypoint
      */
     map.addEventListener('tap', function(ev) {
         const target = ev.target;
         const pointer = ev.currentPointer;
         const coords = map.screenToGeo(pointer.viewportX, pointer.viewportY);
    
         if (!(target instanceof H.map.Marker)) {
             const marker = addMarker(coords, waypoints.length + 1);
             waypoints.push(marker);
             updateRoute();
         }
     });
    

    This event handler allows the user to add new waypoints as markers by clicking on the map at the desired location, which then updates the route accordingly.

  5. Add a map event listener for the dbltap event:

     /**
      * Listen to the dbltap event to remove a waypoint
      */
     map.addEventListener('dbltap', function(ev) {
         const target = ev.target;
    
         if (target instanceof H.map.Marker) {
             // Prevent the origin or destination markers from being removed
             if (['origin', 'destination'].indexOf(target.getData().id) !== -1) {
                 return;
             }
    
             const markerIdx = waypoints.indexOf(target);
             if (markerIdx !== -1) {
                 // Remove the marker from the array of way points
                 waypoints.splice(markerIdx, 1)
                 // Iterate over the remaining waypoints and update their data
                 waypoints.forEach((marker, idx) => {
                     const id = idx + 1;
                     // Update marker's id
                     marker.setData({
                         id
                     });
                     // Update marker's icon to show its new id
                     marker.setIcon(getMarkerIcon(id))
                 });
             }
    
             // Remove the marker from the map
             map.removeObject(target);
    
             updateRoute();
         }
     });
    

    The function in this event listener handles the removal of a waypoint when a user double-taps a marker.

    When a user double-taps on a marker, the function first checks if the marker is an origin or destination marker. If it is, the function does not allow the marker removal. Otherwise, the function finds the index of the marker in the waypoints array and removes the marker from the array.

    After removing the marker from the waypoints array, the function updates the data and icon of the remaining markers in the waypoints array to reflect their new order. Finally, the function removes the marker from the map and calls the updateRoute() function to update the route based on the new set of waypoints.

    For more information, see Route with multiple via waypoints.

  6. Add a map behavior to disable map zooming when double-tapping markers:

     behavior.disable(H.mapevents.Behavior.Feature.DBL_TAP_ZOOM);
    

Result: You can now add, remove, or drag waypoint markers to new locations, while the map dynamically recalculates the fastest rout after each event, as shown in the following example:

Interactive waypoints
Figure 5. Interactive waypoints

Source code

The following section provides the full source code that was used in this tutorial.

Note

Before trying out the following code, replace the value of the apikey parameter with your own API key.

//BOILERPLATE CODE TO INITIALIZE THE MAP
const platform = new H.service.Platform({
    'apikey': 'Your API Key'
});

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

// Instantiate (and display) a map:
var map = new H.Map(
    document.getElementById("map"),
    defaultLayers.vector.normal.map, {
        zoom: 6,
        center: {
            lat: 53.480759,
            lng: -2.242631
        }
    });

// MapEvents enables the event system
// Behavior implements default interactions for pan/zoom (also on mobile touch environments)
const behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));

// Disable zoom on double-tap to allow removing waypoints on double-tap
behavior.disable(H.mapevents.Behavior.Feature.DBL_TAP_ZOOM);

window.addEventListener('resize', () => map.getViewPort().resize());

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

// ROUTING LOGIC STARTS HERE

// This variable holds the instance of the route polyline
let routePolyline;

/**
 * Handler for the H.service.RoutingService8#calculateRoute call
 *
 * @param {object} response The response object returned by calculateRoute method
 */
function routeResponseHandler(response) {
    const sections = response.routes[0].sections;
    const lineStrings = [];
    sections.forEach((section) => {
        // convert Flexible Polyline encoded string to geometry
        lineStrings.push(H.geo.LineString.fromFlexiblePolyline(section.polyline));
    });
    const multiLineString = new H.geo.MultiLineString(lineStrings);
    const bounds = multiLineString.getBoundingBox();

    // Create the polyline for the route
    if (routePolyline) {
        // If the routePolyline we just set has the new geometry
        routePolyline.setGeometry(multiLineString);
    } else {
        // If routePolyline is not yet defined, instantiate a new H.map.Polyline
        routePolyline = new H.map.Polyline(multiLineString, {
            style: {
                lineWidth: 5
            }
        });
    }

    // Add the polyline to the map
    map.addObject(routePolyline);
}

/**
 * Returns an instance of H.map.Icon to style the markers
 * @param {number|string} id An identifier that will be displayed as marker label
 *
 * @return {H.map.Icon}
 */
function getMarkerIcon(id) {
    const svgCircle = `<svg width="30" height="30" version="1.1" xmlns="http://www.w3.org/2000/svg">
  <g id="marker">
    <circle cx="15" cy="15" r="10" fill="#0099D8" stroke="#0099D8" stroke-width="4" />
    <text x="50%" y="50%" text-anchor="middle" fill="#FFFFFF" font-family="Arial, sans-serif" font-size="12px" dy=".3em">${id}</text>
  </g></svg>`;
    return new H.map.Icon(svgCircle, {
        anchor: {
            x: 10,
            y: 10
        }
    });
}

/**
 * Create an instance of H.map.Marker and add it to the map
 *
 * @param {object} position  An object with 'lat' and 'lng' properties defining the position of the marker
 * @param {string|number} id An identifier that will be displayed as marker label
 * @return {H.map.Marker} The instance of the marker that was created
 */
function addMarker(position, id) {
    const marker = new H.map.Marker(position, {
        data: {
            id
        },
        icon: getMarkerIcon(id),
        // Enable smooth dragging
        volatility: true
    });

    // Enable draggable markers
    marker.draggable = true;

    map.addObject(marker);
    return marker;
}

/**
 * This method calls the routing service to retrieve the route line geometry
 */
function updateRoute() {
    routingParams.via = new H.service.Url.MultiValueQueryParameter(
        waypoints.map(wp => `${wp.getGeometry().lat},${wp.getGeometry().lng}`));

    // Call the routing service with the defined parameters
    router.calculateRoute(routingParams, routeResponseHandler, console.error);
}

// ADD MARKERS FOR ORIGIN/DESTINATION
const origin = {
    lat: 55.953251,
    lng: -3.188267
}; // Edinburgh
const destination = {
    lat: 51.507797,
    lng: -0.128604
}; // London

const originMarker = addMarker(origin, 'A');
const destinationMarker = addMarker(destination, 'B');

// CALCULATE THE ROUTE BETWEEN THE TWO WAYPOINTS
// This array holds instances of H.map.Marker representing the route waypoints
const waypoints = []

// Define the routing service parameters
const routingParams = {
    'origin': `${origin.lat},${origin.lng}`,
    'destination': `${destination.lat},${destination.lng}`,
    // defines multiple waypoints
    'via': new H.service.Url.MultiValueQueryParameter(waypoints),
    'transportMode': 'car',
    'return': 'polyline'
};

// Get an instance of the H.service.RoutingService8 service
const router = platform.getRoutingService(null, 8);

// Call the routing service with the defined parameters and display the route
updateRoute();

/**
 * Listen to the dragstart and store relevant position information of the marker
 */
map.addEventListener('dragstart', function(ev) {
    const target = ev.target;
    const pointer = ev.currentPointer;
    if (target instanceof H.map.Marker) {
        // Disable the default draggability of the underlying map
        behavior.disable(H.mapevents.Behavior.Feature.PANNING);

        var targetPosition = map.geoToScreen(target.getGeometry());
        // Calculate the offset between mouse and target's position
        // when starting to drag a marker object
        target['offset'] = new H.math.Point(
            pointer.viewportX - targetPosition.x, pointer.viewportY - targetPosition.y);
    }
}, false);

/**
 * Listen to the dragend and update the route
 */
map.addEventListener('dragend', function(ev) {
    const target = ev.target;
    if (target instanceof H.map.Marker) {
        // re-enable the default draggability of the underlying map
        // when dragging has completed
        behavior.enable(H.mapevents.Behavior.Feature.PANNING);
        const coords = target.getGeometry();
        const markerId = target.getData().id;

        // Update the routing params `origin` and `destination` properties
        // in case we dragging either the origin or the destination marker
        if (markerId === 'A') {
            routingParams.origin = `${coords.lat},${coords.lng}`;
        } else if (markerId === 'B') {
            routingParams.destination = `${coords.lat},${coords.lng}`;
        }

        updateRoute();
    }
}, false);

/**
 * Listen to the drag event and move the position of the marker as necessary
 */
map.addEventListener('drag', function(ev) {
    const target = ev.target;
    const pointer = ev.currentPointer;
    if (target instanceof H.map.Marker) {
        target.setGeometry(
            map.screenToGeo(pointer.viewportX - target['offset'].x, pointer.viewportY - target['offset'].y)
        );
    }
}, false);

/**
 * Listen to the tap event to add a new waypoint
 */
map.addEventListener('tap', function(ev) {
    const target = ev.target;
    const pointer = ev.currentPointer;
    const coords = map.screenToGeo(pointer.viewportX, pointer.viewportY);

    if (!(target instanceof H.map.Marker)) {
        const marker = addMarker(coords, waypoints.length + 1);
        waypoints.push(marker);
        updateRoute();
    }
});

/**
 * Listen to the dbltap event to remove a waypoint
 */
map.addEventListener('dbltap', function(ev) {
    const target = ev.target;

    if (target instanceof H.map.Marker) {
        // Prevent origin or destination markers from being removed
        if (['origin', 'destination'].indexOf(target.getData().id) !== -1) {
            return;
        }

        const markerIdx = waypoints.indexOf(target);
        if (markerIdx !== -1) {
            // Remove the marker from the array of way points
            waypoints.splice(markerIdx, 1)
            // Iterate over the remaining waypoints and update their data
            waypoints.forEach((marker, idx) => {
                const id = idx + 1;
                // Update marker's id
                marker.setData({
                    id
                });
                // Update marker's icon to show its new id
                marker.setIcon(getMarkerIcon(id))
            });
        }

        // Remove the marker from the map
        map.removeObject(target);

        updateRoute();
    }
});

Next steps

  • To explore other Maps API for JavaScript use cases, see Examples.
  • To explore the design and features of the Maps API for JavaScript, see the API Reference.

results matching ""

    No results matching ""