[guadec-web: 1/3] added routes and refactor
- From: Oliver Gutiérrez <ogutierrez src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [guadec-web: 1/3] added routes and refactor
- Date: Sat, 30 Jun 2018 22:49:14 +0000 (UTC)
commit c4ce2e0b6cb63248b52c77ee56679ffa928c7f1d
Author: Jorge Sanz <xurxosanz gmail com>
Date: Sat Jun 30 12:55:44 2018 +0200
added routes and refactor
content/pages/map.md | 133 +++++-----
src/js/guadec_map/guadec-map.js | 536 +++++++++++++++++++++++++++-------------
2 files changed, 441 insertions(+), 228 deletions(-)
---
diff --git a/content/pages/map.md b/content/pages/map.md
index f599349..7d79c6c 100644
--- a/content/pages/map.md
+++ b/content/pages/map.md
@@ -5,6 +5,9 @@ Date: 20180615
<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>
+<script src="https://cdn.jsdelivr.net/npm/@mapbox/polyline@1.0.0/src/polyline.min.js"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/tinycolor/1.4.1/tinycolor.min.js"></script>
+
<script src='/theme/js/guadec-map/guadec-map.js'></script>
<style>
@@ -23,25 +26,6 @@ Date: 20180615
.osm-source{
font-size: 0.7em;
}
- .filter-ctrl {
- position: absolute;
- top: 10px;
- right: 50px;
- 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-tip{
display:none;
}
@@ -56,9 +40,11 @@ Date: 20180615
}
.mapboxgl-popup-content p {
margin: 0;
+ max-width: 100px;
}
.mapboxgl-popup-content h3 {
font-size: 1rem;
+ margin-top: 5px;
}
.mapboxgl-popup-close-button{
color: #4a86cf;
@@ -66,9 +52,6 @@ Date: 20180615
</style>
<div id="map">
-<div class='filter-ctrl'>
- <input id='filter-input' type='text' name='filter' placeholder='Filter by name' />
-</div>
</div>
<script type="module">
@@ -99,13 +82,40 @@ Date: 20180615
{ osm_id: 1304074112, name: 'Airport bus stop',icon: 'bus-45'}, // 188
{ osm_id: 999522025, name: 'ATM', icon: 'bank-45'}, // ATM machine
],
- /* list of routes to render */
+ /*
+ list of routes to render
+ FROM and TO need are OSM node identifiers
+ It's BETTER if all nodes are already in the
+ osm_nodes list but not mandatory.
+ */
routes : [
{
from: 2870058034,
- to: 469474241,
- title: 'Sample route',
+ to: 4414057566,
+ title: 'first route',
+ description: 'This is a longer text to describe the route, labels need to be short!',
color: '#f00'
+ },
+ {
+ from: 974730957,
+ to: 1304074112,
+ title: 'second route',
+ description: 'This is a longer text to describe the route, labels need to be short!',
+ color: '#4a86cf'
+ },
+ {
+ from: 974730957,
+ to: 4414057566,
+ title: 'third route',
+ description: 'This is a longer text to describe the route, labels need to be short!',
+ color: '#526635'
+ },
+ {
+ from: 1711201698,
+ to: 441236298,
+ title: 'fourth route',
+ description: 'This is a longer text to describe the route, labels need to be short!',
+ color: '#5c3566'
}
],
/* Basemap Styles
@@ -128,44 +138,49 @@ Date: 20180615
'highway', 'network', 'bench', 'shelter', 'ref',
'adr:street', 'picture',
'website','wikidata','wikipedia'
- ]
+ ],
+ mapbox_token :
'pk.eyJ1IjoieHVyeG9zYW56IiwiYSI6ImNqaXk4NW40MTA3NWUzcG5vMjlobWk2dGIifQ.iI-Ns8Qh5uEg9dDwZnnecw'
};
- (async function() {
+ var guadec_light_color = tinycolor(options.main_color).lighten(35).toHexString();
+ var guadec_map = new GuadecMap(options);
+ var map = null;
+
+ // Promise to load the map
+ var get_map = new Promise((resolve,reject)=>{
+
+ // Get the map
+ map = guadec_map.init_map();
- var guadec_map = new GuadecMap(options);
- var map = null;
+ // Do some style tweaks and then return it
+ map.on('load',function(){
+ map.removeLayer('place_suburbs')
+ .removeLayer('building-top')
+ .setPaintProperty('building','fill-color',guadec_light_color);
- // Promise to load the map
- async function get_map(){
- return new Promise((resolve,reject)=>{
- map = guadec_map.init_map();
- map.on('load',function(){
- resolve();
- })
- });
- }
+ resolve();
+ });
+ });
- // Promise to load the data from OSM
- async function get_data(){
- var osm_data = await guadec_map.fetch_data();
- return new Promise((resolve,reject)=>{
- var geojson_data = guadec_map.process_osm_data(osm_data);
- guadec_map.set_geojson_data(geojson_data);
- resolve();
- });
- }
+ // Promise to load the data from OSM
+ var get_data = new Promise((resolve,reject) => {
+ guadec_map.fetch_data().then(osm_data => {
+ guadec_map.process_osm_data(osm_data);
+ resolve();
+ }).catch(error =>reject(error));
+ });
- // When map and OSM data is retrieved, we can load POIS and Routes
- Promise.all([
- get_map(),
- get_data()
- ]).then(function(){
- // Load the POIS
- guadec_map.load_pois();
- // Load the Routes
- guadec_map.load_routes();
- })
+ // When map and OSM data are retrieved, we can load POIS and Routes
+ Promise.all([
+ get_map,
+ get_data
+ ]).then(() => {
+ // Load the Routes
+ guadec_map.load_routes();
+ // Load the POIS
+ guadec_map.load_pois();
+ }).catch(error => console.log(error));
- })();
-</script>
+ // make the map a global for testing
+ window.guadec_map = map;
+</script>
\ No newline at end of file
diff --git a/src/js/guadec_map/guadec-map.js b/src/js/guadec_map/guadec-map.js
index 754a519..418f83c 100644
--- a/src/js/guadec_map/guadec-map.js
+++ b/src/js/guadec_map/guadec-map.js
@@ -31,131 +31,134 @@ class GuadecMap {
]
}
}
-
- /* helper to get the list of ids */
- static get_ids(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 */
- static get_props(options,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 = options.osm_nodes.filter(filter_node)
- .concat(options.osm_ways.filter(filter_node));
- if (candidate.length == 1){
- properties = Object.assign(properties,candidate[0]);
+ _get_routes(context, geometries){
+ // Data needed
+ var default_color = context.options.main_color;
+ var map = context.map;
+ var routes = context.options.routes;
+ var token = context.options.mapbox_token;
+
+ var get_coordinates = function(id){
+ var cand = geometries.filter(x => x.id == id );
+ if (cand.length ==1){
+ return cand[0];
+ } else {
+ return undefined;
+ }
};
- return feature;
- }
-
- /* Generate a valid OSM Overpass API request */
- get_osm_query(options){
- var ways = options.osm_ways.map(GuadecMap.get_ids).join(',');
- var nodes = options.osm_nodes.map(GuadecMap.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 */
- static tags_to_props(feature){
- var 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'])
- };
+ var get_route = function(from,to) {
+ var base_url = 'https://api.mapbox.com/directions/v5/mapbox/driving';
+ var params = {
+ 'overview': 'full',
+ 'geometries': 'polyline6',
+ 'access_token': token
+ };
- // Override the name in Engish, if it exists
- if (properties['name:en'] != undefined){
- properties['name'] = properties['name:en']
+ // Transform to string
+ var coordinates = from.join(',') + ';' + to.join(',');
+ var params_str = Object.keys(params).map( p => `${p}=${params[p]}`).join('&');
+ // Final URL to get data from Mapbox
+ var url = `${base_url}/${coordinates}.json?${params_str}`
+
+ return fetch(url).then(response => response.json())
};
- return feature
- }
-
- /* transforms OSM data into geojson and adds that as
- points and labels to the map */
- process_osm_data(data){
- console.log('loading data...');
- var context = this,
- // 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){
- var copy = JSON.parse(JSON.stringify(poly));
- copy['geometry'] = GuadecMap.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(GuadecMap.tags_to_props),
- get_props_amp = function(feature){
- return GuadecMap.get_props(context.options,feature)
- },
- final_points = points_geojson_props.map(get_props_amp);
-
- // Build final geojson collection
- return {
- 'type': 'FeatureCollection',
- 'features': final_points
- };
- }
+ var features_promises = routes.map(function(route){
+ return new Promise((resolve,reject) => {
+ // Get route data
+ var from = get_coordinates(route['from']);
+ var to = get_coordinates(route['to']);
+ var title = route['title'];
+ var color = route['color'];
+ var description = route['description'];
+
+ if (from && to){
+ var from_geom = from.coordinates;
+ var to_geom = to.coordinates;
+ get_route(from_geom,to_geom)
+ .then(function(route){
+ if (route.routes){
+ var the_route = route.routes[0];
+ resolve({
+ type : 'Feature',
+ geometry : polyline.toGeoJSON(the_route.geometry,6),
+ properties : {
+ distance : the_route['distance'],
+ duration: the_route['duration'],
+ name : title,
+ color : color,
+ description: description
+ }
+ })
+ } else {
+ reject({
+ error : 'no routes found',
+ data : route
+ });
+ }
+ });
+ } else {
+ reject({
+ 'error' : 'bad data',
+ 'data' : route
+ });
+ }
+ });
+ });
- set_geojson_data(data){
- this.geojson_data = data;
+ Promise.all(features_promises)
+ .then(function(values){
+ /* Route layer */
+ map.addLayer({
+ 'id': 'guadec_routes',
+ 'type': 'line',
+ 'source': {
+ 'type': 'geojson',
+ 'data': {
+ 'type' : 'FeatureCollection',
+ 'features': values
+ }
+ },
+ 'layout': {},
+ 'paint': {
+ "line-color":[
+ "case",
+ ["has", 'color'], ["get", "color"],
+ default_color,
+ ],
+ "line-width": 2
+ }
+ },'boundary_country_inner');
+ })
+ .catch( error => console.error(error));
}
-
+
+ /* This method initializes and returns a Mapbox map instance
+ with the interacitivity ready to be used */
init_map(){
- /*
- TODO move this to a class instantiation so we can put the options on a constructor and then have
methods to render the pois and the routes
- */
- var options = this.options;
+ var context = this;
+ var options = context.options;
var map = new mapboxgl.Map({
- container: 'map',
- style: options.basemap_style,
- center: options.center,
- zoom: options.zoom,
- attributionControl: true
- });
-
+ container: 'map',
+ style: options.basemap_style,
+ center: options.center,
+ zoom: options.zoom,
+ attributionControl: true,
+ hash: true
+ });
+
/* Navigation control */
map.addControl(new mapboxgl.NavigationControl());
-
+
/* Popup up singleton */
var tooltip = new mapboxgl.Popup({
closeButton: false,
closeOnClick: true,
anchor: "top",
offset: [0, 8]
- });
+ });
// helper to render the properties
var get_properties_list = function(properties){
@@ -179,37 +182,106 @@ class GuadecMap {
};
var interactivity_handler = function(location,is_tooltip){
- if (! map.getLayer('guadec_icon')) return;
-
- var features = map.queryRenderedFeatures(location.point, {
- layers: ['guadec_icon']
- });
+ if (! map.getLayer('guadec_icon') || ! map.getLayer('guadec_routes')) return;
+
+ var features_icons = map.queryRenderedFeatures(location.point, { layers: ['guadec_icon'] });
+ var features_routes = map.queryRenderedFeatures(location.point, { layers: ['guadec_routes'] });
+
+ // remove previous interactivity elements
tooltip.remove();
-
- if (features != ''){
+ if (typeof map.getLayer('selected_route') !== "undefined" ){
+ map.removeLayer('selected_route')
+ map.removeSource('selected_route');
+ }
+
+ if (features_icons != ''){
+ var feature = features_icons[0];
var popup = null;
- var feature = features[0];
+ var popup_content = null;
if (is_tooltip){
- tooltip.setHTML(`<span>${feature.properties.name}</span>`)
- .setLngLat(location.lngLat)
- .addTo(map);
+ popup = tooltip;
+
+ if (feature.properties.cluster){
+ popup_content = `<span>${feature.properties.point_count} points</span>`;
+ } else {
+ popup_content = `<span>${feature.properties.name}</span>`;
+ }
} else {
popup = new mapboxgl.Popup({
anchor:'bottom',
+ closeOnClick: true,
className:'guadec-popup'
})
- .setHTML(`
+
+ if (feature.properties.cluster){
+ popup_content = `<h3>${feature.properties.point_count} points</h3>`;
+ } else {
+ popup_content = `
<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>
- `
- )
+ ${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>
+ `;
+ }
+
+
+ }
+
+ popup.setHTML(popup_content)
+ .setLngLat(location.lngLat)
+ .addTo(map);
+
+ } else if ( features_routes != '') {
+ var feature = features_routes[0];
+ var highlight_color = tinycolor(options.main_color).lighten(20);
+
+ // Render the feature
+ map.addSource('selected_route', {
+ "type":"geojson",
+ "data": feature.toJSON()
+ });
+ map.addLayer({
+ "id": "selected_route",
+ "type": "line",
+ "source": "selected_route",
+ "layout": {
+ "line-join": "round",
+ "line-cap": "round"
+ },
+ "paint": {
+ "line-color": highlight_color.toHexString(),
+ "line-width": 8
+ }
+ },'guadec_routes');
+
+ // Render the interactivity
+
+ var popup = null;
+ var popup_content = null;
+
+ if (is_tooltip){
+ popup = tooltip;
+ popup_content = `<span>${feature.properties.name}</span>`;
+ } else {
+ popup = new mapboxgl.Popup({
+ anchor:'bottom',
+ closeOnClick: true,
+ className:'guadec-popup'
+ });
+ popup_content = `
+ <h3>${feature.properties.name}</h3>
+ <p>${feature.properties.description}</p>`;
+ }
+
+ popup.setHTML(popup_content)
.setLngLat(location.lngLat)
.addTo(map);
- }
}
}
@@ -218,40 +290,134 @@ class GuadecMap {
interactivity_handler(location,false);
});
-
+
/* Popup interactivity */
map.on('mousemove',function(location){
interactivity_handler(location,true);
});
-
-
-
+
this.map = map;
return map;
}
-
+
+ /* Get data from OSM and return a promise when is parsed */
fetch_data(){
- var context = this;
+ var context = this,
+ _get_osm_query= function(options){
+ var _get_ids = el => typeof el == "number" ? el : el['osm_id'];
+
+ var ways = options.osm_ways.map(_get_ids).join(',');
+ var nodes = options.osm_nodes.map(_get_ids).join(',');
+
+ return `[out:xml][timeout:300];
+ (
+ way(id:${ways});
+ node(id:${nodes});
+ )->.a;
+ (.a; .a >;);out qt;`
+ };
+
console.log('fetching osm data...')
+
return fetch(this.overpass_url,{
method: "POST",
- body: this.get_osm_query(this.options)
+ body: _get_osm_query(this.options)
})
.then(response => response.text())
.then(str => (new window.DOMParser()).parseFromString(str, "text/xml"))
}
+
+
+ /* transforms OSM data into geojson and adds that as
+ points and labels to the map */
+ process_osm_data(data){
+ console.log('loading data...');
+ var context = this,
+ // 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){
+ var copy = JSON.parse(JSON.stringify(poly));
+ copy['geometry'] = GuadecMap.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(function(feature){
+ var 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
+ }
+ ),
+ /* takes a feature, and augment it with any custom properties
+ passed on thi list of nodes and ways */
+ final_points = points_geojson_props.map(function(feature){
+ var options = context.options,
+ /* 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 = options.osm_nodes.filter(filter_node)
+ .concat(options.osm_ways.filter(filter_node));
+
+ if (candidate.length == 1){
+ properties = Object.assign(properties,candidate[0]);
+ };
+
+ return feature;
+ });
+
+ context.geojson_data = {
+ 'type': 'FeatureCollection',
+ 'features': final_points
+ };
+ // Build final geojson collection
+ return context.geojson_data;
+ }
+
load_pois(){
var geojson_data = this.geojson_data;
var options = this.options;
var map = this.map;
+ var poi_color = tinycolor(options.main_color).darken().toHexString();
+
/* Icon layer */
map.addLayer({
'id': 'guadec_icon',
'type': 'symbol',
'source': {
'type': 'geojson',
- 'data': geojson_data
+ 'data': geojson_data,
+ 'cluster' : true,
+ 'clusterMaxZoom': 12
},
'layout': {
"symbol-placement": "point",
@@ -271,10 +437,10 @@ class GuadecMap {
"text-optional": true,
"icon-size": {
"stops": [
- [0, 0.3],
- [12, 0.6],
- [14, 1],
- [18, 1.5],
+ [0, 0.1],
+ [12, 0.7],
+ [13,0.5],
+ [20, 1.5],
]
},
"text-size": {
@@ -287,7 +453,7 @@ class GuadecMap {
}
},
'paint': {
- "text-color": options.main_color,
+ "text-color": poi_color,
"icon-opacity": 1,
"text-opacity": {
"stops": [
@@ -301,38 +467,70 @@ class GuadecMap {
"text-halo-blur": 1
}
},'place_hamlet');
-
-
-
- /* 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
- var 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]);
- }
- });
}
-
+
load_routes(){
- throw "not implemented"
+ var routes = this.options.routes;
+ var data = this.geojson_data;
+
+ // generate a list of nodes and geometries
+ var geom_ids = data.features.map(function(feature){
+ return {
+ 'id' : feature.properties['id' ],
+ 'coordinates' : feature.geometry.coordinates
+ }
+ });
+
+ // get the list of route from/to ids not appearing in that list
+ var new_ids = Array.from(new Set (
+ routes.map(function(route){
+ // check from
+ if (geom_ids.filter(x => x.id == route.from).length == 0){
+ return route.from
+ }
+ })
+ .concat(
+ routes.map(function(route){
+ // check to
+ if (geom_ids.filter(x => x.id == route.to).length == 0){
+ return route.to;
+ }
+ })
+ )
+ .filter(x => x != undefined)
+ ));
+
+ // if needed get from overpass the rest of the geometries
+ if (new_ids.length>0){
+ var overpass_url = this.overpass_url;
+ var get_osm_geoms = new Promise(function(resolve,reject){
+ var new_ids_str = new_ids.join(',');
+ var osm_query = `[out:json][timeout:300];node(id:${new_ids_str}); out skel qt;`;
+
+ console.log('Fetching more data from OSM for the routes');
+ fetch(overpass_url,{ method: "POST", body: osm_query })
+ .then(response => response.json())
+ .then(function(data){
+ resolve(
+ geom_ids.concat(
+ data.elements.map(function(data){
+ return { 'id': data.id, 'coordinates' : [data.lon, data.lat] }
+ })
+ )
+ );
+ });
+ });
+ } else {
+ var get_osm_geoms = Promise.resolve(geom_ids);
+ }
+
+ /*
+ When the data is here, all set up to get the routes
+ from Mapbox
+ */
+ var context = this;
+ get_osm_geoms.then(function(all_geom_ids){
+ context._get_routes(context,all_geom_ids);
+ });
}
-
}
\ No newline at end of file
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]