BR.CircleGoArea = L.Control.extend({ radius: null, // in meters circleLayer: null, boundaryLayer: null, maskRenderer: L.svg({ padding: 2 }), countries: null, countriesMask: null, states: null, statesLoading: false, options: { countriesUrl: BR.conf.countriesUrl || 'dist/boundaries/countries.topo.json', statesUrl: BR.conf.statesUrl || 'dist/boundaries/germany-states.topo.json', overpassDataUrl: (BR.conf.overpassBaseUrl || 'https://overpass-api.de/api/interpreter') + '?data=', shortcut: { draw: { enable: 73, // char code for 'i' disable: 27, // char code for 'ESC' }, }, }, initialize: function (routing, nogos, pois, options) { this.routing = routing; this.nogos = nogos; this.pois = pois; L.setOptions(this, options); }, onAdd: function (map) { var self = this; this.map = map; this.circleLayer = L.layerGroup([]).addTo(map); this.drawButton = L.easyButton({ states: [ { stateName: 'activate-circlego', icon: 'fa-circle-o', onClick: function () { self.draw(true); }, title: i18next.t('keyboard.generic-shortcut', { action: i18next.t('map.draw-circlego-start'), key: 'I', }), }, { stateName: 'deactivate-circlego', icon: 'fa-circle-o active', onClick: function () { self.draw(false); }, title: i18next.t('keyboard.generic-shortcut', { action: i18next.t('map.draw-circlego-stop'), key: '$t(keyboard.escape)', }), }, ], }); 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 () { self.draw(false); }); L.DomEvent.addListener(document, 'keydown', this._keydownListener, this); var container = new L.DomUtil.create('div'); return container; }, draw: function (enable) { this.drawButton.state(enable ? 'deactivate-circlego' : 'activate-circlego'); if (enable) { this.routing.draw(false); this.pois.draw(false); this.map.on('click', this.onMapClick, this); if (this.countriesMask) { this.map.addLayer(this.countriesMask); } this._unlockOutsideArea(); L.DomUtil.addClass(this.map.getContainer(), 'circlego-draw-enabled'); } else { this.map.off('click', this.onMapClick, this); if (this.countriesMask && this.map.hasLayer(this.countriesMask)) { this.map.removeLayer(this.countriesMask); } this._lockOutsideArea(); L.DomUtil.removeClass(this.map.getContainer(), 'circlego-draw-enabled'); } }, _keydownListener: function (e) { if (!BR.Util.keyboardShortcutsAllowed(e)) { return; } if (e.keyCode === this.options.shortcut.draw.disable) { this.draw(false); } else if (e.keyCode === this.options.shortcut.draw.enable) { this.draw(true); } }, _getBoundary: function (center, adminLevel, adminLevelFallback) { adminLevel = adminLevel || 8; var query = '[out:json]; is_in(' + center[1] + ', ' + center[0] + ')->.a;' + '(area.a[admin_level="' + adminLevel + '"];)->.p; relation(pivot.p); out geom;'; if (adminLevelFallback) { // order is important (separate out), so fallback entry always comes last in result query += '(area.a[admin_level="' + adminLevelFallback + '"];)->.p; relation(pivot.p); out geom;'; } var url = this.options.overpassDataUrl + encodeURIComponent(query); this.marker.setIcon(this.iconSpinner); BR.Util.getJson( url, '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) this._getBoundary(center, 6); } else { BR.message.showError('No boundary found for coordinate "' + center + '"'); } return; } var geoJson = osmtogeojson(osmJson); this._setBoundary(geoJson); } this.marker.setIcon(this.icon); }, this) ); }, _setBoundary: function (geoJson) { // drop admin_centre nodes geoJson.features = geoJson.features.filter(function (feature) { return feature.geometry.type !== 'Point'; }); var boundaryLine = turf.polygonToLine(geoJson.features[0]); this.boundaryLayer = L.geoJson(boundaryLine, { style: function (feature) { return { weight: 1, color: 'black', opacity: 0.6, interactive: false, }; }, }).addTo(this.map); var buffer = turf.buffer(geoJson, this.radius, { units: 'meters' }); var ring = turf.polygonToLine(buffer.features[0]); if (ring.type !== 'FeatureCollection') { ring = turf.featureCollection([ring]); } /* hack: it seems there is a bug when using a single closed ring line, cf. https://github.com/nrenner/brouter-web/issues/349#issue-755514458 so instead cut into smaller chunks of about half the circumference that worked for circles */ var split = turf.lineChunk(ring, 62, { units: 'kilometers' }); this._setNogo(split); this.setOutsideArea(ring); }, _getPolygonForPoint: function (center, featureCollection) { var polygon = null; var point = turf.point(center); var features = featureCollection.features; for (var i = 0; i < features.length; i++) { var feature = features[i]; var inside = turf.booleanPointInPolygon(point, feature); if (inside) { polygon = feature; break; } } return polygon; }, _getState: function (center) { return this._getPolygonForPoint(center, this.states); }, _getCountry: function (center) { return this._getPolygonForPoint(center, this.countries); }, _applyStateRules: function (center) { var state = this._getState(center); if (state) { var ref = state.properties.all_tags.ref; if (['MV', 'NDS', 'SL', 'SN'].indexOf(ref) !== -1) { // Address this._setNogoCircle(center); } else if (ref === 'ST') { // admin_level 7 + 8 (Gemeinde oder Verbandsgemeinde) // Also select AL 8 (Gemeinde) as fallback where there is no AL 7. // AL 7 first, so we can always take first entry. this._getBoundary(center, 7, 8); } else if (ref === 'BB') { // admin_level 6 (Landkreis oder kreisfreie Stadt) this._getBoundary(center, 6); } else if (ref === 'HH' || state.properties.id === -62422) { // admin_level 4 - (Bundesländer und Stadtstaaten) // Hamburg and Berlin (AL 4) included in states file (Berlin ref missing) this._setBoundary(turf.featureCollection([state])); } else { console.error('unhandled state: ' + ref + ', id: ' + state.properties.id); this._getBoundary(center); } } else { // admin_level 8 (Gemeinde) this._getBoundary(center); } }, _applyCountryRules: function (center) { var country = this._getCountry(center); if (country) { var name = country.properties.name; if (name === 'Germany') { this.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.radius = 10000; this._setNogoCircle(center); } else { console.error('unhandled country: ' + name); this.radius = null; } } else { // NOOP, no rules implemented for this location this.radius = null; } }, // debugging _logStates: function (states) { for (var i = 0; i < states.features.length; i++) { var state = states.features[i]; console.log( state.properties.all_tags.ref + ', ' + state.properties.all_tags.name + ', ' + state.properties.id ); } }, // debugging _addGeoJsonLayer: function (states, options) { // delay, otherwise triggers premature hash update through mapmove setTimeout( L.bind(function () { L.geoJson(states, { style: function (feature) { return L.extend( { weight: 1, color: 'navy', opacity: 0.8, fill: false, interactive: false, }, options ); }, }).addTo(this.map); }, this), 100 ); }, _loadStates: function () { if (this.statesLoading) return; 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._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) ); }, _setNogo: function (ring) { this.nogoPolylines = L.geoJson(ring, BR.NogoAreas.prototype.polylineOptions); this.nogos.addNogos(null, this.nogoPolylines.getLayers(), null); }, _removeNogo: function () { if (this.nogoPolylines) { this.nogos.removeNogos(null, this.nogoPolylines.getLayers(), null); this.nogoPolylines = null; } }, _setNogoCircle: function (center) { var polygon = this.circleToPolygon(center, this.radius); this._setNogo(polygon); this.setOutsideArea(polygon); }, setNogoRing: function (center) { this._clearLayers(); this._removeNogo(); if (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._applyCountryRules(center); } } }, _lockOutsideArea: function () { if (this.outsideArea) { this.outsideArea.eachLayer(function (layer) { layer._path.classList.add('circlego-outside'); }); this.outsideArea.on('click', L.DomEvent.stop); } }, _unlockOutsideArea: function () { if (this.outsideArea) { this.outsideArea.eachLayer(function (layer) { layer._path.classList.remove('circlego-outside'); }); this.outsideArea.off('click', L.DomEvent.stop); } }, setOutsideArea: function (ring) { var mask = turf.mask(turf.polygonize(ring)); this.outsideArea = L.geoJson(mask, { renderer: this.maskRenderer, style: function (feature) { return { weight: 4, color: 'black', opacity: 0.4, fillColor: 'black', fillOpacity: 0.4, }; }, smoothFactor: 0.5, }).addTo(this.map); this._lockOutsideArea(); }, onMapClick: function (e) { this.setCircle([e.latlng.lng, e.latlng.lat]); }, setOptions: function (opts) { this.radius = opts.circlego[2]; if (opts.polylines) { this.nogoPolylines = L.featureGroup(opts.polylines, BR.NogoAreas.prototype.polylineOptions); } this.setCircle([opts.circlego[0], opts.circlego[1]], opts.polylines); }, setCircle: function (center, polylines) { var marker = (this.marker = this._createMarker(center)); this.clear(); marker.addTo(this.circleLayer); // prevent editing (when called by hash) until countries are loaded, see _loadCountries call in onAdd if (!this.countries) { marker.dragging.disable(); } if (!polylines) { this.setNogoRing(center); } else { var features = []; for (var i = 0; i < polylines.length; i++) { features.push(polylines[i].toGeoJSON()); } var ring = turf.featureCollection(features); this.setOutsideArea(ring); } this.draw(false); }, _createMarker: function (center) { var self = this; var icon = (this.icon = L.VectorMarkers.icon({ icon: 'home', markerColor: BR.conf.markerColors.circlego, })); this.iconSpinner = L.VectorMarkers.icon({ icon: 'spinner', spin: true, markerColor: BR.conf.markerColors.circlego, }); var popupContent = ''; var marker = L.marker([center[1], center[0]], { icon: icon, draggable: true, // prevent being on top of route markers zIndexOffset: -500, }) .bindPopup(popupContent) .on('dragend', function (e) { self.setNogoRing([e.target.getLatLng().lng, e.target.getLatLng().lat]); }) .on('click', function () { var drawing = self.drawButton.state() == 'deactivate-circlego'; if (drawing) { self.circleLayer.removeLayer(marker); self.setNogoRing(undefined); } }) .on( 'popupopen', function (evt) { this._onPopupOpen(evt.popup, popupContent); }, this ) .on('popupclose', this._onPopupClose, this); return marker; }, _onPopupOpen: function (popup, popupContent) { var exportName = ''; var html = '
';
if (this.radius) {
if (this.boundaryLayer) {
var name = this.boundaryLayer.getLayers()[0].feature.properties.name;
exportName += name + ' + ';
html += BR.Util.sanitizeHTMLContent(name) + '
+ ';
}
var radiusText = (this.radius / 1000).toFixed();
exportName += radiusText + ' km';
html += radiusText + ' km';
if (this.nogoPolylines) {
html += '
';
html +=
'' +
i18next.t('export.format_gpx') +
'';
html += '
';
html +=
'' +
i18next.t('export.format_geojson') +
'';
}
} else {
html += i18next.t('map.not-applicable-here');
}
html += '