Maplibre-GL Filter by Travel Time
Finding out your vacation rental is too far from the beach is a letdown - symbolize and filter houses (4,389!) based on walk time to the beach in Lerici, Italy
<!DOCTYPE html>
<html>
<head>
<!-- Include targomo core -->
<script src="https://releases.targomo.com/core/latest.min.js"></script>
<!-- Include maplibregl javascript and css -->
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css" rel="stylesheet">
<!-- 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%;
}
#filter {
position: absolute;
margin: 10px;
font-family: 'Open Sans', sans-serif;
top: 0;
display: flex;
align-items: center;
padding: 5px 8px;
background: #fff;
}
</style>
</head>
<body>
<!-- where the map will live -->
<div id="map"></div>
<div id="filter">
<div>Walk time (m)</div>
<input type="range" min="0" max="30" value="30" name="time" step="1"
onchange="rangevalue.value=value; filterPoints(value)" />
<output id="rangevalue">30</output>
</div>
<script>
// create targomo client
const client = new tgm.TargomoClient('westcentraleurope', '__targomo_key_here__')
// define map center and search source
const lnglat = [9.918, 44.08]
const travelTimes = [300, 600, 900, 1200, 1500, 1800]
// set the progress bar
const pBar = new mipb({ fg: "#FF8319" })
pBar.show()
// add the map and set the center
const map = new maplibregl.Map({
container: 'map',
style: client.basemaps.getGLStyleURL("Basic"),
zoom: 12.5, center: lnglat
}).addControl(new maplibregl.NavigationControl())
// disable scroll zoom
map.scrollZoom.disable()
map.on('load', () => {
// add empty source
map.addSource('sources', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
})
map.addLayer({
id: 'beaches',
type: 'circle',
source: 'sources',
paint: {
'circle-radius': 7,
'circle-color': '#DAA520',
'circle-stroke-width': 1.5,
'circle-stroke-color': '#fff'
}
}, 'bridge_major')
// add empty source
map.addSource('targets', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] },
attribution: '<a href="https://www.targomo.com/developers/resources/attribution/" target="_blank">© Targomo</a>'
})
// reachable houses
map.addLayer({
id: 'houses-reachable',
type: 'circle',
source: 'targets',
filter: ['<', 'travelTime', 30 * 60],
paint: {
'circle-radius': {
base: 1.75,
stops: [[12, 2], [22, 180]]
},
'circle-color': {
property: 'travelTime',
stops: [
[360, '#1a9641'],
[720, '#a6d96a'],
[1080, '#ffffbf'],
[1440, '#fdae61'],
[1800, '#d7191c']
]
},
}
}, 'beaches')
// unreachable houses, symbolized gray
map.addLayer({
id: 'houses-unreachable',
type: 'circle',
source: 'targets',
filter: ['>=', 'travelTime', 30 * 60],
paint: {
'circle-radius': {
base: 1.75,
stops: [[12, 2], [22, 180]]
},
'circle-color': '#aaa'
}
}, 'houses-reachable')
// go get the actual data
dataInit()
})
async function dataInit() {
const beachurl = 'https://raw.githubusercontent.com/targomo/data-exports/master/overpass/beach_public_lerici.geojson'
const houseurl = 'https://raw.githubusercontent.com/targomo/data-exports/master/overpass/building_house_lerici.geojson'
// get OSM public beaches dataset
const beaches = await fetch(beachurl).then(async (data) => {
return await data.json()
})
// update map
map.getSource('sources').setData(beaches)
// get OSM building=house dataset
const houses = await fetch(houseurl).then(async (data) => {
return await data.json()
})
// update map
map.getSource('targets').setData(houses)
// calculate reachability of houses from beaches
getReachability(beaches, houses)
}
function getReachability(beaches, houses) {
// create formatted 'sources' for analysis
const sources = beaches.features.map((beach) => {
return {
id: beach.properties['@id'],
lat: beach.geometry.coordinates[1],
lng: beach.geometry.coordinates[0]
}
})
// create formatted 'targets' for analysis
const targets = houses.features.map((house) => {
return {
id: house.properties['@id'],
lat: house.geometry.coordinates[1],
lng: house.geometry.coordinates[0]
}
})
// define some options for the reachability service
const options = {
travelType: 'walk',
maxEdgeWeight: 30 * 60,
edgeWeight: 'time'
}
// calculate reachability of houses from beaches
client.reachability.combined(sources, targets, options).then((reachability) => {
// only consider the minimum travel time
const combinedReach = {}
targets.forEach(place => combinedReach[String(place.id)] = -1)
reachability.forEach(target => {
const id = String(target.id)
if (!combinedReach[id]) {
console.warn('NOT FOUND', String(target.id))
} else {
if (target.travelTime > -1) {
if (combinedReach[id] > -1) {
combinedReach[id] = Math.min(combinedReach[id], target.travelTime)
} else {
combinedReach[id] = target.travelTime
}
}
}
})
// map targets to travel times
const reach = {
type: 'FeatureCollection',
features: targets.map(place => {
let location = {
type: 'Feature',
properties: {
travelTime: combinedReach[String(place.id)] > -1 ?
combinedReach[String(place.id)] :
30 * 60 + 1
},
geometry: {
type: 'Point',
coordinates: [place.lng, place.lat]
}
}
return location
})
}
// update source with travel-time augmented data
map.getSource('targets').setData(reach)
pBar.hide()
})
}
function filterPoints(minutes) {
// update the map filter to update symbolization based on range value
map.setFilter('houses-reachable', ['<', 'travelTime', minutes * 60])
map.setFilter('houses-unreachable', ['>=', 'travelTime', minutes * 60 + 1])
}
</script>
</body>
</html>
Copied to clipboard