Hands On

Mashing Up Data Hub and Points of Interest Search

By Raymond Camden | 30 September 2020

Try HERE Maps

Create a free API key to build location-aware apps and services.

Get Started

Data Hub provides a powerful API for storing and working with geospatial data, but one of the best ways to make use of Data Hub is to mash it up with one of our other services. Today, I'm sharing an example of using data stored in Data Hub and integrating results from the powerful HERE Geocoding and Search API. We'll be paying special attention to the Browse endpoint.

The Browse API lets you search for POIs (Points of Interest) filtered by one or more categories. The Category system is truly incredible, with a very long list of "regular" categories as well as a unique system just for types of food. As an example of how detailed the food system gets, let’s take Asian cuisine. You can filter by over twenty different types of Asian food, and in those twenty plus categorizations, there's even more fine-grained specific categories such Northeastern Chinese.)

For our demo, we're going to take an existing application using Data Hub data and enhance it with use of the Browse API. Our existing application will be using data from the National Parks Service list of parks in the United States of America. Yes, I'm using that again, because what can I say, when I find good data, I like to keep playing with it!

In case you haven't seen any of my previous blog posts or online talks, the data consists of list of features that includes things like the park's name, open hours, URLs, and more. As geospatial data it obviously includes information about the location of the park as well. Here's one feature:

{ 
"id": "0", 
"type": "Feature", 
"properties": { 
    "url": "https://www.nps.gov/frla/index.htm", 
    "name": "Frederick Law Olmsted", 
    "image": "https://www.nps.gov/common/uploads/structured_data/3C853BE9-1DD8-B71B-0B625B6B8B89F1A0.jpg", 
    "topics": "Civil War,Landscape Design,Schools and Education,Wars and Conflicts", 
    "fullName": "Frederick Law Olmsted National Historic Site", 
    "parkCode": "frla", 
    "activities": "Guided Tours,Junior Ranger Program,Museum Exhibits,Self-Guided Tours - Walking", 
    "description": "Frederick Law Olmsted (1822-1903) is recognized as the founder of American landscape architecture and the nation's foremost parkmaker. Olmsted moved his home to suburban Boston in 1883 and established the world's first full-scale professional office for the practice of landscape design. During the next century, his sons and successors perpetuated Olmsted's design ideals, philosophy, and influence.", 
    "designation": "National Historic Site", 
    "phoneNumber": "6175661689", 
    "weatherInfo": "Summer: Warm temperatures, average high temperature around 80 degrees Fahrenheit, often with humidity. July and August bring the hottest temperatures.\nFall: Cooler temperatures, mean temperatures between 45 and 65 degrees Fahrenheit, sometimes rainy. Peak fall foliage is in mid-October.\nWinter: Cold, with snow, average low temperature around 25 degrees Fahrenheit. \nSpring: Cold to cool temperatures, average mean temperatures between 40 and 60 degrees Fahrenheit.", 
    "emailAddress": "frla_interpretation@nps.gov", 
    "directionsUrl": "http://www.nps.gov/frla/planyourvisit/directions.htm", 
    "directionsInfo": "Site is located on the southwest corner of Warren and Dudley Streets in Brookline, south of Route 9, near the Brookline Reservoir.\n\nSite is 0.7 miles from the Brookline Hills MBTA stop on the Green Line, D Branch.", 
    "@ns:com:here:xyz": { 
        "tags": [ 
        "national-parks-studio" 
        ], 
        "space": "yi9C54LG", 
        "version": 0, 
        "createdAt": 1594238710406, 
        "updatedAt": 1594238710406 
    } 
}, 
"geometry": { 
    "type": "Point", 
    "coordinates": [ 
        -71.13112957, 
        42.32550867, 
        0 
    ] 
    } 
},

For my demo, I began by adding a simple map.

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

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

var map = new H.Map( 
    document.getElementById('mapContainer'), 
    defaultLayers.vector.normal.map, 
    { 
        zoom: 3, 
        center: { lat: 38.7984, lng: -96.3944 }, 
        pixelRatio: window.devicePixelRatio || 1 
    } 
); 

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

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

From the top, I create an instance of our mapping platform, add default layers, select where in the DOM I want to render my map, and then create a map object. For my center position I just picked something that centered the map well for my American-centric data. Finally I add in some default behavior and UI elements.

To add data from Data Hub, you only need a few lines of code:

const XYZ_TOKEN = 'AHDIOVe6R6y1ry3kSAjeRQA'; 
const service = platform.getXYZService({ 
    token: XYZ_TOKEN, 
}); 

const XYZ_SPACE = 'yi9C54LG'; 
const mySpaceProvider = new H.service.xyz.Provider(service, XYZ_SPACE, { 
}); 

const mySpaceLayer = new H.map.layer.TileLayer(mySpaceProvider); 

// add a layer to the map 
map.addLayer(mySpaceLayer); 

mySpaceProvider.getStyle().setInteractive(['xyz'], true);

This sets up an instance of the XYZ service (the older name of Data Hub) using a token that provides read-only access to the data. I specify my space and create a provider object tied to it. I then make a new layer tied to the provider and finally add it to my map. The last line prepares the provider for interactivity. What interactivity?

When you click, I want to create an "info bubble" (think a small popup) that will display information about the park. Let's look at that code:

let bubble; 

// Add 'tap' event listener, that opens info bubble 
mySpaceProvider.addEventListener('tap', function (evt) { 
    let position = map.screenToGeo(evt.currentPointer.viewportX, evt.currentPointer.viewportY), 
        data = evt.target.getData(); 

    if (!bubble) { 
        bubble = new H.ui.InfoBubble(position, { content: '' }) 
        ui.addBubble(bubble); 
    } 

    bubble.setContent(` 
    <div style="height:320px; overflow:auto;width: 320px;"> 
    <h2>${data.properties['properties.name']}</h2> 
    <p>${data.properties['properties.description']}</p> 
    <img class="park" src="${data.properties['properties.image']}" title="Photo of Park"> 
    </div>`); 

    bubble.setPosition(position); 
    bubble.open(); 
    map.setCenter(position, true);   

});

I add a tap event to the provider. This event gets the location of the tap event as well as the data from the feature. Specifically the getData() call returns that information. If an info bubble hasn't been built yet, I initialize it with some blank text and add it to the UI.

Next I set the content based on the park data. As you saw above, there's quite a bit there, but to keep it simple I displayed the name, description, and image. Here's an example of the map before anything is clicked.

img1-2

And here's how the info bubble looks.

img2-2

For a complete listing of this code, see the repository file here and you can play with an online version here.

Alright, let's kick it up a notch! As I mentioned, the Search service and Browse API has a powerful category system. One of those categories is "600-6900-0395", which has a label of "Camping-Hiking Shop" and is described as "A business that sells camping/hiking-related items, such as tents, sleeping bags and other accessories."

If you wanted to find shops like this at a particular location, you would issue a GET request to:

https://browse.search.hereapi.com/v1/browse?apiKey=${KEY}&at=${lat},${lng}&categories=600-6900-0395 

Where KEY, lat, and lng, would be replaced with real values. This returns a list of possible matches near the location. Here's one example:

{ 
    "title": "Keweenaw Base Camp", 
    "id": "here:pds:place:840f02uj-f0274bac17234621a132c54fed3e5ea2", 
    "resultType": "place", 
    "address": { 
        "label": "Keweenaw Base Camp, 14603 Love Lake Rd, Atlantic Mine, MI 49905, United States", 
        "countryCode": "USA", 
        "countryName": "United States", 
        "stateCode": "MI", 
        "state": "Michigan", 
        "county": "Houghton", 
        "city": "Atlantic Mine", 
        "street": "Love Lake Rd", 
        "postalCode": "49905", 
        "houseNumber": "14603" 
    }, 
    "position": { 
        "lat": 47.1431, 
        "lng": -88.68881 
    }, 
    "access": [ 
        { 
            "lat": 47.14336, 
            "lng": -88.68915 
        } 
    ], 
    "distance": 124537, 
    "categories": [ 
        { 
            "id": "800-8600-0000", 
            "name": "Sports Facility/Venue", 
            "primary": true 
        }, 
        { 
            "id": "550-5510-0378", 
            "name": "Campsite" 
        }, 
        { 
            "id": "600-6900-0395", 
            "name": "Camping/Hiking Shop" 
        } 
    ], 
    "references": [ 
        { 
            "supplier": { 
                "id": "core" 
            }, 
            "id": "1194049011" 
        } 
    ], 
    "contacts": [ 
        { 
            "mobile": [ 
                { 
                    "value": "+19062507219" 
                }, 
                { 
                    "value": "+19062810614" 
                }, 
                { 
                    "value": "+19064825240" 
                } 
            ], 
            "www": [ 
                { 
                    "value": "http://www.keweenaw.org" 
                }, 
                { 
                    "value": "https://campluther.com/kbc" 
                } 
            ], 
            "email": [ 
                { 
                    "value": "info@keweenaw.org" 
                }, 
                { 
                    "value": "marcus@campluther.com" 
                } 
            ] 
        } 
    ] 
}

As you can see, it's quite detailed. For our updated demo, let's see if we can enhance the park info bubbles to display nearby camping stores.

First I created a simple constant for the category ID:

const CAMP_STORE = '600-6900-0395';

The next change was to the tap handler. Here's the entirety of it and I'll explain the major changes:

mySpaceProvider.addEventListener('tap', async evt => { 
    let position = map.screenToGeo(evt.currentPointer.viewportX, evt.currentPointer.viewportY), 
        data = evt.target.getData(); 

    if (!bubble) { 
        bubble = new H.ui.InfoBubble(position, { content: '' }) 
        ui.addBubble(bubble); 
    } 

    bubble.setContent(` 
    <div style="height:320px; overflow:auto;width: 320px;"> 
    <h2>${data.properties['properties.name']}</h2> 
    <p>${data.properties['properties.description']}</p> 
    <img class="park" src="${data.properties['properties.image']}" title="Photo of Park"> 
    <div id="storeInfo"></div> 
    </div>`); 

    bubble.setPosition(position); 
    bubble.open(); 
    map.setCenter(position, true); 

    let stores = await loadStoreInfo(position); 

    let storeHTML = ` 
<h2>Nearby Camping Stores</h2> 
    `; 

    if(stores.length > 0) { 
        storeHTML += '<ul>'; 
        stores.forEach(s => { 
            storeHTML += `<li>${s.title} at ${s.address.label}</li>`; 
        }); 

        storeHTML += '</ul>'; 

    } else { 
        storeHTML += 'Unfortunately, there are no camping stores nearby.'; 
    } 

    document.querySelector('#storeInfo').innerHTML = storeHTML; 
});

The first real change is in the content of the bubble. There's a new empty div using the id storeInfo. After the bubble is opened, I then call out to a new function, loadStoreInfo, that returns camping stores by the park's location. I then generate new HTML to render those stores. What I displayed here was rather arbitrary, but I felt like the title and address label were enough. This new information is then injected into the bubble.

The function to get stores merely wraps a call to the API:

async function loadStoreInfo(pos) { 
    let url = `https://browse.search.hereapi.com/v1/browse?apiKey=${KEY}&at=${pos.lat},${pos.lng}&categories=${CAMP_STORE}&limit=10`; 
    let resp = await fetch(url); 
    let data = await resp.json(); 
    return data.items; 
}

I should note that the JavaScript Maps library from HERE also has a search service wrapper built into it. This hides some of the details of using the search services and includes support for browse. Personally I just prefer using the fetch call as I know it better, but you've got options and can use what makes sense for you!

Here's a quick example of how it looks:

img3-2

We could also take this a step further and provide navigation to those stores and even display those routes on the map. Feel free to fork my code and play with it. The source for this version may be found here and the online version is here.