Merge pull request #362 from nrenner/ringgo-patch

Use boundaries to determine country rules on click
This commit is contained in:
Norbert Renner 2021-01-23 21:47:10 +01:00 committed by GitHub
commit d33413d00e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 220 additions and 104 deletions

View file

@ -79,7 +79,7 @@ var paths = {
'layers/config/geometry.js',
],
layersConfigDestName: 'layersConf.js',
boundaries: 'resources/boundaries/*.geojson',
boundaries: ['resources/boundaries/*.geojson', 'resources/boundaries/*.topo.json'],
zip: ['dist/**', 'index.html', 'config.template.js', 'keys.template.js'],
dest: 'dist',
destName: 'brouter-web',

View file

@ -30,6 +30,38 @@ BR.Util = {
return new Error(msg);
},
getJson: function (url, context, cb) {
BR.Util.get(url, function (err, data) {
if (err) {
BR.message.showError('Error getting ' + context + ': ' + err);
return cb(err);
}
try {
var json = JSON.parse(data);
cb(null, json);
} catch (err) {
BR.message.showError('Error parsing ' + context + ': ' + err);
console.error(err);
cb(err);
}
});
},
getGeoJson: function (url, context, cb) {
BR.Util.getJson(url, context, function (err, data) {
if (err) return cb(err);
var geoJson = data;
if (data && data.type && data.type === 'Topology') {
var key = Object.keys(data.objects)[0];
geoJson = topojson.feature(data, data.objects[key]);
}
cb(null, geoJson);
});
},
// check if localStorage is available, especially for catching SecurityError
// when cookie settings are blocking access (Chrome, Pale Moon, older Firefox)
//
@ -118,6 +150,10 @@ BR.Util = {
}
// fallback when country not available
return lang[0] === language;
if (language) {
return lang[0] === language;
}
return false;
},
};

View file

@ -1,13 +1,16 @@
BR.CircleGoArea = L.Control.extend({
circleLayer: null,
boundaryLayer: null,
outsideAreaRenderer: L.svg({ padding: 1 }),
maskRenderer: L.svg({ padding: 2 }),
countries: null,
countriesMask: null,
states: null,
statesLoading: false,
options: {
radius: 1000, // in meters
stateRules: false,
statesUrl: BR.conf.statesUrl || 'dist/boundaries/germany-states.geojson',
countriesUrl: BR.conf.countriesUrl || 'dist/boundaries/countries.topo.json',
statesUrl: BR.conf.statesUrl || 'dist/boundaries/germany-states.topo.json',
overpassBaseUrl: BR.conf.overpassBaseUrl || 'https://overpass-api.de/api/interpreter?data=',
shortcut: {
draw: {
@ -58,16 +61,22 @@ BR.CircleGoArea = L.Control.extend({
],
});
if (this.options.stateRules) {
this.drawButton.disable();
this._loadStates(
L.bind(function () {
this.drawButton.enable();
if (this.marker && !this.marker.dragging.enabled()) {
this.marker.dragging.enable();
}
}, this)
);
this.drawButton.disable();
this.once(
'countries:loaded',
function () {
this.drawButton.enable();
if (this.marker && !this.marker.dragging.enabled()) {
this.marker.dragging.enable();
}
},
this
);
this._loadCountries();
// preload states in parallel, before clicked country is known, using browser language as indicator
if (BR.Util.isCountry('DE')) {
this._loadStates();
}
map.on('routing:draw-start', function () {
@ -86,10 +95,12 @@ BR.CircleGoArea = L.Control.extend({
this.routing.draw(false);
this.pois.draw(false);
this.map.on('click', this.onMapClick, this);
this.map.addLayer(this.countriesMask);
this._unlockOutsideArea();
L.DomUtil.addClass(this.map.getContainer(), 'circlego-draw-enabled');
} else {
this.map.off('click', this.onMapClick, this);
this.map.removeLayer(this.countriesMask);
this._lockOutsideArea();
L.DomUtil.removeClass(this.map.getContainer(), 'circlego-draw-enabled');
}
@ -126,17 +137,11 @@ BR.CircleGoArea = L.Control.extend({
var url = this.options.overpassBaseUrl + encodeURIComponent(query);
this.marker.setIcon(this.iconSpinner);
BR.Util.get(
BR.Util.getJson(
url,
L.bind(function (err, data) {
if (err) {
BR.message.showError('Error getting boundary for coordinate "' + center + '": ' + err);
return;
}
try {
var osmJson = JSON.parse(data);
'boundary for coordinate "' + center + '"',
L.bind(function (err, osmJson) {
if (!err) {
if (osmJson.elements.length === 0) {
if (adminLevel === 8 || adminLevel === 7) {
// admin_level 6 (kreisfreie Stadt)
@ -150,14 +155,9 @@ BR.CircleGoArea = L.Control.extend({
var geoJson = osmtogeojson(osmJson);
this._setBoundary(geoJson);
this.marker.setIcon(this.icon);
} catch (err) {
BR.message.showError('Error parsing boundary: ' + err);
console.error(err);
} finally {
this.marker.setIcon(this.icon);
}
this.marker.setIcon(this.icon);
}, this)
);
},
@ -196,21 +196,29 @@ BR.CircleGoArea = L.Control.extend({
this.setOutsideArea(ring);
},
_getState: function (center) {
var state = null;
_getPolygonForPoint: function (center, featureCollection) {
var polygon = null;
var point = turf.point(center);
var features = this.states.features;
var features = featureCollection.features;
for (var i = 0; i < features.length; i++) {
var feature = features[i];
var inside = turf.booleanPointInPolygon(point, feature);
if (inside) {
state = feature;
polygon = feature;
break;
}
}
return state;
return polygon;
},
_getState: function (center) {
return this._getPolygonForPoint(center, this.states);
},
_getCountry: function (center) {
return this._getPolygonForPoint(center, this.countries);
},
_applyStateRules: function (center) {
@ -244,6 +252,42 @@ BR.CircleGoArea = L.Control.extend({
}
},
_applyCountryRules: function (center) {
var country = this._getCountry(center);
if (country) {
var name = country.properties.name;
if (name === 'Germany') {
this.options.radius = 15000;
if (!this.states) {
this.marker.setIcon(this.iconSpinner);
this.once(
'states:loaded',
function () {
this.marker.setIcon(this.icon);
if (this.states) {
this._applyStateRules(center);
}
},
this
);
this._loadStates();
} else {
this._applyStateRules(center);
}
} else if (name === 'Metropolitan France') {
this.options.radius = 20000;
this._setNogoCircle(center);
} else {
console.error('unhandled country: ' + name);
}
} else {
// NOOP, no rules implemented for this location
}
},
// debugging
_logStates: function (states) {
for (var i = 0; i < states.features.length; i++) {
@ -255,19 +299,22 @@ BR.CircleGoArea = L.Control.extend({
},
// debugging
_addStatesLayer: function (states) {
_addGeoJsonLayer: function (states, options) {
// delay, otherwise triggers premature hash update through mapmove
setTimeout(
L.bind(function () {
L.geoJson(states, {
style: function (feature) {
return {
weight: 1,
color: 'navy',
opacity: 0.8,
fill: false,
interactive: false,
};
return L.extend(
{
weight: 1,
color: 'navy',
opacity: 0.8,
fill: false,
interactive: false,
},
options
);
},
}).addTo(this.map);
}, this),
@ -275,29 +322,63 @@ BR.CircleGoArea = L.Control.extend({
);
},
_loadStates: function (cb) {
BR.Util.get(
this.options.statesUrl,
L.bind(function (err, data) {
if (err) {
BR.message.showError('Error getting states: ' + err);
return;
}
_loadStates: function () {
if (this.statesLoading) return;
try {
this.states = JSON.parse(data);
this.statesLoading = true;
BR.Util.getGeoJson(
this.options.statesUrl,
'states',
L.bind(function (err, data) {
if (!err) {
this.states = data;
// debugging
//this._logStates(this.states);
//this._addStatesLayer(this.states);
this.fire('states:loaded');
cb();
} catch (err) {
BR.message.showError('Error parsing states: ' + err);
console.error(err);
//this._addGeoJsonLayer(this.states);
}
this.statesLoading = false;
this.fire('states:loaded');
}, this)
);
},
_loadCountries: function () {
BR.Util.getJson(
this.options.countriesUrl,
'countries',
L.bind(function (err, data) {
if (err) return;
var key = Object.keys(data.objects)[0];
this.countries = topojson.feature(data, data.objects[key]);
var union = topojson.merge(data, [data.objects[key]]);
this.countriesMask = L.geoJson(union, {
renderer: this.maskRenderer,
// use Leaflet.snogylop plugin here, turf.mask too slow (~4s) for some reason
invert: true,
style: function (feature) {
return {
weight: 1,
color: 'darkgreen',
opacity: 0.8,
fillColor: '#020',
fillOpacity: 0.2,
className: 'circlego-outside',
};
},
});
this.countriesMask.on('click', L.DomEvent.stop);
this.countriesMask.bindTooltip(i18next.t('map.not-applicable-here'), {
sticky: true,
offset: [10, 0],
direction: 'right',
opacity: 0.8,
});
this.fire('countries:loaded');
}, this)
);
},
@ -325,23 +406,19 @@ BR.CircleGoArea = L.Control.extend({
this._removeNogo();
if (center) {
if (this.options.stateRules) {
if (!this.states) {
// wait for states to be loaded (when circlego hash parameter without polylines)
this.marker.setIcon(this.iconSpinner);
this.once(
'states:loaded',
function () {
this._applyStateRules(center);
this.marker.setIcon(this.icon);
},
this
);
} else {
this._applyStateRules(center);
}
if (!this.countries) {
// wait for countries to be loaded (when circlego hash parameter without polylines)
this.marker.setIcon(this.iconSpinner);
this.once(
'countries:loaded',
function () {
this.marker.setIcon(this.icon);
this._applyCountryRules(center);
},
this
);
} else {
this._setNogoCircle(center);
this._applyCountryRules(center);
}
}
},
@ -368,7 +445,7 @@ BR.CircleGoArea = L.Control.extend({
var mask = turf.mask(turf.polygonize(ring));
this.outsideArea = L.geoJson(mask, {
renderer: this.outsideAreaRenderer,
renderer: this.maskRenderer,
style: function (feature) {
return {
weight: 4,
@ -427,8 +504,8 @@ BR.CircleGoArea = L.Control.extend({
this.clear();
marker.addTo(this.circleLayer);
if (this.options.stateRules && !this.states) {
// prevent editing (when called by hash) until states are loaded, see _loadStates call in onAdd
// prevent editing (when called by hash) until countries are loaded, see _loadCountries call in onAdd
if (!this.countries) {
marker.dragging.disable();
}
@ -538,19 +615,5 @@ BR.CircleGoArea = L.Control.extend({
BR.CircleGoArea.include(L.Evented.prototype);
BR.circleGoArea = function (routing, nogos, pois) {
var circleGo = null;
var options = {};
if (BR.Util.isCountry('FR', 'fr')) {
options.radius = 20000;
} else if (BR.Util.isCountry('DE', 'de')) {
options.radius = 15000;
options.stateRules = true;
}
if (options) {
circleGo = new BR.CircleGoArea(routing, nogos, pois, options);
}
return circleGo;
return new BR.CircleGoArea(routing, nogos, pois);
};

View file

@ -96,8 +96,8 @@
"delete-nogo-areas": "Delete all no-go areas",
"delete-pois": "Delete all points of interest",
"delete-route": "Delete route",
"draw-circlego-start": "Draw limited {{radius}}km go-to zone",
"draw-circlego-stop": "Stop drawing limited {{radius}}km go-to zone",
"draw-circlego-start": "Draw limited go-to zone",
"draw-circlego-stop": "Stop drawing limited go-to zone",
"draw-poi-start": "Draw points of interest",
"draw-poi-stop": "Stop drawing points of interest",
"draw-route-start": "Draw route",
@ -132,6 +132,7 @@
"edit": "Click to edit",
"help": "&square; = move / resize, <span class=\"fa fa-trash-o\"></span> = delete,<br>click circle to quit editing"
},
"not-applicable-here": "not applicable here",
"opacity-slider": "Set transparency of route track and markers",
"opacity-slider-shortcut": "{{action}}\n(Hold {{key}} key to mute temporarily)",
"preview": "Preview",

View file

@ -74,6 +74,7 @@
"mapbbcode": "MapBBCode/mapbbcode#v1.2.0",
"osmtogeojson": "^3.0.0-beta.4",
"promise-polyfill": "^8.2.0",
"topojson-client": "^3.1.0",
"url-search-params": "~0.5.0",
"whatwg-fetch": "^3.5.0"
},

View file

@ -1,11 +1,25 @@
# boundaries
## germany-states.geojson
Downloaded from https://osm-boundaries.com.
TopoJSON (https://github.com/topojson/topojson) used to convert to topology, simplify and reduce precision:
`npm install -g topojson`
## germany-states
Currently only containing states that do not use the municipality boundary for the Corona 15 km allowed zone rule.
Downloaded from https://osm-boundaries.com with:
```
curl --remote-name --remote-header-name --location --max-redirs -1 "https://osm-boundaries.com/Download/Submit?apiKey=YOUR_API_KEY&db=osm20201109&osmIds=-28322,-62771,-62372,-62467,-62607,-62422,-62782,-62504&includeAllTags&simplify=0.0001"
curl --remote-name --remote-header-name --location --max-redirs -1 "https://osm-boundaries.com/Download/Submit?apiKey=YOUR_API_KEY&db=osm20201109&osmIds=-28322,-62771,-62372,-62467,-62607,-62422,-62782,-62504&includeAllTags"
geo2topo germany-states.geojson | toposimplify -s 3e-12 | topoquantize 1e6 > germany-states.topo.json
```
## countries
```
curl --remote-name --remote-header-name --location --max-redirs -1 "https://osm-boundaries.com/Download/Submit?apiKey=YOUR_API_KEY&db=osm20201109&osmIds=-51477,-1403916"
geo2topo countries.geojson | toposimplify -s 3e-12 | topoquantize 1e6 > countries.topo.json
```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8173,7 +8173,7 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
topojson-client@3:
topojson-client@3, topojson-client@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==