Skip to content

Instantly share code, notes, and snippets.

@jsanz
Last active June 29, 2018 08:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jsanz/1bc2f6a9c998fbc014bc1820dcb2c76d to your computer and use it in GitHub Desktop.
Save jsanz/1bc2f6a9c998fbc014bc1820dcb2c76d to your computer and use it in GitHub Desktop.
#MapboxGL + #OSM data

Get data from OSM using Overpass API and render it on a Mapbox GL map.

<html>
<head>
<link rel='stylesheet' href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.css' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.44.1/mapbox-gl.js'></script>
<script src="https://tyrasd.github.io/osmtogeojson/osmtogeojson.js"></script>
<style>
#map {
margin: 0;
height: 100vh;
width: 100%;
}
.mapboxgl-popup-content a {
color: #4a86cf;
text-decoration-line: none;
}
.mapboxgl-popup-content ul {
padding-left: 18px;
}
.osm-source{
font-size: 0.7em;
}
.filter-ctrl {
position: absolute;
top: 18px;
right: 60px;
z-index: 1;
width: 180px;
}
.filter-ctrl input[type=text] {
font: 12px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
width: 100%;
border: 0;
background-color: #fff;
height: 40px;
margin: 0;
color: rgba(0,0,0,.5);
padding: 10px;
box-shadow: 0 0 0 2px rgba(0,0,0,0.1);
border-radius: 3px;
}
.mapboxgl-popup-anchor-top .mapboxgl-popup-content{
background-color: #B4C6DD;
padding: 5px;
}
</style>
</head>
<body>
<div id="map" style="width: 100%; height: 100vh;"></div>
<div class='filter-ctrl'>
<input id='filter-input' type='text' name='filter' placeholder='Filter by name' />
</div>
<script>
/* parameters */
var /* List of ways to include in the map */
osm_ways = [
27152910, // almeria railway
29220363, // almeria university
435775764, // Civitas
37923960, // airport
{ osm_id: 36406179, name: 'UAL Parking'}, // UAL parking
],
/* List of nodes to include in the map */
osm_nodes = [
4414057566, // la mala
{ osm_id: 4433529185, name: 'Overriden name'}, // hortensia
2870058034, // 292
974730957, // 144
],
/* Basemap Styles
OpenMapTiles https://openmaptiles.github.io/positron-gl-style/style-cdn.json
CARTO https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json
*/
basemap_style = 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json',
/* initial center and zoom level, you can use bboxfinder.com to find proper values */
center = [-2.421,36.823],
zoom = 12,
/* Main color to use anywhere */
main_color = '#4a86cf',
/* Icon for the points ont the map */
icon = 'https://i.imgur.com/cux5ypg.png', // '/theme/img/Gnomelogo-footprint.png',
/* White list of properties to allow to
be displayed in the popup, order matters! */
popup_properties = [
'description',
'shop','amenity','wheelchair',
'highway', 'network', 'bench', 'shelter', 'ref',
'adr:street',
'website','wikidata','wikipedia'
]
;
</script>
<script>
/* Functions and helpers */
var overpass_url = 'http://overpass-api.de/api/interpreter',
/* from https://www.npmjs.com/package/geojson-polygon-center */
polygon_center = function (polygon) {
var minx = miny = 1000,
maxx = maxy = -1000
polygon = polygon[0]
for (var i = 0; i < polygon.length; i++) {
var point = polygon[i]
var x = point[0]
var y = point[1]
if (x < minx) minx = x
else if (x > maxx) maxx = x
if (y < miny) miny = y
else if (y > maxy) maxy = y
}
return {
type: 'Point',
coordinates: [
minx + ((maxx - minx) / 2),
miny + ((maxy - miny) / 2)
]
}
},
/* helper to get the list of ids */
get_ids = function(el){
return typeof el == "number" ? el : el['osm_id'];
},
/* takes a feature, and augment it with any custom properties
passed on thi list of nodes and ways */
get_props = function(feature){
var /* filter checker */
filter_node = function(node){
return typeof node != "number" && node['osm_id'] == feat_id;
},
properties = feature['properties'],
/* feature id */
feat_id = properties['id'],
/* candidates */
candidate = osm_nodes.filter(filter_node)
.concat(osm_ways.filter(filter_node));
if (candidate.length == 1){
properties = Object.assign(properties,candidate[0]);
};
return feature;
},
/* Generate a valid OSM Overpass API request */
get_osm_query = function(){
var ways = osm_ways.map(get_ids).join(',');
var nodes = osm_nodes.map(get_ids).join(',');
return `[out:xml][timeout:300];
(
way(id:${ways});
node(id:${nodes});
)->.a;
(.a; .a >;);out qt;`
}
/* moves tags up to the main properties function */
tags_to_props = function(feature){
properties = feature['properties']
tags = properties['tags'];
Object.assign(properties, tags);
delete properties['tags'];
if (properties['id'] == undefined){
properties['id'] = String(feature['id'])
} else {
properties['id'] = String(properties['id'])
};
// Override the name in Engish, if it exists
if (properties['name:en'] != undefined){
properties['name'] = properties['name:en']
};
return feature
},
/* transforms OSM data into geojson and adds that as
points and labels to the map */
load_osm_data = function(data){
console.log('loading data...');
var // Convert to GeoJSON
geojson_data = osmtogeojson(data),
// Filter ways
polys_geojson = geojson_data.features.filter(function(feature){
return feature.properties.type == "way"
}),
// Filter points
points_geojson = geojson_data.features.filter(function(feature){
return feature.properties.type == "node"
}),
// Generate centroids for points
polys_geojson_points = polys_geojson.map(function(poly){
copy = JSON.parse(JSON.stringify(poly));
copy['geometry'] = polygon_center(copy.geometry.coordinates);
return copy
}),
// Get together both set of points
all_features = points_geojson.concat(polys_geojson_points),
// Get all properties out from the tags
points_geojson_props = all_features.map(tags_to_props),
// Override any custom properties
final_points = points_geojson_props.map(get_props);
// Build final geojson collection
return {
'type': 'FeatureCollection',
'features': final_points
};
},
/* Loads the data retrieved into a mapboxgl map */
add_layers = function(map, data){
map.loadImage(icon, function(error, image) {
if (error) throw error;
map.addImage('gnome', image);
/* Icon layer */
map.addLayer({
'id': 'guadec_icon',
'type': 'symbol',
'source': {
'type': 'geojson',
'data': geojson_data
},
'layout': {
"symbol-placement": "point",
"text-field": '{name}' ,
"icon-image": "gnome",
"text-font": ["Open Sans Regular"],
"icon-allow-overlap": true,
"text-offset": [.3,.3],
"text-anchor": "top-left",
"text-max-width": 5,
"text-justify": "left",
"text-allow-overlap": false,
"text-optional": true,
"icon-size": {
"stops": [
[0, 0.10],
[12, 0.12],
[14, 0.15],
[18, 0.25]
]
},
"text-size": {
"stops": [
[0, 0],
[9,0],
[12, 15],
[16, 20]
]
}
},
'paint': {
"text-color": main_color,
"icon-opacity": 1,
"text-opacity": {
"stops": [
[0, 0],
[9,0],
[12, 1]
]
},
"text-halo-color": "white",
"text-halo-width": 1.5,
"text-halo-blur": 1
}
},'place_hamlet');
});
};
</script>
<script>
/* SCRIPT */
var geojson_data = {},
map = new mapboxgl.Map({
container: 'map',
style: basemap_style,
center: center,
zoom: zoom,
attributionControl: true
});
/*Once map is loaded, get data from OSM to add as a new layer */
map.on('load', function() {
console.log('fetching osm data...')
fetch(overpass_url,{
method: "POST",
body: get_osm_query()})
.then(response => response.text())
.then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
.then(function(data){
// Get the geojson to work
geojson_data = load_osm_data(data);
console.log(geojson_data);
// Add it as a layer
add_layers(map,geojson_data);
})
.catch(error => console.log("Error:", error));
});
/* Navigation control */
map.addControl(new mapboxgl.NavigationControl());
/* Popup up singleton */
var tooltip = new mapboxgl.Popup({
closeButton: false,
closeOnClick: true,
anchor: "top"
});
// helper to render the properties
var get_properties_list = function(properties){
return popup_properties.filter(function(key){
return (Object.keys(properties).findIndex(x => x == key) > -1)
})
.map(function(key){
if (key == 'website'){
return `<li><a href="${properties[key]}">${key}</a></li>`
} else if (key == 'wikidata'){
return `<li><a href="https://www.wikidata.org/wiki/${properties[key]}">${key}</a></li>`
} else if (key == 'wikipedia'){
return `<li><a href="https://en.wikipedia.org/wiki/${properties[key]}">${key}</a></li>`
} else {
return `<li><strong>${key}</strong>: ${properties[key]}</li>`
}
}).join('')
};
var interactivity_handler = function(location,is_tooltip){
var features = map.queryRenderedFeatures(location.point, {
layers: ['guadec_icon']
});
tooltip.remove();
if (features != ''){
popup = null;
var feature = features[0];
if (is_tooltip){
tooltip.setHTML(`<span>${feature.properties.name}</span>`)
.setLngLat(location.lngLat)
.addTo(map);
} else {
popup = new mapboxgl.Popup({anchor:'bottom'})
.setHTML(`
<h3>${feature.properties.name}</h3>
<ul>
${get_properties_list(feature.properties)}
</ul>
<p class="osm-source"><a href="https://www.openstreetmap.org/${feature.properties.type}/${feature.properties.id}">Source</a></p>
`
)
.setLngLat(location.lngLat)
.addTo(map);
}
}
}
/* Popup interactivity */
map.on('click',function(location){
interactivity_handler(location,false);
});
/* Popup interactivity */
map.on('mousemove',function(location){
interactivity_handler(location,true);
});
/* Filter control */
document
.getElementById('filter-input')
.addEventListener('keyup', function(e) {
function normalize(string) {
return string.trim().toLowerCase();
};
// Get the value of the input
var value = normalize(e.target.value);
if (value == ""){
// If it's empty remove the filter
map.setFilter('guadec_icon', null);
} else {
// Filter the geojson features and get their ids
ids = geojson_data.features
.filter( x => normalize( x['properties']['name']).match(new RegExp(value,"g")) != null)
.map(x => x['properties']['id'])
// Set the filter of the layer to match those ids
map.setFilter('guadec_icon', ['match', ['get', 'id'], ids, true, false]);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment