Merge branch 'master' into master

This commit is contained in:
abrensch 2019-08-05 17:20:54 +02:00 committed by GitHub
commit 42abbbcb75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
104 changed files with 26871 additions and 1753 deletions

View file

@ -1,24 +1,28 @@
(function () {
var touchScreen = (function () {
(function() {
var touchScreen = (function() {
var result = null;
if ('maxTouchPoints' in navigator) {
result = navigator.maxTouchPoints > 0;
} else if (window.matchMedia && window.matchMedia('(any-pointer:coarse),(any-pointer:fine),(any-pointer:none)').matches) {
result = window.matchMedia("(any-pointer:coarse)").matches;
} else if (
window.matchMedia &&
window.matchMedia(
'(any-pointer:coarse),(any-pointer:fine),(any-pointer:none)'
).matches
) {
result = window.matchMedia('(any-pointer:coarse)').matches;
} else if ('msMaxTouchPoints' in navigator) {
result = navigator.msMaxTouchPoints > 0;
};
}
return result;
}()),
touchScreenDetectable = touchScreen !== null;
})(),
touchScreenDetectable = touchScreen !== null,
touch = touchScreenDetectable ? touchScreen : L.Browser.touch;
BR.Browser = {
touchScreen: touchScreen,
touchScreenDetectable: touchScreenDetectable
touchScreenDetectable: touchScreenDetectable,
touch: touch
};
}());
})();

311
js/LayersConfig.js Normal file
View file

@ -0,0 +1,311 @@
BR.LayersConfig = L.Class.extend({
defaultBaseLayers: BR.confLayers.defaultBaseLayers,
defaultOverlays: BR.confLayers.defaultOverlays,
legacyNameToIdMap: BR.confLayers.legacyNameToIdMap,
initialize: function(map) {
this._map = map;
this._addLeafletProvidersLayers();
this._customizeLayers();
this.loadDefaultLayers();
this._addLanguageDefaultLayer();
},
loadDefaultLayers: function() {
if (BR.Util.localStorageAvailable()) {
var item = localStorage.getItem('map/defaultLayers');
if (item) {
var defaultLayers = JSON.parse(item);
this.defaultBaseLayers = defaultLayers.baseLayers;
this.defaultOverlays = defaultLayers.overlays;
}
}
},
storeDefaultLayers: function(baseLayers, overlays) {
if (BR.Util.localStorageAvailable()) {
var defaultLayers = {
baseLayers: baseLayers,
overlays: overlays
};
localStorage.setItem(
'map/defaultLayers',
JSON.stringify(defaultLayers)
);
}
},
_addLeafletProvidersLayers: function() {
var includeList = BR.confLayers.leafletProvidersIncludeList;
for (var i = 0; i < includeList.length; i++) {
var id = includeList[i];
var obj = {
geometry: null,
properties: {
id: id,
name: id.replace('.', ' '),
dataSource: 'leaflet-providers'
},
type: 'Feature'
};
BR.layerIndex[id] = obj;
}
},
_customizeLayers: function() {
var propertyOverrides = BR.confLayers.getPropertyOverrides();
for (id in propertyOverrides) {
var layer = BR.layerIndex[id];
if (layer) {
var properties = propertyOverrides[id];
for (key in properties) {
var value = properties[key];
layer.properties[key] = value;
}
} else {
console.error('Layer not found: ' + id);
}
}
BR.layerIndex['MtbMap'].geometry = BR.confLayers.europeGeofabrik;
BR.layerIndex['1069'].geometry = BR.confLayers.europeGeofabrik;
BR.layerIndex['OpenStreetMap.CH'].geometry =
BR.confLayers.switzerlandPadded;
BR.layerIndex['1017'].geometry = BR.confLayers.osmapaPl;
},
_addLanguageDefaultLayer: function() {
// language code -> layer id
var languageLayersMap = {};
var i;
for (i = 0; i < BR.confLayers.languageDefaultLayers.length; i++) {
var id = BR.confLayers.languageDefaultLayers[i];
var layer = BR.layerIndex[id];
if (layer) {
var layerLanguage = layer.properties['language_code'];
if (layerLanguage) {
languageLayersMap[layerLanguage] = id;
}
}
}
// iterate language code hierarchy, e.g ["de-DE", "de", "en"] (includes `i18next.options.fallbackLng`)
for (i = 0; i < i18next.languages.length; i++) {
var language = i18next.languages[i];
var layerId = languageLayersMap[language];
if (layerId) {
this.defaultBaseLayers.unshift(layerId);
break;
}
}
},
isDefaultLayer: function(id, overlay) {
var result = false;
if (overlay) {
result = this.defaultOverlays.indexOf(id) > -1;
} else {
result = this.defaultBaseLayers.indexOf(id) > -1;
}
return result;
},
getBaseLayers: function() {
return this._getLayers(this.defaultBaseLayers);
},
getOverlays: function() {
return this._getLayers(this.defaultOverlays);
},
_getLayers: function(ids) {
var layers = {};
for (var i = 0; i < ids.length; i++) {
var layerId = ids[i];
var layerData = BR.layerIndex[layerId];
if (layerData) {
// when key required only add if configured
var keyObj = this.getKeyName(layerData.properties.url);
if (!keyObj || (keyObj && BR.keys[keyObj.name])) {
layers[layerData.properties.name] = this.createLayer(
layerData
);
}
} else {
console.error('Layer not found: ' + layerId);
}
}
return layers;
},
// own convention: key placeholder with prefix
// e.g. ?api_key={keys_openrouteservice}
getKeyName: function(url) {
var result = null;
// L.Util.template only matches [\w_-]
var prefix = 'keys_';
var regex = new RegExp('{' + prefix + '([^}]*)}');
var found, name;
if (!url) return result;
found = url.match(regex);
if (found) {
name = found[1];
result = {
name: name,
urlVar: prefix + name
};
}
return result;
},
createLayer: function(layerData) {
var props = layerData.properties;
var url = props.url;
var layer;
// JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
// Leaflet: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
function convertUrlJosm(url) {
var rxSwitch = /{switch:[^}]*}/;
var rxZoom = /{zoom}/g;
var result = url.replace(rxSwitch, '{s}');
result = result.replace(rxZoom, '{z}');
return result;
}
// JOSM: https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png
// Leaflet: ['a','b','c']
function getSubdomains(url) {
var result = 'abc';
var regex = /{switch:([^}]*)}/;
var found = url.match(regex);
if (found) {
result = found[1].split(',');
}
return result;
}
function convertAttributionJosm(props) {
var result = '';
var attr = props.attribution;
if (attr) {
if (attr.html) {
result = attr.html;
} else if (attr.url && attr.text) {
result =
'<a href="' +
attr.url +
'" target="_blank" rel="noopener">' +
attr.text +
'</a>';
} else if (attr.text) {
result = attr.text;
}
}
if (!result) {
console.warn('No attribution: ' + props.id);
}
return result;
}
var options = {
maxZoom: this._map.getMaxZoom(),
bounds:
layerData.geometry && !props.worldTiles
? L.geoJson(layerData.geometry).getBounds()
: null
};
if (props.mapUrl) {
options.mapLink =
'<a target="_blank" href="' +
props.mapUrl +
'">' +
(props.nameShort || props.name) +
'</a>';
}
if (props.attribution) {
options.attribution = props.attribution;
}
var keyObj = this.getKeyName(url);
if (keyObj && BR.keys[keyObj.name]) {
options[keyObj.urlVar] = BR.keys[keyObj.name];
}
if (props.dataSource === 'leaflet-providers') {
layer = L.tileLayer.provider(props.id);
var layerOptions = L.Util.extend(options, {
maxNativeZoom: layer.options.maxZoom
});
L.setOptions(layer, layerOptions);
} else if (props.dataSource === 'LayersCollection') {
layer = L.tileLayer(
url,
L.Util.extend(options, {
minZoom: props.minZoom || 0,
maxNativeZoom: props.maxZoom
})
);
if (props.subdomains) {
layer.subdomains = props.subdomains;
}
} else {
// JOSM
var josmUrl = url;
var url = convertUrlJosm(josmUrl);
var josmOptions = L.Util.extend(options, {
minZoom: props.min_zoom || 0,
maxNativeZoom: props.max_zoom,
subdomains: getSubdomains(josmUrl),
attribution: convertAttributionJosm(props)
});
if (props.type && props.type === 'wms') {
layer = L.tileLayer.wms(
url,
L.Util.extend(josmOptions, {
layers: props.layers,
format: props.format
})
);
} else {
layer = L.tileLayer(url, josmOptions);
}
}
// Layer attribution here only as short link to original site,
// to keep current position use placeholders: {zoom}/{lat}/{lon}
// Copyright attribution in index.html #credits
var getAttribution = function() {
return this.options.mapLink;
};
layer.getAttribution = getAttribution;
layer.id = props.id;
return layer;
}
});
BR.layersConfig = function(map) {
return new BR.LayersConfig(map);
};

188
js/Map.js
View file

@ -1,94 +1,74 @@
BR.Map = {
initMap: function() {
var map,
layersControl;
var map, layersControl;
BR.keys = BR.keys || {};
var maxZoom = 19;
var osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: maxZoom
});
var osmde = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', {
maxNativeZoom: 18,
maxZoom: maxZoom
});
var topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxNativeZoom: 17,
maxZoom: maxZoom
});
var thunderforestAttribution = 'tiles &copy; <a target="_blank" href="https://www.thunderforest.com">Thunderforest</a> '
+ '(<a target="_blank" href="https://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA 2.0</a>)';
var thunderforestAuth = BR.keys.thunderforest ? '?apikey=' + BR.keys.thunderforest : '';
var cycle = L.tileLayer('https://{s}.tile.thunderforest.com/cycle/{z}/{x}/{y}.png' + thunderforestAuth, {
maxNativeZoom: 18,
maxZoom: maxZoom
});
var outdoors = L.tileLayer('https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png' + thunderforestAuth, {
maxNativeZoom: 18,
maxZoom: maxZoom
});
var esri = L.tileLayer('https://{s}.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxNativeZoom: 19,
maxZoom: maxZoom,
subdomains: ['server', 'services'],
attribution: '<a target="_blank" href="http://goto.arcgisonline.com/maps/World_Imagery">World Imagery</a> '
+ '&copy; <a target="_blank" href="https://www.esri.com/">Esri</a>, sources: '
+ 'Esri, DigitalGlobe, Earthstar Geographics, CNES/Airbus DS, GeoEye, USDA FSA, USGS, Getmapping, Aerogrid, IGN, IGP, and the GIS User Community'
});
var cycling = L.tileLayer('https://tile.waymarkedtrails.org/cycling/{z}/{x}/{y}.png', {
maxNativeZoom: 18,
opacity: 0.7,
maxZoom: maxZoom
});
var hiking = L.tileLayer('https://tile.waymarkedtrails.org/hiking/{z}/{x}/{y}.png', {
maxNativeZoom: 18,
opacity: 0.7,
maxZoom: maxZoom
});
map = new L.Map('map', {
worldCopyJump: true
zoomControl: false, // add it manually so that we can translate it
worldCopyJump: true,
minZoom: 0,
maxZoom: maxZoom
});
L.control
.zoom({
zoomInTitle: i18next.t('map.zoomInTitle'),
zoomOutTitle: i18next.t('map.zoomOutTitle')
})
.addTo(map);
if (!map.restoreView()) {
map.setView([50.99, 9.86], 6);
map.setView([50.99, 9.86], 5);
}
// two attribution lines by adding two controls, prevents ugly wrapping on
// small screens, better separates static from layer-specific attribution
var osmAttribution =
$(map.getContainer()).outerWidth() >= 400
? i18next.t('map.attribution-osm-long')
: i18next.t('map.attribution-osm-short');
map.attributionControl.setPrefix(
'&copy; <a target="_blank" href="https://www.openstreetmap.org/copyright">' +
osmAttribution +
'</a>' +
' &middot; <a href="" data-toggle="modal" data-target="#credits">' +
i18next.t('map.copyright') +
'</a>' +
' &middot; <a target="_blank" href="http://brouter.de/privacypolicy.html">' +
i18next.t('map.privacy') +
'</a>'
);
$('#credits').on('show.bs.modal', function(event) {
BR.Map._renderLayerCredits(layersControl._layers);
});
new L.Control.PermalinkAttribution().addTo(map);
map.attributionControl.setPrefix(false);
map.attributionControl.addAttribution('<a href="" data-toggle="modal" data-target="#credits">Copyright & credits</a>')
var baseLayers = {
'OpenStreetMap': osm,
'OpenStreetMap.de': osmde,
'OpenTopoMap': topo,
'OpenCycleMap (Thunderf.)': cycle,
'Outdoors (Thunderforest)': outdoors,
'Esri World Imagery': esri
};
var overlays = {
'Cycling (Waymarked Trails)': cycling,
'Hiking (Waymarked Trails)': hiking
};
var layersConfig = BR.layersConfig(map);
var baseLayers = layersConfig.getBaseLayers();
var overlays = layersConfig.getOverlays();
if (BR.keys.bing) {
baseLayers['Bing Aerial'] = new BR.BingLayer(BR.keys.bing);
baseLayers[i18next.t('map.layer.bing')] = new BR.BingLayer(
BR.keys.bing
);
}
if (BR.keys.digitalGlobe) {
var recent = new L.tileLayer('https://{s}.tiles.mapbox.com/v4/digitalglobe.nal0g75k/{z}/{x}/{y}.png?access_token=' + BR.keys.digitalGlobe, {
minZoom: 1,
maxZoom: 19,
attribution: '&copy; <a href="https://www.digitalglobe.com/platforms/mapsapi">DigitalGlobe</a> ('
+ '<a href="https://bit.ly/mapsapiview">Terms of Use</a>)'
});
baseLayers['DigitalGlobe Recent Imagery'] = recent;
var recent = new L.tileLayer(
'https://{s}.tiles.mapbox.com/v4/digitalglobe.nal0g75k/{z}/{x}/{y}.png?access_token=' +
BR.keys.digitalGlobe,
{
minZoom: 1,
maxZoom: 19,
attribution:
'&copy; <a href="https://www.digitalglobe.com/platforms/mapsapi">DigitalGlobe</a> (<a href="https://bit.ly/mapsapiview">Terms of Use</a>)'
}
);
baseLayers[i18next.t('map.layer.digitalglobe')] = recent;
}
if (BR.conf.clearBaseLayers) {
@ -105,19 +85,27 @@ BR.Map = {
overlays[i] = L.tileLayer(BR.conf.overlays[i]);
}
}
// after applying custom base layer configurations, add first base layer to map
var firstLayer = baseLayers[Object.keys(baseLayers)[0]];
if (firstLayer) {
map.addLayer(firstLayer);
layersControl = BR.layersTab(layersConfig, baseLayers, overlays).addTo(
map
);
var secureContext =
'isSecureContext' in window
? isSecureContext
: location.protocol === 'https:';
if (secureContext) {
L.control
.locate({
strings: {
title: i18next.t('map.locate-me')
},
icon: 'fa fa-location-arrow',
iconLoading: 'fa fa-spinner fa-pulse'
})
.addTo(map);
}
layersControl = L.control.layers(baseLayers, overlays).addTo(map);
L.control.locate({
icon: 'fa fa-location-arrow',
iconLoading: 'fa fa-spinner fa-pulse',
}).addTo(map);
L.control.scale().addTo(map);
new BR.Layers().init(map, layersControl, baseLayers, overlays);
@ -126,15 +114,31 @@ BR.Map = {
BR.debug = BR.debug || {};
BR.debug.map = map;
var layersAndOverlays = baseLayers;
for (var o in overlays) {
layersAndOverlays[o] = overlays[o];
}
return {
map: map,
layersControl: layersControl,
layers: layersAndOverlays
layersControl: layersControl
};
}
},
};
_renderLayerCredits: function(layers) {
var dl = document.getElementById('credits-maps');
var i, obj, dt, dd, attribution;
L.DomUtil.empty(dl);
for (i = 0; i < layers.length; i++) {
obj = layers[i];
attribution = obj.layer.options.attribution;
if (attribution) {
dt = document.createElement('dt');
dt.innerHTML = obj.name;
dd = document.createElement('dd');
dd.innerHTML = obj.layer.options.attribution;
dl.appendChild(dt);
dl.appendChild(dd);
}
}
}
};

View file

@ -1,5 +1,4 @@
BR.Util = {
get: function(url, cb) {
var xhr = new XMLHttpRequest();
@ -16,17 +15,17 @@ BR.Util = {
};
try {
xhr.send();
} catch(e) {
} catch (e) {
cb(e);
}
},
getError: function(xhr) {
var msg = 'no response from server';
var msg = i18next.t('warning.no-response');
if (xhr.responseText) {
msg = xhr.responseText;
msg = xhr.responseText;
} else if (xhr.status || xhr.statusText) {
msg = xhr.status + ': ' + xhr.statusText;
msg = xhr.status + ': ' + xhr.statusText;
}
return new Error(msg);
},
@ -46,9 +45,8 @@ BR.Util = {
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch(e) {
} catch (e) {
return false;
}
}
};
};

View file

@ -0,0 +1,99 @@
BR.ControlLayers = L.Control.Layers.extend({
getActiveLayers: function() {
var result = [];
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
if (this._map.hasLayer(obj.layer)) {
if (obj.overlay) {
result.push(obj);
} else {
result.unshift(obj);
}
}
}
return result;
},
getActiveBaseLayer: function() {
var activeLayers = this.getActiveLayers();
for (var i = 0; i < activeLayers.length; i++) {
var obj = activeLayers[i];
if (!obj.overlay) {
return obj;
}
}
return null;
},
removeActiveLayers: function() {
var removed = [];
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
if (this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
removed.push(obj);
}
}
return removed;
},
getLayer: function(name) {
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
if (obj.name === name) {
return obj;
}
}
return null;
},
getBaseLayers: function() {
return this._layers.filter(function(obj) {
return !obj.overlay;
});
},
activateLayer: function(obj) {
if (!this._map.hasLayer(obj.layer)) {
this._map.addLayer(obj.layer);
}
},
activateFirstLayer: function() {
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
if (!obj.overlay) {
this.activateLayer(obj);
break;
}
}
},
activateBaseLayerIndex: function(index) {
var baseLayers = this.getBaseLayers();
var obj = baseLayers[index];
this.activateLayer(obj);
},
_addLayer: function(layer, name, overlay) {
L.Control.Layers.prototype._addLayer.call(this, layer, name, overlay);
// override z-index assignment to fix that base layers added later
// are on top of overlays; set all base layers to 0
if (this.options.autoZIndex && layer.setZIndex) {
if (!overlay) {
// undo increase in super method
this._lastZIndex--;
layer.setZIndex(0);
}
}
}
});

View file

@ -1,39 +0,0 @@
BR.Control = L.Control.extend({
options: {
position: 'bottomleft'
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'info'),
heading,
div;
if (this.options.heading) {
heading = L.DomUtil.create('div', 'heading', container);
heading.innerHTML = this.options.heading;
this._content = L.DomUtil.create('div', 'content', container);
} else {
this._content = container;
}
if (this.options.divId) {
div = L.DomUtil.get(this.options.divId);
L.DomUtil.removeClass(div, 'hidden');
this._content.appendChild(div);
}
var stop = L.DomEvent.stopPropagation;
L.DomEvent
.on(container, 'click', stop)
.on(container, 'mousedown', stop)
.on(container, 'dblclick', stop)
.on(container, 'mousewheel', stop)
.on(container, 'MozMousePixelScroll', stop);
// disabled because links not working, remove?
//L.DomEvent.on(container, 'click', L.DomEvent.preventDefault);
return container;
}
});

View file

@ -1,16 +0,0 @@
BR.Download = L.Class.extend({
update: function (urls) {
if (urls) {
['gpx', 'kml', 'geojson', 'csv'].forEach(function(e, i, a) {
var a = L.DomUtil.get('dl-'+e);
a.setAttribute('href', urls[e]);
a.setAttribute('download', 'brouter.'+e);
a.removeAttribute('disabled');
})
}
}
});
BR.download = function() {
return new BR.Download();
};

167
js/control/Export.js Normal file
View file

@ -0,0 +1,167 @@
BR.Export = L.Class.extend({
latLngs: [],
initialize: function(router) {
this.router = router;
this.exportButton = $('#exportButton');
var trackname = (this.trackname = document.getElementById('trackname'));
this.tracknameAllowedChars = BR.conf.tracknameAllowedChars;
if (this.tracknameAllowedChars) {
this.tracknameMessage = document.getElementById(
'trackname-message'
);
var patternRegex = new RegExp(
'[' + this.tracknameAllowedChars + ']+'
);
// warn about special characters getting removed by server quick fix (#194)
trackname.pattern = patternRegex.toString().slice(1, -1);
trackname.addEventListener(
'input',
L.bind(this._validationMessage, this)
);
}
this.exportButton.on('click', L.bind(this._generateTrackname, this));
L.DomUtil.get('submitExport').onclick = L.bind(this._export, this);
this.update([]);
},
update: function(latLngs) {
this.latLngs = latLngs;
if (latLngs.length < 2) {
this.exportButton.addClass('disabled');
} else {
this.exportButton.removeClass('disabled');
}
},
_export: function() {
var exportForm = document.forms['export'];
var format =
exportForm['format'].value ||
$('#export-format input:radio:checked').val();
var name = encodeURIComponent(exportForm['trackname'].value);
var includeWaypoints = exportForm['include-waypoints'].checked;
var uri = this.router.getUrl(
this.latLngs,
format,
name,
includeWaypoints
);
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent(
'click',
true,
true,
window,
0,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
var link = document.createElement('a');
link.href = uri;
link.dispatchEvent(evt);
},
_validationMessage: function() {
var trackname = this.trackname;
var replaceRegex = new RegExp(
'[^' + this.tracknameAllowedChars + ']',
'g'
);
if (trackname.validity.patternMismatch) {
var replaced = trackname.value.replace(replaceRegex, '');
var patternStr = this.tracknameAllowedChars.replace(/\\/g, '');
this.tracknameMessage.textContent =
'[' + patternStr + '] --> ' + replaced;
} else {
this.tracknameMessage.textContent = '';
}
},
_generateTrackname: function() {
var trackname = this.trackname;
this._getCityAtPosition(
this.latLngs[0],
L.bind(function(from) {
this._getCityAtPosition(
this.latLngs[this.latLngs.length - 1],
L.bind(function(to) {
var distance = document.getElementById('distance')
.innerHTML;
if (this.tracknameAllowedChars) {
distance = distance.replace(',', '.'); // temp. fix (#202)
}
if (!from || !to) {
trackname.value = null;
} else if (from === to) {
trackname.value = i18next.t('export.route-loop', {
from: from,
distance: distance
});
} else {
trackname.value = i18next.t(
'export.route-from-to',
{ from: from, to: to, distance: distance }
);
}
if (this.tracknameAllowedChars) {
// temp. fix: replace and remove characters that will get removed by server quick fix (#194)
trackname.value = trackname.value
.replace(/[>)]/g, '')
.replace(/ \(/g, ' - ');
this._validationMessage();
}
}, this)
);
}, this)
);
},
_getCityAtPosition: function(lonlat, cb) {
var url = L.Util.template(
'https://nominatim.openstreetmap.org/reverse?lon={lng}&lat={lat}&format=json',
lonlat
);
BR.Util.get(
url,
L.bind(function(err, response) {
try {
var addr = JSON.parse(response).address;
cb(
addr.village ||
addr.town ||
addr.hamlet ||
addr.city_district ||
addr.city
);
} catch (err) {
BR.message.showError(
'Error getting position city "' + lonlat + '": ' + err
);
return cb(null);
}
})
);
}
});
BR.export = function() {
return new BR.Export();
};

View file

@ -1,24 +1,20 @@
BR.Itinerary = L.Class.extend({
options: {
heading: 'Itinerary'
},
onAdd: function (map) {
initialize: function() {
this._content = document.getElementById('itinerary');
L.DomUtil.removeClass(this._content.parentElement, 'hidden');
this.update();
},
update: function (polyline, segments) {
var i, j, iter, html = '';
update: function(polyline, segments) {
var i,
j,
iter,
html = '';
html += '<pre>';
for (i = 0; segments && i < segments.length; i++)
{
html += '<pre class="flexgrow">';
for (i = 0; segments && i < segments.length; i++) {
iter = segments[i].feature.iternity;
for (j = 0; iter && j < iter.length; j++)
{
html += iter[j] + '\n';
for (j = 0; iter && j < iter.length; j++) {
html += iter[j] + '\n';
}
}
html += '</pre>';

View file

@ -1,10 +1,9 @@
BR.Layers = L.Class.extend({
_loadLayers: function() {
this._customLayers = {};
if (BR.Util.localStorageAvailable()) {
var layers = JSON.parse(localStorage.getItem("map/customLayers"));
var layers = JSON.parse(localStorage.getItem('map/customLayers'));
for (a in layers) {
this._addLayer(a, layers[a].layer, layers[a].isOverlay);
}
@ -14,21 +13,28 @@ BR.Layers = L.Class.extend({
_loadTable: function() {
var layersData = [];
for (layer in this._customLayers) {
layersData.push([layer, this._customLayers[layer].layer._url, this._customLayers[layer].isOverlay ? "Overlay" : "Layer"]);
layersData.push([
layer,
this._customLayers[layer].layer._url,
this._customLayers[layer].isOverlay ? 'Overlay' : 'Layer'
]);
}
if (this._layersTable != null) {
this._layersTable.destroy();
}
this._layersTable = $('#custom_layers_table').DataTable({
this._layersTable = $('#custom_layers_table').DataTable({
data: layersData,
info: false,
searching: false,
paging: false,
language: {
emptyTable: i18next.t('sidebar.layers.table.empty')
},
columns: [
{ title: "Name" },
{ title: "URL" },
{ title: "Type" }
{ title: i18next.t('sidebar.layers.table.name') },
{ title: i18next.t('sidebar.layers.table.URL') },
{ title: i18next.t('sidebar.layers.table.type') }
]
});
},
@ -36,22 +42,29 @@ BR.Layers = L.Class.extend({
init: function(map, layersControl, baseLayers, overlays) {
this._layersControl = layersControl;
this._map = map;
this._layers = {}
for (var l in overlays)
this._layers[l] = [overlays[l], true];
for (var l in baseLayers)
this._layers[l] = [baseLayers[l], false];
this._layers = {};
for (var l in overlays) this._layers[l] = [overlays[l], true];
for (var l in baseLayers) this._layers[l] = [baseLayers[l], false];
L.DomUtil.get('custom_layers_add_base').onclick = L.bind(this._addBaseLayer, this);
L.DomUtil.get('custom_layers_add_overlay').onclick = L.bind(this._addOverlay, this);
L.DomUtil.get('custom_layers_remove').onclick = L.bind(this._remove, this);
L.DomUtil.get('custom_layers_add_base').onclick = L.bind(
this._addBaseLayer,
this
);
L.DomUtil.get('custom_layers_add_overlay').onclick = L.bind(
this._addOverlay,
this
);
L.DomUtil.get('custom_layers_remove').onclick = L.bind(
this._remove,
this
);
this._loadLayers();
this._loadTable();
var table = this._layersTable;
$('#custom_layers_table tbody').on( 'click', 'tr', function () {
if ( $(this).hasClass('selected') ) {
$('#custom_layers_table tbody').on('click', 'tr', function() {
if ($(this).hasClass('selected')) {
$(this).removeClass('selected');
} else {
table.$('tr.selected').removeClass('selected');
@ -59,16 +72,9 @@ BR.Layers = L.Class.extend({
}
});
addLayer = L.easyButton(
'fa-plus-square',
function () {
$('#custom_layers').modal();
},
'Add or remove custom layers',
{
position: 'topright'
}
).addTo(map);
L.DomUtil.get('custom_layers_button').onclick = function() {
$('#custom_layers').modal();
};
},
_remove: function(evt) {
@ -78,7 +84,10 @@ BR.Layers = L.Class.extend({
this._layersControl.removeLayer(this._customLayers[name].layer);
this._map.removeLayer(this._customLayers[name].layer);
delete this._customLayers[name];
this._layersTable.row('.selected').remove().draw( false );
this._layersTable
.row('.selected')
.remove()
.draw(false);
this._sync();
}
},
@ -98,16 +107,17 @@ BR.Layers = L.Class.extend({
},
_addLayer: function(layerName, layerUrl, isOverlay) {
if (layerName in this._layers)
return
if (layerName in this._layers) return;
if (layerName in this._customLayers)
return
if (layerName in this._customLayers) return;
try {
var layer = L.tileLayer(layerUrl);
this._customLayers[layerName] = {layer: layer, isOverlay: isOverlay};
this._customLayers[layerName] = {
layer: layer,
isOverlay: isOverlay
};
if (isOverlay) {
this._layersControl.addOverlay(layer, layerName);
@ -118,17 +128,20 @@ BR.Layers = L.Class.extend({
this._sync();
return layer;
} catch (e) {
console.warn("Oops:", e);
return
console.warn('Oops:', e);
return;
}
},
_sync: function() {
if (BR.Util.localStorageAvailable()) {
localStorage.setItem("map/customLayers", JSON.stringify(this._customLayers, function(k, v) {
// dont write Leaflet.Layer in localStorage; simply keep the URL
return v._url || v;
}));
localStorage.setItem(
'map/customLayers',
JSON.stringify(this._customLayers, function(k, v) {
// dont write Leaflet.Layer in localStorage; simply keep the URL
return v._url || v;
})
);
}
}
});
});

484
js/control/LayersTab.js Normal file
View file

@ -0,0 +1,484 @@
BR.LayersTab = BR.ControlLayers.extend({
previewLayer: null,
previewBounds: null,
saveLayers: [],
initialize: function(layersConfig, baseLayers, overlays, options) {
L.Control.Layers.prototype.initialize.call(
this,
baseLayers,
overlays,
options
);
this.layersConfig = layersConfig;
},
addTo: function(map) {
this._map = map;
this.onAdd(map);
L.DomUtil.get('layers-control-wrapper').appendChild(this._section);
this.initOpacitySlider(map);
this.initButtons();
this.initJsTree();
return this;
},
onAdd: function(map) {
BR.ControlLayers.prototype.onAdd.call(this, map);
map.on(
'baselayerchange overlayadd overlayremove',
this.storeActiveLayers,
this
);
},
onRemove: function(map) {
BR.ControlLayers.prototype.onRemove.call(this, map);
map.off(
'baselayerchange overlayadd overlayremove',
this.storeActiveLayers,
this
);
},
initOpacitySlider: function(map) {
var self = this;
var overlayOpacitySlider = new BR.OpacitySlider({
id: 'overlay',
reversed: false,
orientation: 'horizontal',
defaultValue: 1,
title: i18next.t('layers.opacity-slider'),
callback: function(opacity) {
for (var i = 0; i < self._layers.length; i++) {
if (
!self._layers[i].overlay ||
!map.hasLayer(self._layers[i].layer)
) {
continue;
}
self._layers[i].layer.setOpacity(opacity);
}
}
});
L.DomUtil.get(
'leaflet-control-layers-overlays-opacity-slider'
).appendChild(overlayOpacitySlider.getElement());
},
initButtons: function() {
var expandTree = function(e) {
this.jstree.open_all();
};
var collapseTree = function(e) {
this.jstree.close_all();
};
var toggleOptionalLayers = function(e) {
var button = L.DomUtil.get('optional_layers_button');
var treeButtons = L.DomUtil.get('tree-button-group');
var div = L.DomUtil.get('optional-layers-tree');
div.hidden = !div.hidden;
treeButtons.hidden = !treeButtons.hidden;
button.classList.toggle('active');
if (div.hidden) {
this.deselectNode();
}
};
L.DomUtil.get('expand_tree_button').onclick = L.bind(expandTree, this);
L.DomUtil.get('collapse_tree_button').onclick = L.bind(
collapseTree,
this
);
L.DomUtil.get('optional_layers_button').onclick = L.bind(
toggleOptionalLayers,
this
);
},
initJsTree: function() {
var layerIndex = BR.layerIndex;
var treeData = this.toJsTree(BR.confLayers.tree);
var oldSelected = null;
var onSelectNode = function(e, data) {
var layerData = layerIndex[data.node.id];
var selected = data.selected[0];
if (selected !== oldSelected) {
this.showPreview(layerData);
oldSelected = selected;
} else {
data.instance.deselect_node(data.node);
}
};
var onDeselectNode = function(e, data) {
this.hidePreview();
oldSelected = null;
};
var onCheckNode = function(e, data) {
var layerData = layerIndex[data.node.id];
var layer = this.createLayer(layerData);
var name = layerData.properties.name;
var overlay = layerData.properties.overlay;
if (overlay) {
this.addOverlay(layer, name);
} else {
this.addBaseLayer(layer, name);
}
this.storeDefaultLayers();
};
var onUncheckNode = function(e, data) {
var obj = this.getLayerById(data.node.id);
if (!obj) return;
this.removeLayer(obj.layer);
if (this._map.hasLayer(obj.layer)) {
this._map.removeLayer(obj.layer);
if (!obj.overlay) {
this.activateFirstLayer();
}
}
this.storeDefaultLayers();
};
$('#optional-layers-tree')
.on('select_node.jstree', L.bind(onSelectNode, this))
.on('deselect_node.jstree', L.bind(onDeselectNode, this))
.on('check_node.jstree', L.bind(onCheckNode, this))
.on('uncheck_node.jstree', L.bind(onUncheckNode, this))
.on('ready.jstree', function(e, data) {
data.instance.open_all();
})
.jstree({
plugins: ['checkbox'],
checkbox: {
whole_node: false,
tie_selection: false
},
core: {
multiple: false,
themes: {
icons: false,
dots: false
},
data: treeData
}
});
this.jstree = $('#optional-layers-tree').jstree(true);
},
toJsTree: function(layerTree) {
var data = {
children: []
};
var self = this;
function createRootNode(name) {
var rootNode = {
text: i18next.t('sidebar.layers.category.' + name, name),
state: {
disabled: true
},
children: []
};
return rootNode;
}
function getText(props, parent) {
var text = '';
var code = props.country_code || props.language_code;
if (code && parent.text !== code) {
text += '<span class="tree-code">' + code + '</span>';
}
text += props.name;
return text;
}
function createNode(id, layerData, parent) {
var props = layerData.properties;
var url = props.url;
var keyObj = self.layersConfig.getKeyName(url);
var childNode = null;
// when key required only add if configured
if (!keyObj || (keyObj && BR.keys[keyObj.name])) {
childNode = {
id: id,
text: getText(props, parent),
state: {
checked: self.layersConfig.isDefaultLayer(
id,
props.overlay
)
}
};
}
return childNode;
}
function walkTree(inTree, outTree) {
function walkObject(obj) {
for (name in obj) {
var value = obj[name];
var rootNode = createRootNode(name);
outTree.children.push(rootNode);
walkTree(value, rootNode);
}
}
if (Array.isArray(inTree)) {
for (var i = 0; i < inTree.length; i++) {
var entry = inTree[i];
if (typeof entry === 'object') {
walkObject(entry);
} else {
var layer = BR.layerIndex[entry];
if (layer) {
var childNode = createNode(entry, layer, outTree);
if (childNode) {
outTree.children.push(childNode);
}
} else {
console.error('Layer "' + entry + '" not found');
}
}
}
} else {
walkObject(inTree);
}
}
walkTree(layerTree, data);
return data.children;
},
storeDefaultLayers: function() {
var baseLayers = [];
var overlays = [];
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
// id set in LayersConfig.createLayer
var id = obj.layer.id;
if (id) {
if (obj.overlay) {
overlays.push(id);
} else {
baseLayers.push(id);
}
}
}
this.layersConfig.storeDefaultLayers(baseLayers, overlays);
},
createLayer: function(layerData) {
var layer = this.layersConfig.createLayer(layerData);
var overlay = layerData.properties.overlay;
// preview z-index, like in BR.ControlLayers._addLayer
layer.options.zIndex = overlay ? this._lastZIndex + 1 : 0;
return layer;
},
getLayerById: function(id) {
for (var i = 0; i < this._layers.length; i++) {
var obj = this._layers[i];
if (obj.layer.id === id) {
return obj;
}
}
return null;
},
getLayerByLegacyName: function(legacyName) {
var obj = null;
var id = this.layersConfig.legacyNameToIdMap[legacyName];
if (id) {
obj = this.getLayerById(id);
}
return obj;
},
activateDefaultBaseLayer: function() {
var index = BR.conf.defaultBaseLayerIndex || 0;
var activeBaseLayer = this.getActiveBaseLayer();
if (!activeBaseLayer) {
this.activateBaseLayerIndex(index);
}
},
saveRemoveActiveLayers: function() {
this.saveLayers = this.removeActiveLayers();
},
restoreActiveLayers: function(overlaysOnly) {
for (var i = 0; i < this.saveLayers.length; i++) {
var obj = this.saveLayers[i];
if (!overlaysOnly || (overlaysOnly && obj.overlay)) {
var hasLayer = !!this._getLayer(L.Util.stamp(obj.layer));
if (hasLayer) {
this.activateLayer(obj);
} else if (!obj.overlay) {
// saved base layer has been removed during preview, select first
this.activateFirstLayer();
}
}
}
this.saveLayers = [];
},
removePreviewLayer: function() {
if (this.previewLayer && this._map.hasLayer(this.previewLayer)) {
this._map.removeLayer(this.previewLayer);
this.previewLayer = null;
return true;
}
return false;
},
showPreviewBounds: function(layerData) {
if (layerData.geometry) {
this.previewBounds = L.geoJson(layerData.geometry, {
// fill/mask outside of bounds polygon with Leaflet.snogylop
invert: true,
// reduce unmasked areas appearing due to clipping while panning and zooming out
renderer: L.svg({ padding: 1 }),
color: '#333',
fillOpacity: 0.4,
weight: 2
}).addTo(this._map);
}
},
removePreviewBounds: function() {
if (this.previewBounds && this._map.hasLayer(this.previewBounds)) {
this._map.removeLayer(this.previewBounds);
this.previewBounds = null;
}
},
deselectNode: function() {
var selected = this.jstree.get_selected();
if (selected.length > 0) {
this.jstree.deselect_node(selected[0]);
}
},
onBaselayerchange: function() {
// execute after current input click handler,
// otherwise added overlay checkbox state doesn't update
setTimeout(
L.Util.bind(function() {
this.removePreviewBounds();
this.removePreviewLayer();
this.restoreActiveLayers(true);
this.deselectNode();
}, this),
0
);
},
showPreview: function(layerData) {
var layer = this.createLayer(layerData);
this._map.addLayer(layer);
this.removePreviewBounds();
if (!this.removePreviewLayer()) {
this.saveRemoveActiveLayers();
this._map.once('baselayerchange', this.onBaselayerchange, this);
}
this.previewLayer = layer;
this.showPreviewBounds(layerData);
},
hidePreview: function(layer) {
this._map.off('baselayerchange', this.onBaselayerchange, this);
this.removePreviewBounds();
this.removePreviewLayer();
this.restoreActiveLayers();
},
toLayerString: function(obj) {
return obj.layer.id ? obj.layer.id : obj.name;
},
getLayerFromString: function(layerString) {
var obj = this.getLayerById(layerString);
if (!obj) {
// fallback to name for custom and config layers
obj = this.getLayer(layerString);
if (!obj) {
// legacy layer name support
obj = this.getLayerByLegacyName(layerString);
}
}
return obj;
},
storeActiveLayers: function() {
if (BR.Util.localStorageAvailable()) {
var objList = this.getActiveLayers();
var idList = objList.map(
L.bind(function(obj) {
return this.toLayerString(obj);
}, this)
);
var str = JSON.stringify(idList);
localStorage.setItem('map/activeLayers', str);
}
},
loadActiveLayers: function() {
if (BR.Util.localStorageAvailable()) {
var item = localStorage.getItem('map/activeLayers');
if (item) {
var idList = JSON.parse(item);
for (var i = 0; i < idList.length; i++) {
var id = idList[i];
var obj = this.getLayerFromString(id);
if (obj) {
this.activateLayer(obj);
}
}
}
}
}
});
BR.layersTab = function(baseLayers, overlays, options) {
return new BR.LayersTab(baseLayers, overlays, options);
};

View file

@ -4,40 +4,52 @@ BR.Message = L.Class.extend({
// Bootstrap data-api's auto-initialization doesn't work in Controls because of stopPropagation
alert: false
},
initialize: function (id, options) {
initialize: function(id, options) {
L.setOptions(this, options);
this.id = id;
},
_show: function (msg, type) {
_show: function(msg, type) {
var ele = L.DomUtil.get(this.id),
iconClass = (type === 'warning') ? 'fa-exclamation-triangle' : 'fa-times-circle',
alertClass = (type === 'warning') ? 'alert-warning' : 'alert-danger';
iconClass =
type === 'warning'
? 'fa-exclamation-triangle'
: 'fa-times-circle',
alertClass = type === 'warning' ? 'alert-warning' : 'alert-danger';
L.DomEvent.disableClickPropagation(ele);
ele.innerHTML =
'<div class="alert ' + alertClass + ' alert-dismissible fade in" role="alert">'
+ ' <button type="button" class="close" data-dismiss="alert" aria-label="Close">'
+ ' <span aria-hidden="true">&times;</span>'
+ ' </button>'
+ ' <span class="fa ' + iconClass + '" aria-hidden="true"/></span>'
+ msg
+ '</div>';
'<div class="alert ' +
alertClass +
' alert-dismissible fade show" role="alert">' +
' <button type="button" class="close" data-dismiss="alert" aria-label="Close">' +
' <span aria-hidden="true">&times;</span>' +
' </button>' +
' <span class="fa ' +
iconClass +
'" aria-hidden="true"/></span>' +
msg +
'</div>';
if (this.options.alert) {
$('#' + this.id + ' .alert').alert();
}
},
hide: function () {
hide: function() {
$('#' + this.id + ' .alert').alert('close');
},
showError: function (err) {
showError: function(err) {
if (err == 'Error: target island detected for section 0\n') {
err = i18next.t('warning.no-route-found');
}
this._show(err, 'error');
},
showWarning: function (msg) {
showWarning: function(msg) {
this._show(msg, 'warning');
}
});

View file

@ -1,67 +1,58 @@
BR.OpacitySlider = L.Control.extend({
options: {
position: 'topleft',
BR.OpacitySlider = L.Class.extend({
options: {
id: '',
reversed: true,
orientation: 'vertical',
defaultValue: BR.conf.defaultOpacity,
title: '',
callback: function(opacity) {}
},
onAdd: function (map) {
var container = L.DomUtil.create('div', 'leaflet-bar control-slider'),
input = $('<input id="slider" type="text"/>'),
item = BR.Util.localStorageAvailable() ? localStorage.opacitySliderValue : null,
value = item ? parseInt(item) : BR.conf.defaultOpacity * 100,
initialize: function(options) {
L.setOptions(this, options);
var input = (this.input = $(
'<input id="slider-' + this.options.id + '" type="text"/>'
)),
item = BR.Util.localStorageAvailable()
? localStorage['opacitySliderValue' + this.options.id]
: null,
value = item ? parseInt(item) : this.options.defaultValue * 100,
minOpacity = (BR.conf.minOpacity || 0) * 100;
if (value < minOpacity) {
value = minOpacity;
}
// prevent also dragging map in Chrome
L.DomEvent.disableClickPropagation(container);
input
.slider({
id: this.options.id,
min: 0,
max: 100,
step: 1,
value: value,
orientation: this.options.orientation,
reversed: this.options.reversed,
selection: this.options.reversed ? 'before' : 'after', // inverted, serves as track style, see css
tooltip: 'hide'
})
.on('slide slideStop', { self: this }, function(evt) {
evt.data.self.options.callback(evt.value / 100);
})
.on('slideStop', { self: this }, function(evt) {
if (BR.Util.localStorageAvailable()) {
localStorage[
'opacitySliderValue' + evt.data.self.options.id
] = evt.value;
}
});
var stopClickAfterSlide = function(evt) {
L.DomEvent.stop(evt);
removeStopClickListeners();
};
var removeStopClickListeners = function() {
document.removeEventListener('click', stopClickAfterSlide, true);
document.removeEventListener('mousedown', removeStopClickListeners, true);
};
$(container).html(input);
$(container).attr('title', 'Set transparency of route track and markers');
input.slider({
min: 0,
max: 100,
step: 1,
value: value,
orientation: 'vertical',
reversed : true,
selection: 'before', // inverted, serves as track style, see css
tooltip: 'hide'
}).on('slideStart', function (evt) {
// dragging beyond slider control selects zoom control +/- text in Firefox
L.DomUtil.disableTextSelection();
}).on('slide slideStop', { self: this }, function (evt) {
evt.data.self.options.callback(evt.value / 100);
}).on('slideStop', function (evt) {
if (BR.Util.localStorageAvailable()) {
localStorage.opacitySliderValue = evt.value;
}
L.DomUtil.enableTextSelection();
// When dragging outside slider and over map, click event after mouseup
// adds marker when active on Chromium. So disable click (not needed)
// once after sliding.
document.addEventListener('click', stopClickAfterSlide, true);
// Firefox does not fire click event in this case, so make sure stop listener
// is always removed on next mousedown.
document.addEventListener('mousedown', removeStopClickListeners, true);
});
this.getElement().title = this.options.title;
this.options.callback(value / 100);
},
return container;
getElement: function() {
return this.input.slider('getElement');
}
});

View file

@ -0,0 +1,64 @@
BR.OpacitySliderControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function(map) {
var container = L.DomUtil.create('div', 'leaflet-bar control-slider');
// prevent also dragging map in Chrome
L.DomEvent.disableClickPropagation(container);
// migrate legacy value
if (BR.Util.localStorageAvailable()) {
var value = localStorage.getItem('opacitySliderValue');
if (value !== null) {
localStorage.setItem(
'opacitySliderValue' + this.options.id,
value
);
localStorage.removeItem('opacitySliderValue');
}
}
var slider = new BR.OpacitySlider(this.options);
container.appendChild(slider.getElement());
var stopClickAfterSlide = function(evt) {
L.DomEvent.stop(evt);
removeStopClickListeners();
};
var removeStopClickListeners = function() {
document.removeEventListener('click', stopClickAfterSlide, true);
document.removeEventListener(
'mousedown',
removeStopClickListeners,
true
);
};
slider.input
.on('slideStart', function(evt) {
// dragging beyond slider control selects zoom control +/- text in Firefox
L.DomUtil.disableTextSelection();
})
.on('slideStop', { self: this }, function(evt) {
L.DomUtil.enableTextSelection();
// When dragging outside slider and over map, click event after mouseup
// adds marker when active on Chromium. So disable click (not needed)
// once after sliding.
document.addEventListener('click', stopClickAfterSlide, true);
// Firefox does not fire click event in this case, so make sure stop listener
// is always removed on next mousedown.
document.addEventListener(
'mousedown',
removeStopClickListeners,
true
);
});
return container;
}
});

View file

@ -1,11 +1,15 @@
BR.Profile = L.Class.extend({
BR.Profile = L.Evented.extend({
cache: {},
initialize: function () {
initialize: function() {
var textArea = L.DomUtil.get('profile_upload');
this.editor = CodeMirror.fromTextArea(textArea, {
lineNumbers: true
});
L.DomUtil.get('upload').onclick = L.bind(this._upload, this);
L.DomUtil.get('clear').onclick = L.bind(this.clear, this);
this.ele = L.DomUtil.get('profile_upload');
autosize(this.ele);
this.message = new BR.Message('profile_message', {
alert: true
});
@ -15,9 +19,7 @@ BR.Profile = L.Class.extend({
var button = evt.target || evt.srcElement;
evt.preventDefault();
this.ele.value = null;
this.ele.defaultValue = null;
autosize.update(this.ele);
this._setValue('');
this.fire('clear');
button.blur();
@ -26,39 +28,54 @@ BR.Profile = L.Class.extend({
update: function(options) {
var profileName = options.profile,
profileUrl,
ele = this.ele,
dirty = ele.defaultValue !== ele.value;
empty = !this.editor.getValue(),
clean = this.editor.isClean();
this.profileName = profileName;
if (profileName && BR.conf.profilesUrl && (!ele.value || !dirty)) {
if (profileName && BR.conf.profilesUrl && (empty || clean)) {
if (!(profileName in this.cache)) {
profileUrl = BR.conf.profilesUrl + profileName + '.brf';
BR.Util.get(profileUrl, L.bind(function(err, profileText) {
if (err) {
console.warn('Error getting profile from "' + profileUrl + '": ' + err);
return;
}
BR.Util.get(
profileUrl,
L.bind(function(err, profileText) {
if (err) {
console.warn(
'Error getting profile from "' +
profileUrl +
'": ' +
err
);
return;
}
this.cache[profileName] = profileText;
this.cache[profileName] = profileText;
// don't set when option has changed while loading
if (!this.profileName || this.profileName === profileName) {
ele.value = profileText;
ele.defaultValue = ele.value;
autosize.update(this.ele);
}
}, this));
// don't set when option has changed while loading
if (
!this.profileName ||
this.profileName === profileName
) {
this._setValue(profileText);
}
}, this)
);
} else {
ele.value = this.cache[profileName];
ele.defaultValue = ele.value;
autosize.update(this.ele);
this._setValue(this.cache[profileName]);
}
}
},
show: function() {
this.editor.refresh();
},
onResize: function() {
this.editor.refresh();
},
_upload: function(evt) {
var button = evt.target || evt.srcElement,
profile = this.ele.value;
profile = this.editor.getValue();
this.message.hide();
$(button).button('uploading');
@ -66,12 +83,15 @@ BR.Profile = L.Class.extend({
this.fire('update', {
profileText: profile,
callback: function () {
callback: function() {
$(button).button('reset');
$(button).blur();
}
});
},
_setValue: function(profileText) {
this.editor.setValue(profileText);
this.editor.markClean();
}
});
BR.Profile.include(L.Mixin.Events);

View file

@ -1,43 +1,45 @@
BR.RoutingOptions = BR.Control.extend({
onAdd: function (map) {
$('#profile-alternative').on('changed.bs.select', this._getChangeHandler());
BR.RoutingOptions = L.Evented.extend({
initialize: function() {
$('#profile-alternative').on(
'changed.bs.select',
this._getChangeHandler()
);
// build option list from config
var profiles = BR.conf.profiles;
var profiles_list = L.DomUtil.get('profile');
for (var i = 0; i < profiles.length; i++) {
var option = document.createElement("option");
var option = document.createElement('option');
option.value = profiles[i];
option.text = profiles[i];
option.text = i18next.t('navbar.profile.' + profiles[i]);
profiles_list.appendChild(option);
}
// set default value, used as indicator for empty custom profile
profiles_list.children[0].value = 'Custom';
// <custom> profile is empty at start, select next one
profiles_list.children[1].selected = true;
return BR.Control.prototype.onAdd.call(this, map);
},
refreshUI: function() {
var profile = $('#profile option:selected'),
alternative = $('#alternative option:selected');
$('#stat-profile').html(profile.text() + ' (' + alternative.text() +')');
// we do not allow to select more than one profile and/or alternative at a time
// so we disable the current selected items
$('#profile-alternative').find('option:disabled').each(function(index) {
$(this).prop('disabled', false);
});
$('#profile-alternative').find('option:selected').each(function(index) {
$(this).prop('disabled', true);
});
$('#profile-alternative')
.find('option:disabled')
.each(function(index) {
$(this).prop('disabled', false);
});
$('#profile-alternative')
.find('option:selected')
.each(function(index) {
$(this).prop('disabled', true);
});
// disable custom option if it has no value yet (default value is "Custom")
var custom = L.DomUtil.get('profile').children[0];
if (custom.value === "Custom") {
if (custom.value === 'Custom') {
custom.disabled = true;
}
$('.selectpicker').selectpicker('refresh')
$('.selectpicker').selectpicker('refresh');
},
getOptions: function() {
@ -53,8 +55,12 @@ BR.RoutingOptions = BR.Control.extend({
setOptions: function(options) {
var values = [
options.profile ? options.profile : $('#profile option:selected').val(),
options.alternative ? options.alternative : $('#alternative option:selected').val()
options.profile
? options.profile
: $('#profile option:selected').val(),
options.alternative
? options.alternative
: $('#alternative option:selected').val()
];
$('.selectpicker').selectpicker('val', values);
this.refreshUI();
@ -68,22 +74,28 @@ BR.RoutingOptions = BR.Control.extend({
},
setCustomProfile: function(profile, noUpdate) {
var profiles_grp,
option;
var profiles_grp, option;
profiles_grp = L.DomUtil.get('profile');
option = profiles_grp.children[0]
option.value = profile;
option = profiles_grp.children[0];
option.value = profile || 'Custom';
option.disabled = !profile;
$('#profile').find('option:selected').each(function(index) {
$(this).prop('selected', false);
});
if (profile) {
$('#profile')
.find('option:selected')
.each(function(index) {
$(this).prop('selected', false);
});
} else if (option.selected) {
// clear, select next in group when custom deselected
profiles_grp.children[1].selected = true;
}
option.selected = !!profile;
if (!noUpdate) {
this.fire('update', {options: this.getOptions()});
this.fire('update', { options: this.getOptions() });
}
},
@ -92,7 +104,7 @@ BR.RoutingOptions = BR.Control.extend({
option = profiles_grp.children[0],
profile = null;
if (!option.disabled) {
if (option.value !== 'Custom') {
profile = option.value;
}
return profile;
@ -100,9 +112,7 @@ BR.RoutingOptions = BR.Control.extend({
_getChangeHandler: function() {
return L.bind(function(evt) {
this.fire('update', {options: this.getOptions()});
this.fire('update', { options: this.getOptions() });
}, this);
}
});
BR.RoutingOptions.include(L.Mixin.Events);

View file

@ -1,47 +0,0 @@
BR.Tabs = BR.Control.extend({
options: {
divId: 'sidebar',
// tab a.hash > instance
tabs: {}
},
initialize: function (options) {
L.setOptions(this, options);
},
onAdd: function (map) {
var tabs = this.options.tabs;
for (var key in tabs) {
$('<li><a href="' + key + '" role="tab">' + tabs[key].options.heading + '</a></li>').appendTo('#tab');
if (tabs[key].onAdd) {
tabs[key].onAdd(map);
}
}
$('#tab a').click(function (e) {
e.preventDefault();
$(this).tab('show');
});
// e.target = activated tab
// e.relatedTarget = previous tab
$('#tab a').on('shown.bs.tab', L.bind(function (e) {
var tab = this.options.tabs[e.target.hash],
prevTab = e.relatedTarget ? this.options.tabs[e.relatedTarget.hash] : null;
if (tab && tab.show) {
tab.show();
}
if (prevTab && prevTab.hide) {
prevTab.hide();
}
}, this));
// activate first tab (instead of setting 'active' class in html)
$('#tab li:not(.hidden) a:first').tab('show');
return BR.Control.prototype.onAdd.call(this, map);
}
});

View file

@ -1,7 +1,5 @@
BR.TrackMessages = L.Class.extend({
options: {
heading: 'Segment data',
edgeStyle: {
color: 'yellow',
opacity: 0.8,
@ -13,31 +11,31 @@ BR.TrackMessages = L.Class.extend({
active: false,
columnOptions: {
'Longitude': { visible: false },
'Latitude': { visible: false },
'Elevation': { title: 'elev.', className: 'dt-body-right' },
'Distance': { title: 'dist.', className: 'dt-body-right' },
'CostPerKm': { title: '$/km', className: 'dt-body-right' },
'ElevCost': { title: 'elev$', className: 'dt-body-right' },
'TurnCost': { title: 'turn$', className: 'dt-body-right' },
'NodeCost': { title: 'node$', className: 'dt-body-right' },
'InitialCost': { title: 'initial$', className: 'dt-body-right' }
Longitude: { visible: false },
Latitude: { visible: false },
Elevation: { title: 'elev.', className: 'dt-body-right' },
Distance: { title: 'dist.', className: 'dt-body-right' },
CostPerKm: { title: '$/km', className: 'dt-body-right' },
ElevCost: { title: 'elev$', className: 'dt-body-right' },
TurnCost: { title: 'turn$', className: 'dt-body-right' },
NodeCost: { title: 'node$', className: 'dt-body-right' },
InitialCost: { title: 'initial$', className: 'dt-body-right' }
},
initialize: function (options) {
initialize: function(map, options) {
L.setOptions(this, options);
this._map = map;
var table = document.getElementById('datatable');
this.tableClassName = table.className;
this.tableParent = table.parentElement;
},
onAdd: function (map) {
this._map = map;
},
update: function (polyline, segments) {
var i, messages, columns, headings,
update: function(polyline, segments) {
var i,
messages,
columns,
headings,
data = [];
if (!this.active) {
@ -54,7 +52,7 @@ BR.TrackMessages = L.Class.extend({
this._destroyTable();
if (data.length === 0) {
return;
return;
}
headings = messages[0];
@ -67,13 +65,19 @@ BR.TrackMessages = L.Class.extend({
paging: false,
searching: false,
info: false,
// flexbox workaround: without scrollY height Firefox extends to content height
// (^= minimum height with flexbox?)
scrollY: 50,
scrollX: true,
order: []
});
// highlight track segment (graph edge) on row hover
this._setEdges(polyline, segments);
$('#datatable tbody tr').hover(L.bind(this._handleHover, this), L.bind(this._handleHoverOut, this));
$('#datatable tbody tr').hover(
L.bind(this._handleHover, this),
L.bind(this._handleHoverOut, this)
);
},
show: function() {
@ -88,9 +92,11 @@ BR.TrackMessages = L.Class.extend({
_destroyTable: function() {
var ele;
if ($.fn.DataTable.isDataTable('#datatable') ) {
if ($.fn.DataTable.isDataTable('#datatable')) {
// destroy option too slow on update, really remove elements with destroy method
$('#datatable').DataTable().destroy(true);
$('#datatable')
.DataTable()
.destroy(true);
// recreate original table element, destroy removes all
ele = document.createElement('table');
@ -144,7 +150,14 @@ BR.TrackMessages = L.Class.extend({
},
_setEdges: function(polyline, segments) {
var messages, segLatLngs, length, si, mi, latLng, i, segIndex,
var messages,
segLatLngs,
length,
si,
mi,
latLng,
i,
segIndex,
baseIndex = 0;
// track latLngs index for end node of edge
@ -185,7 +198,10 @@ BR.TrackMessages = L.Class.extend({
endIndex = this._edges[row.index()],
edgeLatLngs = trackLatLngs.slice(startIndex, endIndex + 1);
this._selectedEdge = L.polyline(edgeLatLngs, this.options.edgeStyle).addTo(this._map);
this._selectedEdge = L.polyline(
edgeLatLngs,
this.options.edgeStyle
).addTo(this._map);
},
_handleHoverOut: function(evt) {
@ -193,5 +209,3 @@ BR.TrackMessages = L.Class.extend({
this._selectedEdge = null;
}
});
BR.TrackMessages.include(L.Mixin.Events);

View file

@ -1,13 +1,61 @@
BR.TrackStats = L.Class.extend({
update: function (polyline, segments) {
var stats = this.calcStats(polyline, segments),
length1 = L.Util.formatNum(stats.trackLength/1000,1),
length3 = L.Util.formatNum(stats.trackLength/1000,3),
meanCostFactor = stats.trackLength ? L.Util.formatNum(stats.cost / stats.trackLength, 2) : ''
update: function(polyline, segments) {
if (segments.length == 0) {
$('#distance').html('-');
$('#distance').attr('title', '');
$('#ascend').html('-');
$('#plainascend').html('-');
$('#cost').html('-');
$('#meancostfactor').html('-');
$('#totaltime').html('-');
$('#totalenergy').html('-');
$('#meanenergy').html('-');
return;
}
$('#distance').html(length1 + ' <abbr title="kilometer">km</abbr>');
$('#ascend').html(stats.filteredAscend + ' (' + stats.plainAscend +')' + ' <abbr title="meter">m</abbr>');
$('#cost').html(stats.cost + ' (' + meanCostFactor + ')');
var stats = this.calcStats(polyline, segments),
length1 = L.Util.formatNum(
stats.trackLength / 1000,
1
).toLocaleString(),
length3 = L.Util.formatNum(
stats.trackLength / 1000,
3
).toLocaleString(),
formattedAscend = stats.filteredAscend.toLocaleString(),
formattedPlainAscend = stats.plainAscend.toLocaleString(),
formattedCost = stats.cost.toLocaleString(),
meanCostFactor = stats.trackLength
? L.Util.formatNum(
stats.cost / stats.trackLength,
2
).toLocaleString()
: '0',
formattedTime =
Math.trunc(stats.totalTime / 3600) +
':' +
('0' + Math.trunc((stats.totalTime % 3600) / 60)).slice(-2),
formattedEnergy = L.Util.formatNum(
stats.totalEnergy / 3600000,
2
).toLocaleString(),
meanEnergy = stats.trackLength
? L.Util.formatNum(
stats.totalEnergy / 36 / stats.trackLength,
2
).toLocaleString()
: '0';
$('#distance').html(length1);
// alternative 3-digit format down to meters as tooltip
$('#distance').attr('title', length3 + ' km');
$('#ascend').html(formattedAscend);
$('#plainascend').html(formattedPlainAscend);
$('#cost').html(formattedCost);
$('#meancostfactor').html(meanCostFactor);
$('#totaltime').html(formattedTime);
$('#totalenergy').html(formattedEnergy);
$('#meanenergy').html(meanEnergy);
},
calcStats: function(polyline, segments) {
@ -15,6 +63,8 @@ BR.TrackStats = L.Class.extend({
trackLength: 0,
filteredAscend: 0,
plainAscend: 0,
totalTime: 0,
totalEnergy: 0,
cost: 0
};
var i, props;
@ -24,6 +74,8 @@ BR.TrackStats = L.Class.extend({
stats.trackLength += +props['track-length'];
stats.filteredAscend += +props['filtered ascend'];
stats.plainAscend += +props['plain-ascend'];
stats.totalTime += +props['total-time'];
stats.totalEnergy += +props['total-energy'];
stats.cost += +props['cost'];
}

View file

@ -5,21 +5,26 @@
*/
(function() {
var mapContext;
function verifyTouchStyle(mapContext) {
// revert touch style (large icons) when touch screen detection is available and negative
// see https://github.com/nrenner/brouter-web/issues/69
if (L.Browser.touch && BR.Browser.touchScreenDetectable && !BR.Browser.touchScreen) {
L.DomUtil.removeClass(mapContext.map.getContainer(), 'leaflet-touch');
if (
L.Browser.touch &&
BR.Browser.touchScreenDetectable &&
!BR.Browser.touchScreen
) {
L.DomUtil.removeClass(
mapContext.map.getContainer(),
'leaflet-touch'
);
}
}
function initApp(mapContext) {
var map = mapContext.map,
layersControl = mapContext.layersControl,
mapLayers = mapContext.layers,
search,
router,
routing,
@ -28,20 +33,23 @@
stats,
itinerary,
elevation,
download,
tabs,
exportRoute,
profile,
trackMessages,
sidebar,
drawButton,
deleteButton,
deleteRouteButton,
drawToolbar,
urlHash,
reverseRoute,
saveWarningShown = false;
// By default bootstrap-select use glyphicons
$('.selectpicker').selectpicker({
iconBase: 'fa',
tickIcon: 'fa-check'
tickIcon: 'fa-check',
// don't overlap with footer
windowPadding: [0, 0, 40, 0]
});
search = new BR.Search();
@ -50,45 +58,75 @@
router = L.bRouter(); //brouterCgi dummyRouter
drawButton = L.easyButton({
states: [{
stateName: 'deactivate-draw',
icon: 'fa-pencil active',
onClick: function (control) {
routing.draw(false);
control.state('activate-draw');
states: [
{
stateName: 'deactivate-draw',
icon: 'fa-pencil active',
onClick: function(control) {
routing.draw(false);
control.state('activate-draw');
},
title: i18next.t('map.draw-route-stop')
},
title: 'Stop drawing route (ESC key)'
}, {
stateName: 'activate-draw',
icon: 'fa-pencil',
onClick: function (control) {
routing.draw(true);
control.state('deactivate-draw');
},
title: 'Draw route (D key)'
}]
{
stateName: 'activate-draw',
icon: 'fa-pencil',
onClick: function(control) {
routing.draw(true);
control.state('deactivate-draw');
},
title: i18next.t('map.draw-route-start')
}
]
});
deleteButton = L.easyButton(
reverseRouteButton = L.easyButton(
'fa-random',
function() {
routing.reverse();
},
i18next.t('map.reverse-route')
);
deletePointButton = L.easyButton(
'<span><i class="fa fa-caret-left"></i><i class="fa fa-map-marker" style="margin-left: 1px; color: gray;"></i></span>',
function() {
routing.removeWaypoint(routing.getLast(), function(
err,
data
) {});
},
i18next.t('map.delete-last-point')
);
deleteRouteButton = L.easyButton(
'fa-trash-o',
function () {
bootbox.confirm({
function() {
bootbox.prompt({
size: 'small',
message: "Delete route?",
title: i18next.t('map.delete-route'),
inputType: 'checkbox',
inputOptions: [
{
text: i18next.t('map.delete-nogo-areas'),
value: 'nogo'
}
],
callback: function(result) {
if (result) {
if (result !== null) {
routing.clear();
if (result.length > 0 && result[0] === 'nogo') {
nogos.clear();
}
onUpdate();
urlHash.onMapMove();
}
}
});
},
'Clear route'
i18next.t('map.clear-route')
);
drawToolbar = L.easyBar([drawButton, deleteButton]).addTo(map);
function updateRoute(evt) {
router.setOptions(evt.options);
@ -112,6 +150,11 @@
profile.update(evt.options);
});
BR.NogoAreas.MSG_BUTTON = i18next.t('map.nogo.draw');
BR.NogoAreas.MSG_BUTTON_CANCEL = i18next.t('map.nogo.cancel');
BR.NogoAreas.MSG_CREATE = i18next.t('map.nogo.click-drag');
BR.NogoAreas.MSG_DISABLED = i18next.t('map.nogo.edit');
BR.NogoAreas.MSG_ENABLED = i18next.t('map.nogo.help');
nogos = new BR.NogoAreas();
nogos.on('update', updateRoute);
@ -121,22 +164,26 @@
} else {
stats = new BR.TrackStats();
}
download = new BR.Download();
exportRoute = new BR.Export(router);
elevation = new BR.Elevation();
profile = new BR.Profile();
profile.on('update', function(evt) {
BR.message.hide();
var profileId = routingOptions.getCustomProfile();
router.uploadProfile(profileId, evt.profileText, function(err, profileId) {
router.uploadProfile(profileId, evt.profileText, function(
err,
profileId
) {
if (!err) {
routingOptions.setCustomProfile(profileId, true);
updateRoute({
options: routingOptions.getOptions()
});
if (!saveWarningShown) {
profile.message.showWarning('<strong>Note:</strong> Uploaded custom profiles are only cached temporarily on the server.'
+ '<br/>Please save your edits to your local PC.');
profile.message.showWarning(
i18next.t('warning.temporary-profile')
);
saveWarningShown = true;
}
} else {
@ -156,7 +203,7 @@
profile.message.hide();
routingOptions.setCustomProfile(null);
});
trackMessages = new BR.TrackMessages({
trackMessages = new BR.TrackMessages(map, {
requestUpdate: requestUpdate
});
@ -167,7 +214,9 @@
styles: BR.conf.routingStyles
});
routing.on('routing:routeWaypointEnd routing:setWaypointsEnd', function(evt) {
routing.on('routing:routeWaypointEnd routing:setWaypointsEnd', function(
evt
) {
search.clear();
onUpdate(evt && evt.err);
});
@ -191,8 +240,7 @@
var track = routing.toPolyline(),
segments = routing.getSegments(),
latLngs = routing.getWaypoints(),
segmentsLayer = routing._segments,
urls = {};
segmentsLayer = routing._segments;
elevation.update(track, segmentsLayer);
if (BR.conf.transit) {
@ -202,54 +250,62 @@
}
trackMessages.update(track, segments);
if (latLngs.length > 1) {
urls.gpx = router.getUrl(latLngs, 'gpx');
urls.kml = router.getUrl(latLngs, 'kml');
urls.geojson = router.getUrl(latLngs, 'geojson');
urls.csv = router.getUrl(latLngs, 'csv');
}
download.update(urls);
};
routingOptions.addTo(map);
exportRoute.update(latLngs);
}
routing.addTo(map);
elevation.addBelow(map);
tabs = new BR.Tabs({
tabs: {
'#tab_itinerary': itinerary,
'#tab_data': trackMessages
sidebar = BR.sidebar({
defaultTabId: BR.conf.transit ? 'tab_itinerary' : 'tab_profile',
listeningTabs: {
tab_profile: profile,
tab_data: trackMessages
}
});
if (!BR.conf.transit) {
delete tabs.options.tabs['#tab_itinerary'];
}).addTo(map);
if (BR.conf.transit) {
sidebar.showPanel('tab_itinerary');
}
map.addControl(tabs);
var sidebar = L.control.sidebar('sidebar', {
position: 'left'
});
sidebar.id = 'sidebar-control'; //required for persistence in local storage
map.addControl(sidebar);
nogos.addTo(map);
map.addControl(new BR.OpacitySlider({
callback: L.bind(routing.setOpacity, routing)
}));
drawToolbar = L.easyBar([
drawButton,
reverseRouteButton,
nogos.getButton(),
deletePointButton,
deleteRouteButton
]).addTo(map);
nogos.preventRoutePointOnCreate(routing);
if (BR.keys.strava) {
BR.stravaSegments(map, layersControl);
}
map.addControl(
new BR.OpacitySliderControl({
id: 'route',
title: i18next.t('map.opacity-slider'),
callback: L.bind(routing.setOpacity, routing)
})
);
// initial option settings (after controls are added and initialized with onAdd)
router.setOptions(nogos.getOptions());
router.setOptions(routingOptions.getOptions());
profile.update(routingOptions.getOptions());
// restore active layers from local storage when called without hash
// (check before hash plugin init)
if (!location.hash) {
layersControl.loadActiveLayers();
}
var onHashChangeCb = function(url) {
var url2params = function (s) {
var url2params = function(s) {
s = s.replace(/;/g, '|');
var p = {};
var sep = '&';
if (s.search('&amp;') !== -1)
sep = '&amp;';
if (s.search('&amp;') !== -1) sep = '&amp;';
var params = s.split(sep);
for (var i = 0; i < params.length; i++) {
var tmp = params[i].split('=');
@ -257,7 +313,7 @@
p[tmp[0]] = decodeURIComponent(tmp[1]);
}
return p;
}
};
if (url == null) return;
var opts = router.parseUrlParams(url2params(url));
router.setOptions(opts);
@ -283,35 +339,48 @@
// do not initialize immediately
urlHash = new L.Hash(null, null);
urlHash.additionalCb = function() {
var url = router.getUrl(routing.getWaypoints(), null).substr('brouter?'.length+1);
return url.length > 0 ? '&' + url : null;
};
var url = router
.getUrl(routing.getWaypoints(), null)
.substr('brouter?'.length + 1);
url = url.replace(/\|/g, ';');
return url.length > 0 ? '&' + url : null;
};
urlHash.onHashChangeCb = onHashChangeCb;
urlHash.onInvalidHashChangeCb = onInvalidHashChangeCb;
urlHash.layers = mapLayers;
urlHash.map = map;
urlHash.init(map, mapLayers);
urlHash.init(map, {
layersControl: layersControl
});
// activate configured default base layer or first if no hash,
// only after hash init, by using the same delay
setTimeout(function() {
layersControl.activateDefaultBaseLayer();
}, urlHash.changeDefer);
routingOptions.on('update', urlHash.onMapMove, urlHash);
nogos.on('update', urlHash.onMapMove, urlHash);
// waypoint add, move, delete (but last)
routing.on('routing:routeWaypointEnd', urlHash.onMapMove, urlHash);
// delete last waypoint
routing.on('waypoint:click', function (evt) {
var r = evt.marker._routing;
if (!r.prevMarker && !r.nextMarker) {
urlHash.onMapMove();
}
}, urlHash);
routing.on(
'waypoint:click',
function(evt) {
var r = evt.marker._routing;
if (!r.prevMarker && !r.nextMarker) {
urlHash.onMapMove();
}
},
urlHash
);
$(window).resize(function () {
$(window).resize(function() {
elevation.addBelow(map);
});
$('#elevation-chart').on('show.bs.collapse', function () {
$('#elevation-chart').on('show.bs.collapse', function() {
$('#elevation-btn').addClass('active');
});
$('#elevation-chart').on('hidden.bs.collapse', function () {
$('#elevation-chart').on('hidden.bs.collapse', function() {
$('#elevation-btn').removeClass('active');
// we must fetch tiles that are located behind elevation-chart
map._onResize();
@ -319,36 +388,149 @@
var onHide = function() {
if (this.id && BR.Util.localStorageAvailable()) {
localStorage[this.id] = 'true';
localStorage.removeItem(this.id);
}
};
var onShow = function() {
if (this.id && BR.Util.localStorageAvailable()) {
localStorage.removeItem(this.id);
localStorage[this.id] = 'true';
}
};
var toggleSidebar = function (event) {
sidebar.toggle();
$('#sidebar-btn').toggleClass('active');
};
$('#sidebar-btn').on('click', toggleSidebar);
sidebar.on('shown', onShow);
sidebar.on('hidden', onHide);
// on page load, we want to restore collapsible elements from previous usage
$('.collapse').on('hidden.bs.collapse', onHide)
.on('shown.bs.collapse', onShow)
.each(function() {
if (!(this.id && BR.Util.localStorageAvailable() && localStorage[this.id] === 'true' )) {
$(this).collapse('hide');
}
});
if (BR.Util.localStorageAvailable() && localStorage[sidebar.id] !== 'true') {
toggleSidebar();
}
$('.collapse')
.on('hidden.bs.collapse', onHide)
.on('shown.bs.collapse', onShow)
.each(function() {
if (
this.id &&
BR.Util.localStorageAvailable() &&
localStorage[this.id] === 'true'
) {
$(this).collapse('show');
}
});
$('#submitNogos').on('click', function() {
var geoJSONPromise;
var nogoURL = $('#nogoURL').val();
var nogoFile = $('#nogoFile')[0].files[0];
if (nogoURL) {
// TODO: Handle {{bbox}}
geoJSONPromise = fetch(nogoURL).then(function(response) {
response.json();
});
} else if (nogoFile) {
geoJSONPromise = new Promise(function(resolve, reject) {
var reader = new FileReader();
reader.onload = function() {
resolve(reader.result);
};
reader.readAsText(nogoFile);
}).then(function(response) {
return JSON.parse(response);
});
} else {
$('#nogoError').text('Error: Missing file or URL.');
$('#nogoError').css('display', 'block');
return false;
}
var nogoWeight = parseFloat($('#nogoWeight').val());
if (isNaN(nogoWeight)) {
$('#nogoError').text('Error: Missing default nogo weight.');
$('#nogoError').css('display', 'block');
return false;
}
var nogoRadius = parseFloat($('#nogoRadius').val());
if (isNaN(nogoRadius) || nogoRadius < 0) {
$('#nogoError').text('Error: Invalid default nogo radius.');
$('#nogoError').css('display', 'block');
return false;
}
var nogoBuffer = parseFloat($('#nogoBuffer').val());
if (isNaN(nogoBuffer)) {
$('#nogoError').text('Error: Invalid nogo buffering radius.');
$('#nogoError').css('display', 'block');
return false;
}
geoJSONPromise.then(function(response) {
// Iterate on features in order to discard features without geometry
var cleanedGeoJSONFeatures = [];
turf.featureEach(response, function(feature) {
if (turf.getGeom(feature)) {
var maybeBufferedFeature = feature;
// Eventually buffer GeoJSON
if (nogoBuffer != 0) {
maybeBufferedFeature = turf.buffer(
maybeBufferedFeature,
nogoBuffer,
{ units: 'meters' }
);
}
cleanedGeoJSONFeatures.push(maybeBufferedFeature);
}
});
var geoJSON = L.geoJson(
turf.featureCollection(cleanedGeoJSONFeatures),
{
onEachFeature: function(feature, layer) {
layer.options.nogoWeight =
feature.properties.nogoWeight || nogoWeight;
}
}
);
var nogosPoints = geoJSON.getLayers().filter(function(e) {
return e.feature.geometry.type === 'Point';
});
nogosPoints = nogosPoints.map(function(item) {
var radius = item.feature.properties.radius || nogoRadius;
if (radius > 0) {
return L.circle(item.getLatLng(), { radius: radius });
}
return null;
});
nogosPoints = nogosPoints.filter(function(e) {
return e;
});
nogos.setOptions({
nogos: nogosPoints,
polygons: geoJSON.getLayers().filter(function(e) {
return e.feature.geometry.type === 'Polygon';
}),
polylines: geoJSON.getLayers().filter(function(e) {
return e.feature.geometry.type === 'LineString';
})
});
updateRoute({
options: nogos.getOptions()
});
urlHash.onMapMove();
$('#nogoError').text('');
$('#nogoError').css('display', 'none');
$('#loadNogos').modal('hide');
});
return false;
});
}
mapContext = BR.Map.initMap();
verifyTouchStyle(mapContext);
initApp(mapContext);
i18next
.use(window.i18nextXHRBackend)
.use(window.i18nextBrowserLanguageDetector)
.init(
{
fallbackLng: 'en',
backend: {
loadPath: 'dist/locales/{{lng}}.json'
}
},
function(err, t) {
jqueryI18next.init(i18next, $);
$('html').localize();
mapContext = BR.Map.initMap();
verifyTouchStyle(mapContext);
initApp(mapContext);
}
);
})();

View file

@ -1,17 +1,19 @@
BR.BingLayer = L.BingLayer.extend({
options: {
maxZoom: 19,
attribution: '<a target="_blank" href="https://www.bing.com/maps/">Bing Maps</a>'
+ ' (<a target="_blank" href="https://go.microsoft.com/?linkid=9710837">TOU</a>)'
attribution:
'<a target="_blank" href="https://www.bing.com/maps/">Bing Maps</a>' +
' (<a target="_blank" href="https://go.microsoft.com/?linkid=9710837">TOU</a>)'
},
initialize: function(key, options) {
L.BingLayer.prototype.initialize.call(this, key, options);
this._logo = L.control({position: 'bottomleft'});
this._logo.onAdd = function (map) {
this._logo = L.control({ position: 'bottomleft' });
this._logo.onAdd = function(map) {
this._div = L.DomUtil.create('div', 'bing-logo');
this._div.innerHTML = '<img src="https://www.microsoft.com/maps/images/branding/Bing%20logo%20white_50px-19px.png">';
this._div.innerHTML =
'<img src="https://www.microsoft.com/maps/images/branding/Bing%20logo%20white_50px-19px.png">';
return this._div;
};
},

View file

@ -1,33 +1,47 @@
BR.Elevation = L.Control.Elevation.extend({
options: {
width:$('#map').outerWidth(),
width: $('#map').outerWidth(),
margins: {
top: 20,
right: 30,
bottom: 30,
left: 60
},
theme: "steelblue-theme"
theme: 'steelblue-theme'
},
onAdd: function (map) {
onAdd: function(map) {
var container = L.Control.Elevation.prototype.onAdd.call(this, map);
// revert registering touch events when touch screen detection is available and negative
// see https://github.com/MrMufflon/Leaflet.Elevation/issues/67
if (L.Browser.touch && BR.Browser.touchScreenDetectable && !BR.Browser.touchScreen) {
this._background.on("touchmove.drag", null).
on("touchstart.drag", null).
on("touchstart.focus", null);
L.DomEvent.off(this._container, 'touchend', this._dragEndHandler, this);
this._background.on("mousemove.focus", this._mousemoveHandler.bind(this)).
on("mouseout.focus", this._mouseoutHandler.bind(this)).
on("mousedown.drag", this._dragStartHandler.bind(this)).
on("mousemove.drag", this._dragHandler.bind(this));
L.DomEvent.on(this._container, 'mouseup', this._dragEndHandler, this);
if (
L.Browser.touch &&
BR.Browser.touchScreenDetectable &&
!BR.Browser.touchScreen
) {
this._background
.on('touchmove.drag', null)
.on('touchstart.drag', null)
.on('touchstart.focus', null);
L.DomEvent.off(
this._container,
'touchend',
this._dragEndHandler,
this
);
this._background
.on('mousemove.focus', this._mousemoveHandler.bind(this))
.on('mouseout.focus', this._mouseoutHandler.bind(this))
.on('mousedown.drag', this._dragStartHandler.bind(this))
.on('mousemove.drag', this._dragHandler.bind(this));
L.DomEvent.on(
this._container,
'mouseup',
this._dragEndHandler,
this
);
}
return container;
@ -36,7 +50,7 @@ BR.Elevation = L.Control.Elevation.extend({
addBelow: function(map) {
// waiting for https://github.com/MrMufflon/Leaflet.Elevation/pull/66
// this.width($('#map').outerWidth());
this.options.width = $('#map').outerWidth();
this.options.width = $('#content').outerWidth();
if (this.getContainer() != null) {
this.remove(map);
@ -44,10 +58,13 @@ BR.Elevation = L.Control.Elevation.extend({
function setParent(el, newParent) {
newParent.appendChild(el);
}
this.addTo(map);
}
this.addTo(map);
// move elevation graph outside of the map
setParent(this.getContainer(), document.getElementById('elevation-chart'));
setParent(
this.getContainer(),
document.getElementById('elevation-chart')
);
},
update: function(track, layer) {
@ -62,7 +79,7 @@ BR.Elevation = L.Control.Elevation.extend({
if (track && track.getLatLngs().length > 0) {
this.addData(track.toGeoJSON(), layer);
layer.on("mouseout", this._hidePositionMarker.bind(this));
layer.on('mouseout', this._hidePositionMarker.bind(this));
}
}
});
});

View file

@ -1,76 +1,504 @@
L.drawLocal.draw.toolbar.buttons.circle = 'Draw no-go area (circle)';
L.drawLocal.edit.toolbar.buttons.edit = 'Edit no-go areas';
L.drawLocal.edit.toolbar.buttons.remove = 'Delete no-go areas';
BR.NogoAreas = L.Control.Draw.extend({
initialize: function () {
this.drawnItems = new L.FeatureGroup();
L.Control.Draw.prototype.initialize.call(this, {
draw: {
position: 'topleft',
polyline: false,
polygon: false,
circle: true,
rectangle: false,
marker: false
},
edit: {
featureGroup: this.drawnItems,
//edit: false,
edit: {
selectedPathOptions: {
//opacity: 0.8
}
},
remove: true
}
});
BR.NogoAreas = L.Control.extend({
statics: {
MSG_BUTTON: 'Draw no-go area (circle)',
MSG_BUTTON_CANCEL: 'Cancel drawing no-go area',
MSG_CREATE: 'Click and drag to draw circle',
MSG_DISABLED: 'Click to edit',
MSG_ENABLED:
'&square; = move / resize, <span class="fa fa-trash-o"></span> = delete,<br>click nogo to quit editing',
STATE_CREATE: 'no-go-create',
STATE_CANCEL: 'cancel-no-go-create'
},
onAdd: function (map) {
map.addLayer(this.drawnItems);
style: {
color: '#f06eaa',
weight: 4,
opacity: 0.5,
fillColor: null, //same as color by default
fillOpacity: 0.2,
dashArray: null
},
map.on('draw:created', function (e) {
var layer = e.layer;
this.drawnItems.addLayer(layer);
this._fireUpdate();
}, this);
editStyle: {
color: '#fe57a1',
opacity: 0.6,
dashArray: '10, 10',
fillOpacity: 0.1
},
map.on('draw:editstart', function (e) {
this.drawnItems.eachLayer(function (layer) {
layer.on('edit', function(e) {
this._fireUpdate();
}, this);
}, this);
}, this);
initialize: function() {
this._wasRouteDrawing = false;
},
map.on('draw:deleted', function (e) {
this._fireUpdate();
}, this);
onAdd: function(map) {
var self = this;
return L.Control.Draw.prototype.onAdd.call(this, map);
this.drawnItems = new L.FeatureGroup().addTo(map);
this.drawnItems.on('click', function(e) {
L.DomEvent.stop(e);
e.layer.toggleEdit();
});
var editTools = (this.editTools = map.editTools = new L.Editable(map, {
circleEditorClass: BR.DeletableCircleEditor,
// FeatureGroup instead of LayerGroup to propagate events to members
editLayer: new L.FeatureGroup().addTo(map),
featuresLayer: this.drawnItems
}));
this.button = L.easyButton({
states: [
{
stateName: BR.NogoAreas.STATE_CREATE,
icon: 'fa-ban',
title: BR.NogoAreas.MSG_BUTTON,
onClick: function(control) {
// initial radius of 0 to detect click, see DeletableCircleEditor.onDrawingMouseUp
var opts = L.extend({ radius: 0 }, self.style);
editTools.startCircle(null, opts);
control.state('cancel-no-go-create');
}
},
{
stateName: BR.NogoAreas.STATE_CANCEL,
icon: 'fa-ban active',
title: BR.NogoAreas.MSG_BUTTON_CANCEL,
onClick: function(control) {
editTools.stopDrawing();
control.state('no-go-create');
}
}
]
});
this.editTools.on(
'editable:drawing:end',
function(e) {
self.button.state(BR.NogoAreas.STATE_CREATE);
setTimeout(
L.bind(function() {
// turn editing off after create; async to still fire 'editable:vertex:dragend'
e.layer.disableEdit();
}, this),
0
);
},
this
);
this.editTools.on(
'editable:vertex:dragend editable:deleted',
function(e) {
this._fireUpdate();
},
this
);
this.editTools.on(
'editable:enable',
function(e) {
e.layer.setStyle(this.editStyle);
},
this
);
this.editTools.on(
'editable:disable',
function(e) {
e.layer.setStyle(this.style);
},
this
);
this.tooltip = new BR.EditingTooltip(map, editTools, this.button);
this.tooltip.enable();
// dummy, no own representation, delegating to EasyButton
return L.DomUtil.create('div');
},
// prevent route waypoint added after circle create (map click after up)
preventRoutePointOnCreate: function(routing) {
this.editTools.on(
'editable:drawing:start',
function(e) {
this._wasRouteDrawing = routing.isDrawing();
routing.draw(false);
},
this
);
// after create
this.editTools.on(
'editable:drawing:end',
function(e) {
if (this._wasRouteDrawing) {
setTimeout(function() {
routing.draw(true);
}, 0);
}
},
this
);
},
getOptions: function() {
return {
nogos: this.drawnItems.getLayers()
nogos: this.drawnItems.getLayers().filter(function(e) {
return e instanceof L.Circle;
}),
polygons: this.drawnItems.getLayers().filter(function(e) {
return e instanceof L.Polygon;
}),
polylines: this.drawnItems.getLayers().filter(function(e) {
return e instanceof L.Polyline && !(e instanceof L.Polygon);
})
};
},
setOptions: function(options) {
var nogos = options.nogos;
this.drawnItems.clearLayers();
var polylines = options.polylines;
var polygons = options.polygons;
this._clear();
if (nogos) {
for (var i = 0; i < nogos.length; i++) {
nogos[i].setStyle(this.style);
this.drawnItems.addLayer(nogos[i]);
}
}
if (polylines) {
for (var i = 0; i < polylines.length; i++) {
polylines[i].setStyle(this.style);
this.drawnItems.addLayer(polylines[i]);
}
}
if (polygons) {
for (var i = 0; i < polygons.length; i++) {
polygons[i].setStyle(this.style);
this.drawnItems.addLayer(polygons[i]);
}
}
},
_fireUpdate: function () {
this.fire('update', {options: this.getOptions()});
_clear: function() {
this.drawnItems.clearLayers();
},
clear: function() {
this._clear();
this._fireUpdate();
},
_fireUpdate: function() {
this.fire('update', { options: this.getOptions() });
},
getFeatureGroup: function() {
return this.drawnItems;
},
getEditGroup: function() {
return this.editTools.editLayer;
},
getButton: function() {
return this.button;
}
});
BR.NogoAreas.include(L.Mixin.Events);
BR.NogoAreas.include(L.Evented.prototype);
L.Editable.prototype.createVertexIcon = function(options) {
return BR.Browser.touch
? new L.Editable.TouchVertexIcon(options)
: new L.Editable.VertexIcon(options);
};
BR.EditingTooltip = L.Handler.extend({
options: {
closeTimeout: 2000
},
initialize: function(map, editTools, button) {
this.map = map;
this.editTools = editTools;
this.button = button;
},
addHooks: function() {
// hack: listen to EasyButton click (instead of editable:drawing:start),
// to get mouse position from event for initial tooltip location
L.DomEvent.addListener(
this.button.button,
'click',
this._addCreate,
this
);
this.editTools.featuresLayer.on('layeradd', this._bind, this);
this.editTools.on('editable:drawing:end', this._postCreate, this);
this.editTools.on('editable:enable', this._enable, this);
this.editTools.on('editable:disable', this._disable, this);
},
removeHooks: function() {
L.DomEvent.removeListener(
this.button.button,
'click',
this._addCreate,
this
);
this.editTools.featuresLayer.off('layeradd', this._bind, this);
this.editTools.off('editable:drawing:end', this._postCreate, this);
this.editTools.off('editable:enable', this._enable, this);
this.editTools.off('editable:disable', this._disable, this);
},
_bind: function(e) {
// Position tooltip at bottom of circle, less distracting than
// sticky with cursor or at center.
var layer = e.layer;
layer.bindTooltip(BR.NogoAreas.MSG_DISABLED, {
direction: 'bottom',
className: 'editing-tooltip'
});
// Override to set position to south instead of center (circle latlng);
// works better with zooming than updating offset to match radius
layer.openTooltip = function(layer, latlng) {
if (!latlng && layer instanceof L.Layer) {
latlng = L.latLng(
layer.getBounds().getSouth(),
0.5 *
(layer.getBounds().getWest() +
layer.getBounds().getEast())
);
}
L.Layer.prototype.openTooltip.call(this, layer, latlng);
};
},
_addCreate: function(e) {
// button cancel
if (!this.editTools.drawing()) return;
var initialLatLng = this.map.mouseEventToLatLng(e);
var tooltip = L.tooltip({
// no effect with map tooltip
sticky: true,
// offset wrong with 'auto' when switching direction
direction: 'right',
offset: L.point(5, 28),
className: 'editing-tooltip-create'
});
// self-reference hack for _moveTooltip, as tooltip is not bound to layer
tooltip._tooltip = tooltip;
// simulate sticky feature (follow mouse) for map tooltip without layer
var onOffMove = function(e) {
var onOff = e.type === 'tooltipclose' ? 'off' : 'on';
this._map[onOff]('mousemove', this._moveTooltip, this);
};
this.map.on('tooltipopen', onOffMove, tooltip);
this.map.on('tooltipclose', onOffMove, tooltip);
var onTooltipRemove = function(e) {
this.map.off('tooltipopen', onOffMove, e.tooltip);
this.map.off('tooltipclose', onOffMove, e.tooltip);
this.map.off('tooltipclose', onTooltipRemove, this);
e.tooltip._tooltip = null;
};
this.map.on('tooltipclose', onTooltipRemove, this);
tooltip.setTooltipContent(BR.NogoAreas.MSG_CREATE);
this.map.openTooltip(tooltip, initialLatLng);
var closeTooltip = function() {
this.map.closeTooltip(tooltip);
};
this.editTools.once(
'editable:editing editable:drawing:cancel',
closeTooltip,
this
);
if (BR.Browser.touch) {
// can't move with cursor on touch devices, so show at start pos for a few seconds
setTimeout(L.bind(closeTooltip, this), this.options.closeTimeout);
}
},
_setCloseTimeout: function(layer) {
var timeoutId = setTimeout(function() {
layer.closeTooltip();
}, this.options.closeTimeout);
// prevent timer to close tooltip that changed in the meantime
layer.once('tooltipopen', function(e) {
clearTimeout(timeoutId);
});
},
_postCreate: function() {
// editing is disabled by another handler, tooltip won't stay open before
this.editTools.once(
'editable:disable',
function(e) {
// show for a few seconds, as mouse often not hovering circle after create
e.layer.openTooltip(e.layer);
this._setCloseTimeout(e.layer);
},
this
);
},
_enable: function(e) {
e.layer.setTooltipContent(BR.NogoAreas.MSG_ENABLED);
this.editTools.once(
'editable:editing',
function(e) {
e.layer.closeTooltip();
},
this
);
},
_disable: function(e) {
e.layer.setTooltipContent(BR.NogoAreas.MSG_DISABLED);
this._setCloseTimeout(e.layer);
}
});
BR.DeletableCircleEditor = L.Editable.CircleEditor.extend({
_computeDeleteLatLng: function() {
// While circle is not added to the map, _radius is not set.
var delta =
(this.feature._radius || this.feature._mRadius) *
Math.cos(Math.PI / 4),
point = this.map.project(this.feature._latlng);
return this.map.unproject([point.x - delta, point.y - delta]);
},
_updateDeleteLatLng: function() {
this._deleteLatLng.update(this._computeDeleteLatLng());
this._deleteLatLng.__vertex.update();
},
_addDeleteMarker: function() {
if (!this.enabled()) return;
this._deleteLatLng = this._computeDeleteLatLng();
return new BR.DeleteMarker(this._deleteLatLng, this);
},
_delete: function() {
this.disable();
this.tools.featuresLayer.removeLayer(this.feature);
},
delete: function() {
this._delete();
this.fireAndForward('editable:deleted');
},
initialize: function(map, feature, options) {
L.Editable.CircleEditor.prototype.initialize.call(
this,
map,
feature,
options
);
this._deleteLatLng = this._computeDeleteLatLng();
// FeatureGroup instead of LayerGroup to propagate events to members
this.editLayer = new L.FeatureGroup();
},
addHooks: function() {
L.Editable.CircleEditor.prototype.addHooks.call(this);
if (this.feature) {
this._addDeleteMarker();
}
return this;
},
reset: function() {
L.Editable.CircleEditor.prototype.reset.call(this);
this._addDeleteMarker();
},
onDrawingMouseDown: function(e) {
this._deleteLatLng.update(e.latlng);
L.Editable.CircleEditor.prototype.onDrawingMouseDown.call(this, e);
},
// override to cancel/remove created circle when added by click instead of drag, because:
// - without resize, edit handles stacked on top of each other
// - makes event handling more complicated (editable:vertex:dragend not called)
onDrawingMouseUp: function(e) {
if (this.feature.getRadius() > 0) {
this.commitDrawing(e);
} else {
this.cancelDrawing(e);
this._delete();
}
e.originalEvent._simulated = false;
L.Editable.PathEditor.prototype.onDrawingMouseUp.call(this, e);
},
onVertexMarkerDrag: function(e) {
this._updateDeleteLatLng();
L.Editable.CircleEditor.prototype.onVertexMarkerDrag.call(this, e);
}
});
BR.DeleteMarker = L.Marker.extend({
options: {
draggable: false,
icon: L.divIcon({
iconSize: BR.Browser.touch
? new L.Point(24, 24)
: new L.Point(16, 16),
className: 'leaflet-div-icon fa fa-trash-o nogo-delete-marker'
})
},
initialize: function(latlng, editor, options) {
// derived from L.Editable.VertexMarker.initialize
// We don't use this._latlng, because on drag Leaflet replace it while
// we want to keep reference.
this.latlng = latlng;
this.editor = editor;
L.Marker.prototype.initialize.call(this, latlng, options);
this.latlng.__vertex = this;
this.editor.editLayer.addLayer(this);
// to keep small circles editable, make sure delete button is below drag handle
// (not using "+ 1" to place at bottom of other vertex markers)
this.setZIndexOffset(editor.tools._lastZIndex);
},
onAdd: function(map) {
L.Marker.prototype.onAdd.call(this, map);
this.on('click', this.onClick);
},
onRemove: function(map) {
delete this.latlng.__vertex;
this.off('click', this.onClick);
L.Marker.prototype.onRemove.call(this, map);
},
onClick: function(e) {
this.editor.delete();
}
});

View file

@ -1,7 +1,7 @@
L.Routing.Draw.prototype._hideTrailer = function() {
if (this._trailer.options.opacity !== 0.0) {
this._trailer.setStyle({opacity: 0.0});
}
if (this._trailer.options.opacity !== 0.0) {
this._trailer.setStyle({ opacity: 0.0 });
}
};
BR.Routing = L.Routing.extend({
@ -20,7 +20,7 @@ BR.Routing = L.Routing.extend({
zIndexOffset: -2000
},
onAdd: function (map) {
onAdd: function(map) {
this._segmentsCasing = new L.FeatureGroup().addTo(map);
var container = L.Routing.prototype.onAdd.call(this, map);
@ -31,35 +31,50 @@ BR.Routing = L.Routing.extend({
this._waypoints.on('layeradd', this._setMarkerOpacity, this);
// turn line mouse marker off while over waypoint marker
this.on('waypoint:mouseover', function(e) {
// L.Routing.Edit._segmentOnMouseout without firing 'segment:mouseout' (enables draw)
if (this._dragging) { return; }
this.on(
'waypoint:mouseover',
function(e) {
// L.Routing.Edit._segmentOnMouseout without firing 'segment:mouseout' (enables draw)
if (this._dragging) {
return;
}
this._mouseMarker.setOpacity(0.0);
this._map.off('mousemove', this._segmentOnMousemove, this);
this._suspended = true;
}, this._edit);
this._mouseMarker.setOpacity(0.0);
this._map.off('mousemove', this._segmentOnMousemove, this);
this._suspended = true;
},
this._edit
);
this.on('waypoint:mouseout', function(e) {
this._segmentOnMouseover(e);
this._suspended = false;
}, this._edit);
this.on(
'waypoint:mouseout',
function(e) {
this._segmentOnMouseover(e);
this._suspended = false;
},
this._edit
);
this._edit._mouseMarker.setIcon(L.divIcon({
className: 'line-mouse-marker'
,iconAnchor: [8, 8] // size/2 + border/2
,iconSize: [16, 16]
}));
this._edit._mouseMarker.setIcon(
L.divIcon({
className: 'line-mouse-marker',
iconAnchor: [8, 8], // size/2 + border/2
iconSize: [16, 16]
})
);
// Forward mousemove event to snapped feature (for Leaflet.Elevation to
// update indicator), see also L.Routing.Edit._segmentOnMousemove
this._edit._mouseMarker.on('move', L.bind(function(e) {
var latLng = e.latlng;
if (latLng._feature) {
this._mouseMarker._feature = latLng._feature;
latLng._feature.fire('mousemove', e, true);
}
}, this._edit));
this._edit._mouseMarker.on(
'move',
L.bind(function(e) {
var latLng = e.latlng;
if (latLng._feature) {
this._mouseMarker._feature = latLng._feature;
latLng._feature.fire('mousemove', e, true);
}
}, this._edit)
);
var mouseoutHandler = function(e) {
if (this._mouseMarker._feature) {
this._mouseMarker._feature.fire('mouseout', e, true);
@ -79,12 +94,20 @@ BR.Routing = L.Routing.extend({
// intercept listener: only re-show draw trailer after marker hover
// when edit is not active (i.e. wasn't also supended)
this._parent.off('waypoint:mouseout' , this._catchWaypointEvent, this);
this.on('waypoint:mouseout' , function(e) {
if (!this._parent._edit._suspended) {
this._catchWaypointEvent(e);
}
}, this);
this._parent.off(
'waypoint:mouseout',
this._catchWaypointEvent,
this
);
this.on(
'waypoint:mouseout',
function(e) {
if (!this._parent._edit._suspended) {
this._catchWaypointEvent(e);
}
},
this
);
});
this._draw.on('disabled', function() {
L.DomUtil.removeClass(map.getContainer(), 'routing-draw-enabled');
@ -111,7 +134,12 @@ BR.Routing = L.Routing.extend({
this._map.off('mouseout', hide, this);
this._map.off('mouseover', show, this);
L.DomEvent.off(this._map._controlContainer, 'mouseout', show, this);
L.DomEvent.off(this._map._controlContainer, 'mouseover', hide, this);
L.DomEvent.off(
this._map._controlContainer,
'mouseover',
hide,
this
);
});
// Call show after deleting last waypoint, but hide trailer.
@ -119,180 +147,216 @@ BR.Routing = L.Routing.extend({
// mouseout to show again never fires when deleted. Click handler
// _onMouseClick aborts when hidden, so no waypoint can be added
// although enabled.
this.on('waypoint:click', function() {
if (this._hidden && !this._parent._waypoints._first) {
this._show();
this._hideTrailer();
}
}, this._draw);
this.on(
'waypoint:click',
function() {
if (this._hidden && !this._parent._waypoints._first) {
this._show();
this._hideTrailer();
}
},
this._draw
);
// keys not working when map container does not have focus, use document instead
L.DomEvent.removeListener(this._container, 'keyup', this._keyupListener);
L.DomEvent.removeListener(
this._container,
'keyup',
this._keyupListener
);
L.DomEvent.addListener(document, 'keyup', this._keyupListener, this);
// enable drawing mode
this.draw(true);
return container;
}
},
,_addSegmentCasing: function(e) {
var casing = L.polyline(e.layer.getLatLngs(), this.options.styles.trackCasing);
this._segmentsCasing.addLayer(casing);
e.layer._casing = casing;
this._segments.bringToFront();
}
_addSegmentCasing: function(e) {
var casing = L.polyline(
e.layer.getLatLngs(),
this.options.styles.trackCasing
);
this._segmentsCasing.addLayer(casing);
e.layer._casing = casing;
this._segments.bringToFront();
},
,_removeSegmentCasing: function(e) {
this._segmentsCasing.removeLayer(e.layer._casing);
}
_removeSegmentCasing: function(e) {
this._segmentsCasing.removeLayer(e.layer._casing);
},
,setOpacity: function(opacity) {
// Due to the second Polyline layer for casing, the combined opacity is less
// transparent than with a single layer and the slider is non-linear. The
// inverted formula is used to get the same result as with a single layer.
// SVG simple alpha compositing: Ca' = 1 - (1 - Ea) * (1 - Ca)
// https://www.w3.org/TR/SVG11/masking.html#SimpleAlphaBlending
var sourceOpacity = 1 - Math.sqrt(1 - opacity);
setOpacity: function(opacity) {
// Due to the second Polyline layer for casing, the combined opacity is less
// transparent than with a single layer and the slider is non-linear. The
// inverted formula is used to get the same result as with a single layer.
// SVG simple alpha compositing: Ca' = 1 - (1 - Ea) * (1 - Ca)
// https://www.w3.org/TR/SVG11/masking.html#SimpleAlphaBlending
var sourceOpacity = 1 - Math.sqrt(1 - opacity);
this.options.styles.track.opacity = sourceOpacity;
this.options.styles.trackCasing.opacity = sourceOpacity;
this.options.icons.opacity = opacity;
this.options.styles.track.opacity = sourceOpacity;
this.options.styles.trackCasing.opacity = sourceOpacity;
this.options.icons.opacity = opacity;
this._segments.setStyle({
opacity: sourceOpacity
});
this._segmentsCasing.setStyle({
opacity: sourceOpacity
});
this._waypoints.eachLayer(function(marker) {
marker.setOpacity(opacity);
});
}
,_setMarkerOpacity: function(e) {
e.layer.setOpacity(this.options.icons.opacity);
}
,_removeMarkerEvents: function(marker) {
marker.off('mouseover', this._fireWaypointEvent, this);
marker.off('mouseout' , this._fireWaypointEvent, this);
marker.off('dragstart', this._fireWaypointEvent, this);
marker.off('dragend' , this._fireWaypointEvent, this);
marker.off('drag' , this._fireWaypointEvent, this);
marker.off('click' , this._fireWaypointEvent, this);
}
,clear: function() {
var drawEnabled = this._draw._enabled;
var current = this._waypoints._first;
this.draw(false);
if (current === null) { return; }
this._removeMarkerEvents(current);
while (current._routing.nextMarker) {
var marker = current._routing.nextMarker;
this._removeMarkerEvents(marker);
current = marker;
};
this._waypoints._first = null;
this._waypoints._last = null;
this._waypoints.clearLayers();
this._segments.clearLayers();
if (drawEnabled) {
this.draw(true);
}
}
,setWaypoints: function(latLngs, cb) {
var i;
var callbackCount = 0;
var firstErr;
var $this = this;
var callback = function(err, data) {
callbackCount++;
firstErr = firstErr || err;
if (callbackCount >= latLngs.length) {
$this.fire('routing:setWaypointsEnd', { err: firstErr });
if (cb) {
cb(firstErr);
}
}
};
this.fire('routing:setWaypointsStart');
for (i = 0; latLngs && i < latLngs.length; i++) {
this.addWaypoint(latLngs[i], this._waypoints._last, null, callback);
}
}
// patch to fix error when line is null or error line
// (when called while still segments to calculate, e.g. permalink or fast drawing)
,toPolyline: function() {
var latLngs = [];
this._eachSegment(function(m1, m2, line) {
// omit if null (still calculating) or error
// NOTE: feature check specific to BRouter GeoJSON response, workaround to detect error line
if (line && line.feature) {
latLngs = latLngs.concat(line.getLatLngs());
}
});
return L.polyline(latLngs);
}
,_routeSegment: function(m1, m2, cb) {
var loadingTrailer;
// change segment color before request to indicate recalculation (mark old)
if (m1 && m1._routing.nextLine !== null) {
m1._routing.nextLine.setStyle({color: 'dimgray' });
}
// animate dashed trailer as loading indicator
if (m1 && m2) {
loadingTrailer = new L.Polyline([m1.getLatLng(), m2.getLatLng()], {
color: this.options.styles.track.color,
opacity: this.options.styles.trailer.opacity,
dashArray: [10, 10],
className: 'loading-trailer'
this._segments.setStyle({
opacity: sourceOpacity
});
loadingTrailer.addTo(this._map);
}
this._segmentsCasing.setStyle({
opacity: sourceOpacity
});
this._waypoints.eachLayer(function(marker) {
marker.setOpacity(opacity);
});
},
L.Routing.prototype._routeSegment.call(this, m1, m2, L.bind(function(err, data) {
if (loadingTrailer) {
this._map.removeLayer(loadingTrailer);
_setMarkerOpacity: function(e) {
e.layer.setOpacity(this.options.icons.opacity);
},
_removeMarkerEvents: function(marker) {
marker.off('mouseover', this._fireWaypointEvent, this);
marker.off('mouseout', this._fireWaypointEvent, this);
marker.off('dragstart', this._fireWaypointEvent, this);
marker.off('dragend', this._fireWaypointEvent, this);
marker.off('drag', this._fireWaypointEvent, this);
marker.off('click', this._fireWaypointEvent, this);
},
clear: function() {
var drawEnabled = this._draw._enabled;
var current = this._waypoints._first;
this.draw(false);
if (current === null) {
return;
}
this._removeMarkerEvents(current);
while (current._routing.nextMarker) {
var marker = current._routing.nextMarker;
this._removeMarkerEvents(marker);
current = marker;
}
cb(err, data);
}, this));
}
,getSegments: function() {
var segments = [];
this._waypoints._first = null;
this._waypoints._last = null;
this._waypoints.clearLayers();
this._segments.clearLayers();
this._eachSegment(function(m1, m2, line) {
// omit if null (still calculating) or error
// NOTE: feature check specific to BRouter GeoJSON response, workaround to detect error line
if (line && line.feature) {
segments.push(line);
}
});
if (drawEnabled) {
this.draw(true);
}
},
return segments;
}
setWaypoints: function(latLngs, cb) {
var i;
var callbackCount = 0;
var firstErr;
var $this = this;
// add 'esc' to disable drawing
,_keyupListener: function (e) {
if (e.keyCode === 27) {
this._draw.disable();
} else {
L.Routing.prototype._keyupListener.call(this, e);
var callback = function(err, data) {
callbackCount++;
firstErr = firstErr || err;
if (callbackCount >= latLngs.length) {
$this.fire('routing:setWaypointsEnd', { err: firstErr });
if (cb) {
cb(firstErr);
}
}
};
this.fire('routing:setWaypointsStart');
for (i = 0; latLngs && i < latLngs.length; i++) {
this.addWaypoint(latLngs[i], this._waypoints._last, null, callback);
}
},
// patch to fix error when line is null or error line
// (when called while still segments to calculate, e.g. permalink or fast drawing)
toPolyline: function() {
var latLngs = [];
this._eachSegment(function(m1, m2, line) {
// omit if null (still calculating) or error
// NOTE: feature check specific to BRouter GeoJSON response, workaround to detect error line
if (line && line.feature) {
latLngs = latLngs.concat(line.getLatLngs());
}
});
return L.polyline(latLngs);
},
_routeSegment: function(m1, m2, cb) {
var loadingTrailer;
// change segment color before request to indicate recalculation (mark old)
if (m1 && m1._routing.nextLine !== null) {
m1._routing.nextLine.setStyle({ color: 'dimgray' });
}
// animate dashed trailer as loading indicator
if (m1 && m2) {
loadingTrailer = new L.Polyline([m1.getLatLng(), m2.getLatLng()], {
color: this.options.styles.track.color,
opacity: this.options.styles.trailer.opacity,
dashArray: [10, 10],
className: 'loading-trailer'
});
loadingTrailer.addTo(this._map);
}
L.Routing.prototype._routeSegment.call(
this,
m1,
m2,
L.bind(function(err, data) {
if (loadingTrailer) {
this._map.removeLayer(loadingTrailer);
}
cb(err, data);
}, this)
);
},
getSegments: function() {
var segments = [];
this._eachSegment(function(m1, m2, line) {
// omit if null (still calculating) or error
// NOTE: feature check specific to BRouter GeoJSON response, workaround to detect error line
if (line && line.feature) {
segments.push(line);
}
});
return segments;
},
_keyupListener: function(e) {
// Suppress shortcut handling when a text input field is focussed
if (
document.activeElement.type == 'text' ||
document.activeElement.type == 'textarea'
) {
return;
}
// add 'esc' to disable drawing
if (e.keyCode === 27) {
this._draw.disable();
} else {
L.Routing.prototype._keyupListener.call(this, e);
}
},
isDrawing: function() {
return this._draw._enabled;
},
reverse: function() {
var waypoints = this.getWaypoints();
waypoints.reverse();
this.clear();
this.setWaypoints(waypoints);
}
}
});

View file

@ -1,18 +1,14 @@
BR.Search = L.Control.Geocoder.extend({
options: {
geocoder: new L.Control.Geocoder.Nominatim({
serviceUrl: 'https://nominatim.openstreetmap.org/'
geocoder: new L.Control.Geocoder.LatLng({
next: new L.Control.Geocoder.Nominatim({
serviceUrl: 'https://nominatim.openstreetmap.org/'
}),
sizeInMeters: 800
}),
position: 'topleft'
},
onAdd: function (map) {
map.attributionControl.addAttribution(
'search by <a href="https://wiki.openstreetmap.org/wiki/Nominatim" target="_blank">Nominatim</a>');
return L.Control.Geocoder.prototype.onAdd.call(this, map);
},
markGeocode: function(result) {
this._map.fitBounds(result.geocode.bbox, {
maxZoom: 17

115
js/plugin/Sidebar.js Normal file
View file

@ -0,0 +1,115 @@
BR.Sidebar = L.Control.Sidebar.extend({
storageId: 'sidebar-control',
options: {
position: 'right',
container: 'sidebar',
tabContainer: 'sidebarTabs',
autopan: false,
defaultTabId: '',
// Tabs to be notified when shown or hidden
// (tab div id -> object implementing show/hide methods)
listeningTabs: {}
},
initialize: function(id, options) {
L.Control.Sidebar.prototype.initialize.call(this, id, options);
this.oldTab = null;
},
addTo: function(map) {
L.Control.Sidebar.prototype.addTo.call(this, map);
this.on('content', this._notifyOnContent, this);
this.on('closing', this._notifyOnClose, this);
this.on('toggleExpand', this._notifyOnResize, this);
this.on(
'closing',
function() {
this._map.getContainer().focus();
},
this
);
this._rememberTabState();
if (
L.Browser.touch &&
BR.Browser.touchScreenDetectable &&
!BR.Browser.touchScreen
) {
L.DomUtil.removeClass(this._container, 'leaflet-touch');
L.DomUtil.removeClass(this._tabContainer, 'leaflet-touch');
}
return this;
},
showPanel: function(id) {
var tab = this._getTab(id);
tab.hidden = false;
return this;
},
_rememberTabState: function() {
if (BR.Util.localStorageAvailable()) {
this.on('content closing', this._storeActiveTab, this);
var tabId = localStorage.getItem(this.storageId);
// 'true': legacy value for toggling old sidebar
if (tabId === 'true') {
tabId = this.options.defaultTabId;
} else if (tabId === null) {
// not set: closed by default for new users
tabId = '';
}
if (tabId !== '') {
this.open(tabId);
}
}
},
_notifyShow: function(tab) {
if (tab && tab.show) {
tab.show();
}
},
_notifyHide: function(tab) {
if (tab && tab.hide) {
tab.hide();
}
},
_notifyOnContent: function(e) {
var tab = this.options.listeningTabs[e.id];
this._notifyHide(this.oldTab);
this._notifyShow(tab);
this.oldTab = tab;
},
_notifyOnClose: function(e) {
this._notifyHide(this.oldTab);
this.oldTab = null;
},
_notifyOnResize: function(e) {
var tab = this.oldTab;
if (tab && tab.onResize) {
tab.onResize();
}
},
_storeActiveTab: function(e) {
localStorage.setItem(this.storageId, e.id || '');
}
});
BR.sidebar = function(divId, options) {
return new BR.Sidebar(divId, options);
};

View file

@ -1,8 +1,9 @@
(function(window) {
var HAS_HASHCHANGE = (function() {
var doc_mode = window.documentMode;
return ('onhashchange' in window) &&
(doc_mode === undefined || doc_mode > 7);
return (
'onhashchange' in window && (doc_mode === undefined || doc_mode > 7)
);
})();
L.Hash = function(map, options) {
@ -14,17 +15,17 @@
};
L.Hash.parseHash = function(hash) {
if(hash.indexOf('#map=') === 0) {
if (hash.indexOf('#map=') === 0) {
hash = hash.substr(5);
}
var args = hash.split(/\&(.+)/);
var mapsArgs = args[0].split("/");
var mapsArgs = args[0].split('/');
if (mapsArgs.length == 4) {
var zoom = parseInt(mapsArgs[0], 10),
lat = parseFloat(mapsArgs[1]),
lon = parseFloat(mapsArgs[2]),
layers = decodeURIComponent(mapsArgs[3]).split('-'),
additional = args[1];
lat = parseFloat(mapsArgs[1]),
lon = parseFloat(mapsArgs[2]),
layers = this.parseLayers(mapsArgs[3]),
additional = args[1];
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
return false;
} else {
@ -40,32 +41,19 @@
}
};
L.Hash.formatHash = function(map) {
(L.Hash.formatHash = function(map) {
var center = map.getCenter(),
zoom = map.getZoom(),
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2)),
layers = [];
layers = this.formatLayers();
//console.log(this.options);
var options = this.options;
//Check active layers
for(var key in options) {
if (options.hasOwnProperty(key)) {
if (map.hasLayer(options[key])) {
layers.push(key);
};
};
};
if (layers.length == 0) {
layers.push(Object.keys(options)[0]);
}
var params = [
zoom,
center.lat.toFixed(precision),
center.lng.toFixed(precision),
encodeURIComponent(layers.join("-"))
layers
];
url = "#map=" + params.join("/");
url = '#map=' + params.join('/');
if (this.additionalCb != null) {
var additional = this.additionalCb();
if (additional != null) {
@ -73,160 +61,244 @@
}
}
return url;
},
}),
(L.Hash.prototype = {
options: {
layerSeparator: ','
},
map: null,
lastHash: null,
L.Hash.prototype = {
map: null,
lastHash: null,
parseHash: L.Hash.parseHash,
formatHash: L.Hash.formatHash,
parseHash: L.Hash.parseHash,
formatHash: L.Hash.formatHash,
init: function(map, options) {
this.map = map;
L.Util.setOptions(this, options);
init: function(map, options) {
this.map = map;
L.Util.setOptions(this, options);
// reset the hash
this.lastHash = null;
this.onHashChange();
// reset the hash
this.lastHash = null;
this.onHashChange();
if (!this.isListening) {
this.startListening();
}
},
if (!this.isListening) {
this.startListening();
}
},
_parseLayers: function(layersParam, layerSeparator) {
var layers = layersParam.split(layerSeparator).map(
L.bind(function(layerEncoded) {
var obj = null;
var layerString = decodeURIComponent(layerEncoded);
removeFrom: function(map) {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (layerString) {
obj = this.options.layersControl.getLayerFromString(
layerString
);
}
if (this.isListening) {
this.stopListening();
}
return obj;
}, this)
);
this.map = null;
},
return layers;
},
onMapMove: function() {
// bail if we're moving the map (updating from a hash),
// or if the map is not yet loaded
parseLayers: function(layersParam) {
var countFoundLayers = function(count, obj) {
if (obj) {
count++;
}
return count;
};
if (this.movingMap || !this.map._loaded) {
return false;
}
var layers = this._parseLayers(
layersParam,
this.options.layerSeparator
);
var found = layers.reduce(countFoundLayers, 0);
var hash = this.formatHash(this.map);
if (this.lastHash != hash) {
location.replace(hash);
this.lastHash = hash;
}
},
if (found < layers.length) {
// legacy support for name instead of id and '-' layer separator
var layersLegacy = this._parseLayers(layersParam, '-');
var foundLegacy = layersLegacy.reduce(countFoundLayers, 0);
movingMap: false,
update: function() {
var hash = location.hash;
if (hash === this.lastHash) {
return;
}
var parsed = this.parseHash(hash);
if (!parsed) {
// migration from old hash style to new one
if (this.onInvalidHashChangeCb != null) {
var newHash = this.onInvalidHashChangeCb(hash);
if (newHash != null && newHash != hash) {
parsed = this.parseHash(newHash);
if (foundLegacy > found) {
layers = layersLegacy;
}
}
}
if (parsed) {
this.movingMap = true;
return layers;
},
this.map.setView(parsed.center, parsed.zoom);
var layers = parsed.layers,
options = this.options,
that = this;
//Add/remove layer
this.map.eachLayer(function(layer) {
for (alayer in that.layers) {
if (that.layers[alayer] == layer) {
that.map.removeLayer(layer);
break;
activateLayers: function(layers) {
var layersControl = this.options.layersControl;
var added = false;
layersControl.removeActiveLayers();
layers.forEach(
L.bind(function(obj, index, array) {
if (obj) {
layersControl.activateLayer(obj);
if (obj && !obj.overlay) {
added = true;
}
}
}, this)
);
if (!added) {
// if we couldn't add layers (removed or invalid name), add the default one
layersControl.activateDefaultBaseLayer();
}
},
formatLayers: function() {
var objList = this.options.layersControl.getActiveLayers();
var layerList = objList.map(
L.bind(function(obj) {
return encodeURIComponent(
this.options.layersControl.toLayerString(obj)
);
}, this)
);
return layerList.join(this.options.layerSeparator);
},
removeFrom: function(map) {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (this.isListening) {
this.stopListening();
}
this.map = null;
},
onMapMove: function() {
// bail if we're moving the map (updating from a hash),
// or if the map is not yet loaded
if (this.movingMap || !this.map._loaded) {
return false;
}
var hash = this.formatHash(this.map);
if (this.lastHash != hash) {
location.replace(hash);
this.lastHash = hash;
}
},
movingMap: false,
update: function() {
var hash = location.hash;
if (hash === this.lastHash) {
return;
}
var parsed = this.parseHash(hash);
if (!parsed) {
// migration from old hash style to new one
if (this.onInvalidHashChangeCb != null) {
var newHash = this.onInvalidHashChangeCb(hash);
if (newHash != null && newHash != hash) {
parsed = this.parseHash(newHash);
}
}
});
var added = false;
layers.forEach(function(element, index, array) {
if (element in options) {
added = true;
that.map.addLayer(options[element]);
}
if (parsed) {
this.movingMap = true;
this.map.setView(parsed.center, parsed.zoom);
this.activateLayers(parsed.layers);
if (this.onHashChangeCb != null) {
this.onHashChangeCb(parsed.additional);
}
});
if (!added) {
// if we couldn't add layers (custom ones or invalid name), add the default one
this.map.addLayer(options[Object.keys(options)[0]]);
this.movingMap = false;
} else {
this.onMapMove(this.map);
}
},
if (this.onHashChangeCb != null) {
this.onHashChangeCb(parsed.additional);
// defer hash change updates every 100ms
changeDefer: 100,
changeTimeout: null,
onHashChange: function() {
// throttle calls to update() so that they only happen every
// `changeDefer` ms
if (!this.changeTimeout) {
var that = this;
this.changeTimeout = setTimeout(function() {
that.update();
that.changeTimeout = null;
}, this.changeDefer);
}
},
this.movingMap = false;
} else {
this.onMapMove(this.map);
isListening: false,
hashChangeInterval: null,
startListening: function() {
this.map.on(
'moveend layeradd layerremove',
this.onMapMove,
this
);
if (HAS_HASHCHANGE) {
L.DomEvent.addListener(
window,
'hashchange',
this.onHashChange
);
} else {
clearInterval(this.hashChangeInterval);
this.hashChangeInterval = setInterval(
this.onHashChange,
50
);
}
this.isListening = true;
},
stopListening: function() {
this.map.off(
'moveend layeradd layerremove',
this.onMapMove,
this
);
if (HAS_HASHCHANGE) {
L.DomEvent.removeListener(
window,
'hashchange',
this.onHashChange
);
} else {
clearInterval(this.hashChangeInterval);
}
this.isListening = false;
},
_keyByValue: function(obj, value) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] === value) {
return key;
} else {
return null;
}
}
}
}
},
// defer hash change updates every 100ms
changeDefer: 100,
changeTimeout: null,
onHashChange: function() {
// throttle calls to update() so that they only happen every
// `changeDefer` ms
if (!this.changeTimeout) {
var that = this;
this.changeTimeout = setTimeout(function() {
that.update();
that.changeTimeout = null;
}, this.changeDefer);
}
},
isListening: false,
hashChangeInterval: null,
startListening: function() {
this.map.on("moveend layeradd layerremove", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
this.hashChangeInterval = setInterval(this.onHashChange, 50);
}
this.isListening = true;
},
stopListening: function() {
this.map.off("moveend layeradd layerremove", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
}
this.isListening = false;
},
_keyByValue: function(obj, value) {
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] === value) {
return key;
} else { return null; };
};
};
}
};
});
L.hash = function(map, options) {
return new L.Hash(map, options);
};

View file

@ -0,0 +1,31 @@
BR.stravaSegments = function(map, layersControl) {
const stravaControl = L.control
.stravaSegments({
runningTitle: i18next.t('map.strava-running'),
bikingTitle: i18next.t('map.strava-biking'),
loadingTitle: i18next.t('map.loading'),
stravaToken: BR.keys.strava
})
.addTo(map);
layersControl.addOverlay(
stravaControl.stravaLayer,
i18next.t('map.layer.strava-segments')
);
stravaControl.onError = function(err) {
BR.message.showError(
i18next.t('warning.strava-error', {
error: err && err.message ? err.message : err
})
);
};
// hide strava buttons when layer is inactive
var toggleStravaControl = function() {
var stravaBar = stravaControl.runningButton.button.parentElement;
stravaBar.hidden = !stravaBar.hidden;
};
toggleStravaControl();
stravaControl.stravaLayer.on('add remove', toggleStravaControl);
return stravaControl;
};

View file

@ -2,7 +2,8 @@ L.BRouter = L.Class.extend({
statics: {
// NOTE: the routing API used here is not public!
// /brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml
URL_TEMPLATE: '/brouter?lonlats={lonlats}&nogos={nogos}&profile={profile}&alternativeidx={alternativeidx}&format={format}',
URL_TEMPLATE:
'/brouter?lonlats={lonlats}&nogos={nogos}&polylines={polylines}&polygons={polygons}&profile={profile}&alternativeidx={alternativeidx}&format={format}',
URL_PROFILE_UPLOAD: BR.conf.host + '/brouter/profile',
PRECISION: 6,
NUMBER_SEPARATOR: ',',
@ -10,18 +11,20 @@ L.BRouter = L.Class.extend({
ABORTED_ERROR: 'aborted'
},
options: {
},
options: {},
initialize: function (options) {
initialize: function(options) {
L.setOptions(this, options);
this.queue = async.queue(L.bind(function (task, callback) {
this.getRoute(task.segment, callback);
}, this), 1);
this.queue = async.queue(
L.bind(function(task, callback) {
this.getRoute(task.segment, callback);
}, this),
1
);
// patch to call callbacks on kill for cleanup (loadingTrailer)
this.queue.kill = function () {
this.queue.kill = function() {
var aborted = this.tasks;
this.drain = null;
this.tasks = [];
@ -37,15 +40,32 @@ L.BRouter = L.Class.extend({
getUrlParams: function(latLngs, format) {
params = {};
if (this._getLonLatsString(latLngs) != null)
params.lonlats = this._getLonLatsString(latLngs);
if (this._getNogosString(this.options.nogos).length > 0)
if (
this.options.nogos &&
this._getNogosString(this.options.nogos).length > 0
)
params.nogos = this._getNogosString(this.options.nogos);
if (this.options.profile != null)
params.profile = this.options.profile;
if (
this.options.polylines &&
this._getNogosPolylinesString(this.options.polylines).length > 0
)
params.polylines = this._getNogosPolylinesString(
this.options.polylines
);
if (
this.options.polygons &&
this._getNogosPolygonsString(this.options.polygons).length > 0
)
params.polygons = this._getNogosPolygonsString(
this.options.polygons
);
if (this.options.profile != null) params.profile = this.options.profile;
params.alternativeidx = this.options.alternative;
@ -53,10 +73,16 @@ L.BRouter = L.Class.extend({
params.format = format;
} else {
// do not put values in URL if this is the default value (format===null)
if (params.profile === BR.conf.profiles[0])
if (params.profile === BR.conf.profiles[0]) delete params.profile;
if (params.alternativeidx == 0) delete params.alternativeidx;
// don't add custom profile, as these are only stored temporarily
if (
params.profile &&
params.profile.substring(0, 7) === 'custom_'
) {
delete params.profile;
if (params.alternativeidx == 0)
delete params.alternativeidx;
}
}
return params;
@ -70,6 +96,12 @@ L.BRouter = L.Class.extend({
if (params.nogos) {
opts.nogos = this._parseNogos(params.nogos);
}
if (params.polylines) {
opts.polylines = this._parseNogosPolylines(params.polylines);
}
if (params.polygons) {
opts.polygons = this._parseNogosPolygons(params.polygons);
}
if (params.alternativeidx) {
opts.alternative = params.alternativeidx;
}
@ -79,24 +111,39 @@ L.BRouter = L.Class.extend({
return opts;
},
getUrl: function(latLngs, format) {
getUrl: function(latLngs, format, trackname, exportWaypoints) {
var urlParams = this.getUrlParams(latLngs, format);
var args = []
var args = [];
if (urlParams.lonlats != null && urlParams.lonlats.length > 0)
args.push(L.Util.template('lonlats={lonlats}', urlParams));
if (urlParams.nogos != null)
args.push(L.Util.template('nogos={nogos}', urlParams));
if (urlParams.polylines != null)
args.push(L.Util.template('polylines={polylines}', urlParams));
if (urlParams.polygons != null)
args.push(L.Util.template('polygons={polygons}', urlParams));
if (urlParams.profile != null)
args.push(L.Util.template('profile={profile}', urlParams));
if (urlParams.alternativeidx != null)
args.push(L.Util.template('alternativeidx={alternativeidx}', urlParams));
args.push(
L.Util.template('alternativeidx={alternativeidx}', urlParams)
);
if (urlParams.format != null)
args.push(L.Util.template('format={format}', urlParams));
if (trackname)
args.push(
L.Util.template('trackname={trackname}', {
trackname: trackname
})
);
if (exportWaypoints) args.push('exportWaypoints=1');
var prepend_host = (format != null);
var prepend_host = format != null;
return (prepend_host ? BR.conf.host : '') + '/brouter?' + args.join('&');
return (
(prepend_host ? BR.conf.host : '') + '/brouter?' + args.join('&')
);
},
getRoute: function(latLngs, cb) {
@ -104,26 +151,32 @@ L.BRouter = L.Class.extend({
xhr = new XMLHttpRequest();
if (!url) {
return cb(new Error('Error getting route URL'));
return cb(new Error(i18next.t('warning.cannot-get-route')));
}
xhr.open('GET', url, true);
xhr.onload = L.bind(this._handleRouteResponse, this, xhr, cb);
xhr.onerror = L.bind(function(xhr, cb) {
cb(BR.Util.getError(xhr));
}, this, xhr, cb);
xhr.onerror = L.bind(
function(xhr, cb) {
cb(BR.Util.getError(xhr));
},
this,
xhr,
cb
);
xhr.send();
},
_handleRouteResponse: function(xhr, cb) {
var layer,
geojson;
if (xhr.status === 200
&& xhr.responseText
// application error when not GeoJSON format (text/plain for errors)
&& xhr.getResponseHeader('Content-Type').split(';')[0] === 'application/vnd.geo+json') {
var layer, geojson;
if (
xhr.status === 200 &&
xhr.responseText &&
// application error when not GeoJSON format (text/plain for errors)
xhr.getResponseHeader('Content-Type').split(';')[0] ===
'application/vnd.geo+json'
) {
// leaflet.spin
//gpxLayer.fire('data:loaded');
@ -132,7 +185,7 @@ L.BRouter = L.Class.extend({
layer = L.geoJSON(geojson).getLayers()[0];
return cb(null, layer);
} catch(e) {
} catch (e) {
console.error(e, xhr.responseText);
return cb(e);
}
@ -147,7 +200,7 @@ L.BRouter = L.Class.extend({
uploadProfile: function(profileId, profileText, cb) {
var url = L.BRouter.URL_PROFILE_UPLOAD;
xhr = new XMLHttpRequest();
xhr = new XMLHttpRequest();
// reuse existing profile file
if (profileId) {
@ -158,7 +211,7 @@ L.BRouter = L.Class.extend({
xhr.onload = L.bind(this._handleProfileResponse, this, xhr, cb);
xhr.onerror = function(evt) {
var xhr = this;
cb('Upload error: ' + xhr.statusText);
cb(i18next.t('warning.upload-error', { error: xhr.statusText }));
};
// send profile text only, as text/plain;charset=UTF-8
@ -168,11 +221,15 @@ L.BRouter = L.Class.extend({
_handleProfileResponse: function(xhr, cb) {
var response;
if (xhr.status === 200 && xhr.responseText && xhr.responseText.length > 0) {
if (
xhr.status === 200 &&
xhr.responseText &&
xhr.responseText.length > 0
) {
response = JSON.parse(xhr.responseText);
cb(response.error, response.profileid);
} else {
cb('Profile error: no or empty response from server');
cb(i18next.t('warning.profile-error'));
}
},
@ -180,7 +237,7 @@ L.BRouter = L.Class.extend({
var s = '';
for (var i = 0; i < latLngs.length; i++) {
s += this._formatLatLng(latLngs[i]);
if (i < (latLngs.length - 1)) {
if (i < latLngs.length - 1) {
s += L.BRouter.GROUP_SEPARATOR;
}
}
@ -213,7 +270,16 @@ L.BRouter = L.Class.extend({
s += this._formatLatLng(circle.getLatLng());
s += L.BRouter.NUMBER_SEPARATOR;
s += Math.round(circle.getRadius());
if (i < (nogos.length - 1)) {
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
if (
circle.options.nogoWeight !== undefined &&
circle.options.nogoWeight !== null &&
circle.options.nogoWeight !== -1
) {
s += L.BRouter.NUMBER_SEPARATOR;
s += circle.options.nogoWeight;
}
if (i < nogos.length - 1) {
s += L.BRouter.GROUP_SEPARATOR;
}
}
@ -231,26 +297,137 @@ L.BRouter = L.Class.extend({
groups = s.split(L.BRouter.GROUP_SEPARATOR);
for (var i = 0; i < groups.length; i++) {
// lng,lat,radius
// lng,lat,radius(,weight)
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
// TODO refactor: pass simple obj, create circle in NogoAreas; use shapeOptions of instance
// [lat,lng],radius
nogos.push(L.circle([numbers[1], numbers[0]], {radius: numbers[2]}));
// Parse as a nogo circle
var nogoOptions = { radius: numbers[2] };
if (numbers.length > 3) {
nogoOptions.nogoWeight = numbers[3];
}
nogos.push(L.circle([numbers[1], numbers[0]], nogoOptions));
}
return nogos;
},
_getNogosPolylinesString: function(nogos) {
var s = '';
for (var i = 0, polyline, vertices; i < nogos.length; i++) {
polyline = nogos[i];
vertices = polyline.getLatLngs();
for (var j = 0; j < vertices.length; j++) {
if (j > 0) {
s += L.BRouter.NUMBER_SEPARATOR;
}
s += this._formatLatLng(vertices[j]);
}
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
if (
polyline.options.nogoWeight !== undefined &&
polyline.options.nogoWeight !== null &&
polyline.options.nogoWeight !== -1
) {
s += L.BRouter.NUMBER_SEPARATOR;
s += polyline.options.nogoWeight;
}
if (i < nogos.length - 1) {
s += L.BRouter.GROUP_SEPARATOR;
}
}
return s;
},
_parseNogosPolylines: function(s) {
var groups,
numbers,
latlngs,
nogos = [];
groups = s.split(L.BRouter.GROUP_SEPARATOR);
for (var i = 0; i < groups.length; i++) {
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
if (numbers.length > 1) {
latlngs = [];
for (var j = 0; j < numbers.length - 1; ) {
var lng = Number.parseFloat(numbers[j++]);
var lat = Number.parseFloat(numbers[j++]);
latlngs.push([lat, lng]);
}
var nogoWeight;
if (j < numbers.length) {
nogoWeight = Number.parseFloat(numbers[j++]);
}
nogos.push(L.polyline(latlngs, { nogoWeight: nogoWeight }));
}
}
return nogos;
},
_getNogosPolygonsString: function(nogos) {
var s = '';
for (var i = 0, polygon, vertices; i < nogos.length; i++) {
polygon = nogos[i];
vertices = polygon.getLatLngs()[0];
for (var j = 0; j < vertices.length; j++) {
if (j > 0) {
s += L.BRouter.NUMBER_SEPARATOR;
}
s += this._formatLatLng(vertices[j]);
}
// -1 is default nogo exclusion, it should not be passed as a URL parameter.
if (
polygon.options.nogoWeight !== undefined &&
polygon.options.nogoWeight !== null &&
polygon.options.nogoWeight !== -1
) {
s += L.BRouter.NUMBER_SEPARATOR;
s += polygon.options.nogoWeight;
}
if (i < nogos.length - 1) {
s += L.BRouter.GROUP_SEPARATOR;
}
}
return s;
},
_parseNogosPolygons: function(s) {
var groups,
numbers,
latlngs,
nogos = [];
groups = s.split(L.BRouter.GROUP_SEPARATOR);
for (var i = 0; i < groups.length; i++) {
numbers = groups[i].split(L.BRouter.NUMBER_SEPARATOR);
if (numbers.length > 1) {
latlngs = [];
for (var j = 0; j < numbers.length - 1; ) {
var lng = Number.parseFloat(numbers[j++]);
var lat = Number.parseFloat(numbers[j++]);
latlngs.push([lat, lng]);
}
var nogoWeight;
if (j < numbers.length) {
nogoWeight = Number.parseFloat(numbers[j++]);
}
nogos.push(L.polygon(latlngs, { nogoWeight: nogoWeight }));
}
}
return nogos;
},
// formats L.LatLng object as lng,lat string
_formatLatLng: function(latLng) {
var s = '';
s += L.Util.formatNum(latLng.lng, L.BRouter.PRECISION);
s += L.Util.formatNum(latLng.lng || latLng[1], L.BRouter.PRECISION);
s += L.BRouter.NUMBER_SEPARATOR;
s += L.Util.formatNum(latLng.lat, L.BRouter.PRECISION);
s += L.Util.formatNum(latLng.lat || latLng[0], L.BRouter.PRECISION);
return s;
}
});
L.bRouter = function (options) {
L.bRouter = function(options) {
return new L.BRouter(options);
};
};

View file

@ -21,9 +21,8 @@ var brouterCgi = (function() {
//return 'test/test.gpx';
return url;
}
return {
getUrl: getUrl
}
})();
};
})();