Location Scoring - Closest POI

Distance-weighted scores are designed to favor closer location without the hard-edge of a linear measurement. 14 minutes isn’t much better than 15, but 3 minutes is significantly better.
<!DOCTYPE html>
<html>
<head>
    <!--  Include mapboxgl javascript and css -->
    <script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.0/mapbox-gl.js"></script>
    <link href="https://api.tiles.mapbox.com/mapbox-gl-js/v1.6.0/mapbox-gl.css" rel="stylesheet">
    <!--  Include targomo core -->
    <script src="https://releases.targomo.com/core/latest.min.js"></script>
    <!-- Include turf for view fitting -->
    <script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
    <!--  Include micro progress bar  -->
    <script src="https://www.targomo.com/developers/scripts/mipb.min.js"></script>
    
    <style>
        body, html {
            margin: 0;
            width: 100%;
            height: 100%;
        }
        #map {
            width: 100%;
            height: 100%;
        }
        .score_result {
            position: absolute; padding: 8px;
            top: 10px; left: 10px;
            font-family: 'Open Sans', sans-serif;
            background: white; border-radius: 4px;
            box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
            z-index: 1;
        }
        .mapboxgl-marker {
            cursor: pointer;
        }
        #markerScores1, #markerScores2 {
            display: inline;
        }
        #markerLabel1 {
            color: #FF8319;
        }
        #markerLabel2 {
            color: #4BB5C5;
        }
    </style>
</head>
<body>
    <div class="score_result">
        <b id="reachable_POIs_label">Distance-weighted scores to nearest food/beverage shop:</b>
        <br><br>
        <b id="markerLabel1">Marker 1: </b><div id="markerScores1"></div>&nbsp;&nbsp;&nbsp;
        <b id="markerLabel2">Marker 2: </b><div id="markerScores2"></div>
    </div>

    <!--  where the map will live  -->
    <div id="map"></div>

    <script>// create targomo client
        const client = new tgm.TargomoClient('westcentraleurope', '__targomo_key_here__')
        
        // Travel options
        const EDGE_WEIGHT = 'distance' // Can be 'time' or 'distance'
        const MAX_TRAVEL = 1300 // Integer that represents meters or seconds, depending on EDGE_WEIGHT's value
        const TRAVEL_MODE = 'walk' // Can be 'walk', 'car', 'bike' or 'transit'
        
        const attributionText = `<a href='//localhost:1313/resources/attribution/' target='_blank'>&copy; Targomo</a>`
        const POI_TYPE = 'g_food' // See POI Hierarchy
        const POI_URL = `https://api.targomo.com/pointofinterest/{z}/{x}/{y}.mvt` +
            `?apiKey=${client.serviceKey}&group=${POI_TYPE}&loadAllTags=true&layerType=node`
        
        // Add the map and set the initial center
        const map = new mapboxgl.Map({
            container: 'map',
            style: client.basemaps.getGLStyleURL("Light"),
            zoom: 15,
            minZoom: 9,
            center: [2.4162, 48.8350],
            attributionControl: false
        })
        
        map.addControl(new mapboxgl.NavigationControl())
        map.addControl(new mapboxgl.AttributionControl({ compact: true, customAttribution: attributionText }))
        
        // disable scroll zoom
        map.scrollZoom.disable()
        
        const emptyData = { 'type': 'FeatureCollection', 'features': [] }
        
        const pBar = new mipb({ fg: '#FF8319' })
        
        // Initialize markers
        const marker1 = new mapboxgl.Marker({
            draggable: true,
            color: '#FF8319'
        })
        const marker2 = new mapboxgl.Marker({
            draggable: true,
            color: '#4bb5c5'
        })
        
        marker1.setLngLat([2.4084, 48.8333]).addTo(map)
        marker1.on('dragend', updateMap)
        
        marker2.setLngLat([2.4244, 48.8368]).addTo(map)
        marker2.on('dragend', updateMap)
        
        
        map.on('load', () => {
            addSources()
            addLayers()
            setupMouseEvents()
            updateMap()
        })
        function addSources() {
            map.addSource('closest-route-sources', {
                'type': 'geojson',
                'data': emptyData
            })
            map.addSource('closest-poi-sources', {
                'type': 'geojson',
                'data': emptyData
            })
        }
        
        function addLayers() {
            addPoiLayer()
            addRouteLayer()
            addClosestPoiLayer()
        }
        function addPoiLayer() {
            map.addLayer({
                'id': 'poi',
                'type': 'circle',
                'source': {
                    'type': 'vector',
                    'tiles': [POI_URL]
                },
                'source-layer': 'poi',
                'paint': {
                    'circle-radius': 4,
                    'circle-color': 'green'
                }
            })
        }
        function addRouteLayer() {
            map.addLayer({
                'id': 'closest-route',
                'type': 'line',
                'source': 'closest-route-sources',
                'filter': ['==', ['geometry-type'], 'LineString'],
                'layout': {
                    'line-join': 'round',
                    'line-cap': 'round'
                },
                'paint': {
                    'line-color': 'limegreen',
                    'line-width': 3
                }
            })
        }
        function addClosestPoiLayer() {
            map.addLayer({
                'id': 'closest-poi',
                'type': 'circle',
                'source': 'closest-poi-sources',
                'paint': {
                    'circle-radius': 6,
                    'circle-color': 'green',
                    'circle-stroke-width': 4,
                    'circle-stroke-color': 'limegreen',
                    'circle-stroke-opacity': .5
                }
            })
        }
        
        function setupMouseEvents() {
            setupHoveredPoi()
            setupClickedPoi()
        }
        function setupHoveredPoi() {
            // Change the cursor to a pointer when the mouse is over the poi layer
            map.on('mouseenter', 'poi', () => {
                map.getCanvas().style.cursor = 'pointer'
            })
            // Change it back to default when it leaves
            map.on('mouseleave', 'poi', () => {
                map.getCanvas().style.cursor = ''
            })
        }
        function setupClickedPoi() {
            map.on('click', 'poi', (e) => {
                let properties = e.features[0].properties
                let description = generateDescriptionForPoi(properties)
        
                new mapboxgl.Popup()
                    .setLngLat(e.lngLat)
                    .setHTML(description).addTo(map)
            })
        }
        function generateDescriptionForPoi(properties) {
            let description = ''
            if (properties.numOfPois > 1) {
                description += `<strong>Multiple POIs</strong><br><em>${properties.numOfPois} 
                    POIs here,<br>zoom in to see details</em>`
            } else {
                description += `<strong>POI: ${properties.groupId.replace(/^\w/, c => c.toUpperCase())}</strong><br>`
                const tags = properties.numOfPois === 1 ?
                    JSON.parse(properties.tags) : {}
                description += tags.name ? tags.name : '<em>Name not listed...</em>'
            }
            return description
        }
        
        async function updateMap() {
            pBar.show()
            const locations = getMarkersLocations(marker1, marker2)
            let scores = await getScores(locations)
            
            const closestPois = getClosestPois(scores)
        
            updateLegend(scores)
        
            const poiGeoJSON = poiLocationsToJSON(closestPois)
            setMapClosestPoiSource(poiGeoJSON)
        
            // Getting and showing routes for nearest POI
            await createRoutes(locations, closestPois)
            pBar.hide()
        }
        
        function getMarkersLocations(marker1, marker2) {
            let locations = []
            locations[0] = { ...marker1.getLngLat(), id: 0 }
            locations[1] = { ...marker2.getLngLat(), id: 1 }
            return locations
        }
        
        // Location Scoring API request
        async function getScores(locations) {
            const results = await client.quality.fetch(locations, {
                'poi': {
                    type: 'closestPoiDistance',
                    osmTypes: [
                        {
                            'key': 'group',
                            'value': POI_TYPE
                        }
                    ],
                    maxEdgeWeight: MAX_TRAVEL,
                    edgeWeight: EDGE_WEIGHT,
                    travelMode: {
                        [TRAVEL_MODE]: {}
                    },
                    coreServiceUrl: 'https://api.targomo.com/westcentraleurope/'
                }
            }, true)
        
            return results.data
        }
        
        function getClosestPois(scores) {
            let closestPois = []
            Object.values(scores).map((markerScore, index) => {
                let poi = getFirstPoiInDetails(markerScore)
                let latLngId = nodeToLatLngId(index, poi)
                closestPois.push(latLngId)
            })
            return closestPois
        }
        function getFirstPoiInDetails(marker) {
            return Object.keys(marker.details.poi).map((key) => {
                return marker.details.poi[key][0]
            })[0]
        }
        function nodeToLatLngId(id, node) {
            return { id: id, lat: node.lat, lng: node.lng }
        }
        
        function updateLegend(data) {
            let marker1Score = Math.round(data[0].scores['poi']*100)/100
            let marker2Score = Math.round(data[1].scores['poi']*100)/100
            document.querySelector('#markerScores1').innerHTML = marker1Score
            document.querySelector('#markerScores2').innerHTML = marker2Score
        }
        
        function poiLocationsToJSON(latlngid) {
            return turf.featureCollection(latlngid.map(p => turf.point([p.lng, p.lat])))
        }
        
        function setMapClosestPoiSource(data) {
            map.getSource('closest-poi-sources').setData(data)
        }
        
        // Routing API request
        async function createRoutes(locations, pois) {
            const options = {
                travelType: TRAVEL_MODE,
                maxEdgeWeight: MAX_TRAVEL,
                edgeWeight: EDGE_WEIGHT,
                pathSerializer: 'geojson',
                // yes, 'polygon'... this comes from a legacy implementation when polygons were the only service. 
                // Will be changing in the future to a more generalized approach.
                polygon: {
                    srid: 4326
                }
            }
        
            let features = []
        
            for (let location of locations) {
                const target = pois.find(p => +p.id === +location.id)
                if (target) {
                    const route = await client.routes.fetch([location], [target], options)
                    features = [...features, ...route[0].features]
                }
            }
        
            const routeGeoJSON = turf.featureCollection(features)
        
            map.getSource('closest-route-sources').setData(routeGeoJSON)
        
            map.fitBounds(turf.bbox(routeGeoJSON), { padding: 50 })
        }
        </script>
</body>
</html>
content_copy
Copied to clipboard