forked from MapComplete/MapComplete
More refactoring, stabilizing rotation and direction_gradient
This commit is contained in:
parent
5fec108ba2
commit
778044d0fb
45 changed files with 656 additions and 640 deletions
|
@ -5,7 +5,7 @@ import {Utils} from "../../Utils";
|
|||
import Svg from "../../Svg";
|
||||
import Img from "../../UI/Base/Img";
|
||||
|
||||
export class GeoLocationHandler extends UIElement {
|
||||
export default class GeoLocationHandler extends UIElement {
|
||||
|
||||
private readonly _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _permission: UIEventSource<string> = new UIEventSource<string>("");
|
||||
|
@ -13,17 +13,14 @@ export class GeoLocationHandler extends UIElement {
|
|||
private readonly _hasLocation: UIEventSource<boolean>;
|
||||
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
|
||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||
private readonly _featureSwitch: UIEventSource<boolean>;
|
||||
|
||||
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
featureSwitch: UIEventSource<boolean>) {
|
||||
leafletMap: UIEventSource<L.Map>) {
|
||||
super(undefined);
|
||||
this._currentGPSLocation = currentGPSLocation;
|
||||
this._leafletMap = leafletMap;
|
||||
this._featureSwitch = featureSwitch;
|
||||
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
||||
var self = this;
|
||||
const self = this;
|
||||
import("../../vendor/Leaflet.AccuratePosition.js").then(() => {
|
||||
self.init();
|
||||
})
|
||||
|
@ -92,10 +89,6 @@ export class GeoLocationHandler extends UIElement {
|
|||
}
|
||||
|
||||
InnerRender(): string {
|
||||
if (!this._featureSwitch.data) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this._hasLocation.data) {
|
||||
return Svg.crosshair_blue_img;
|
||||
}
|
||||
|
@ -124,7 +117,7 @@ export class GeoLocationHandler extends UIElement {
|
|||
|
||||
private StartGeolocating(zoomlevel = 19) {
|
||||
const self = this;
|
||||
const map : any = this._leafletMap.data;
|
||||
const map: any = this._leafletMap.data;
|
||||
if (self._permission.data === "denied") {
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -8,14 +8,14 @@ import Img from "../../UI/Base/Img";
|
|||
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
||||
* Shows the given uiToShow-element in the messagebox
|
||||
*/
|
||||
export class StrayClickHandler {
|
||||
export default class StrayClickHandler {
|
||||
private _lastMarker;
|
||||
private _uiToShow: (() => UIElement);
|
||||
|
||||
constructor(
|
||||
lastClickLocation: UIEventSource<{ lat: number, lon:number }>,
|
||||
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||
selectedElement: UIEventSource<string>,
|
||||
filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean>}[]>,
|
||||
filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean> }[]>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
fullscreenMessage: UIEventSource<UIElement>,
|
||||
uiToShow: (() => UIElement)) {
|
||||
|
@ -23,14 +23,14 @@ export class StrayClickHandler {
|
|||
const self = this;
|
||||
filteredLayers.data.forEach((filteredLayer) => {
|
||||
filteredLayer.isDisplayed.addCallback(isEnabled => {
|
||||
if(isEnabled && self._lastMarker && leafletMap.data !== undefined){
|
||||
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
|
||||
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
||||
// This reclick might be at a location where a feature now appeared...
|
||||
leafletMap.data.removeLayer(self._lastMarker);
|
||||
leafletMap.data.removeLayer(self._lastMarker);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
lastClickLocation.addCallback(function (lastClick) {
|
||||
selectedElement.setData(undefined);
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ export default class UpdateFromOverpass implements FeatureSource{
|
|||
location: UIEventSource<Loc>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>,
|
||||
leafletMap: UIEventSource<L.Map>) {
|
||||
console.log("Crating overpass updater")
|
||||
this._location = location;
|
||||
this._layoutToUse = layoutToUse;
|
||||
this._leafletMap = leafletMap;
|
||||
|
|
68
Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
Normal file
68
Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import FeatureSource from "./FeatureSource";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||
|
||||
|
||||
/**
|
||||
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
||||
* If this is the case, multiple objects with a different _matching_layer_id are generated.
|
||||
* If not, the _feature_layter_id is added
|
||||
*/
|
||||
export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
||||
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
|
||||
constructor(layers: { layerDef: LayerConfig }[], upstream: FeatureSource) {
|
||||
let noPassthroughts = true;
|
||||
for (const layer of layers) {
|
||||
if (layer.layerDef.passAllFeatures) {
|
||||
noPassthroughts = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.features = upstream.features.map(features => {
|
||||
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||
if(features === undefined){
|
||||
return newFeatures;
|
||||
}
|
||||
|
||||
|
||||
for (const f of features) {
|
||||
if (f.feature._matching_layer_id) {
|
||||
// Already matched previously
|
||||
// We simply add it
|
||||
newFeatures.push(f);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const layer of layers) {
|
||||
if (layer.layerDef.overpassTags.matchesProperties(f.feature.properties)) {
|
||||
if (layer.layerDef.passAllFeatures) {
|
||||
|
||||
// We copy the feature; the "properties" field is kept identical though!
|
||||
// Keeping "properties" identical is needed, as it might break the 'allElementStorage' otherwise
|
||||
const newFeature = {
|
||||
geometry: f.feature.geometry,
|
||||
id: f.feature.id,
|
||||
type: f.feature.type,
|
||||
properties: f.feature.properties,
|
||||
_matching_layer_id : layer.layerDef.id
|
||||
}
|
||||
newFeatures.push({feature: newFeature, freshness: f.freshness});
|
||||
} else {
|
||||
// If not 'passAllFeatures', we are done
|
||||
f.feature._matching_layer_id = layer.layerDef.id;
|
||||
newFeatures.push(f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return newFeatures;
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -18,7 +18,7 @@ export default class FeatureSourceMerger implements FeatureSource {
|
|||
let all = {}; // Mapping 'id' -> {feature, freshness}
|
||||
for (const source of this._sources) {
|
||||
for (const f of source.features.data) {
|
||||
const id = f.feature.properties.id+f.feature.geometry.type;
|
||||
const id = f.feature.properties.id+f.feature.geometry.type+f.feature._matching_layer_id;
|
||||
const oldV = all[id];
|
||||
if(oldV === undefined){
|
||||
all[id] = f;
|
||||
|
|
|
@ -14,14 +14,13 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
upstream: FeatureSource) {
|
||||
|
||||
const layerDict = {};
|
||||
|
||||
|
||||
const self = this;
|
||||
|
||||
|
||||
function update() {
|
||||
console.log("UPdating...")
|
||||
const features: { feature: any, freshness: Date }[] = upstream.features.data;
|
||||
const newFeatures = features.filter(f => {
|
||||
const layerId = f.feature.properties._matching_layer_id;
|
||||
const layerId = f.feature._matching_layer_id;
|
||||
if (layerId === undefined) {
|
||||
console.error(f)
|
||||
throw "feature._matching_layer_id is undefined"
|
||||
|
@ -37,16 +36,22 @@ export default class FilteringFeatureSource implements FeatureSource {
|
|||
});
|
||||
self.features.setData(newFeatures);
|
||||
}
|
||||
|
||||
for (const layer of layers) {
|
||||
layerDict[layer.layerDef.id] = layer;
|
||||
layer.isDisplayed.addCallback(update)
|
||||
layer.isDisplayed.addCallback(() => {
|
||||
console.log("Updating due to layer change")
|
||||
update()})
|
||||
}
|
||||
upstream.features.addCallback(update);
|
||||
location.map(l => l.zoom).addCallback(update);
|
||||
upstream.features.addCallback(() => {
|
||||
console.log("Updating due to upstream change")
|
||||
update()});
|
||||
location.map(l => l.zoom).addCallback(() => {
|
||||
console.log("UPdating due to zoom level change")
|
||||
update();});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -45,7 +45,7 @@ export default class NoOverlapSource {
|
|||
partitions[layerId] = []
|
||||
}
|
||||
for (const feature of features) {
|
||||
partitions[feature.feature.properties._matching_layer_id].push(feature);
|
||||
partitions[feature.feature._matching_layer_id].push(feature);
|
||||
}
|
||||
|
||||
// With this partitioning in hand, we run over every layer and remove every underlying feature if needed
|
||||
|
|
|
@ -32,7 +32,7 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource {
|
|||
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||
for (const f of features) {
|
||||
const feat = f.feature;
|
||||
const layerId = feat.properties._matching_layer_id;
|
||||
const layerId = feat._matching_layer_id;
|
||||
const layer: LayerConfig = layerDict[layerId].layerDef;
|
||||
if (layer === undefined) {
|
||||
throw "No layer found with id " + layerId;
|
||||
|
@ -50,6 +50,7 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource {
|
|||
}
|
||||
|
||||
const centerPoint = GeoOperations.centerpoint(feat);
|
||||
centerPoint._matching_layer_id = feat._matching_layer_id;
|
||||
newFeatures.push({feature: centerPoint, freshness: f.freshness});
|
||||
|
||||
if(layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY){
|
||||
|
|
|
@ -1,163 +0,0 @@
|
|||
import {TagsFilter, TagUtils} from "./Tags";
|
||||
import {UIEventSource} from "./UIEventSource";
|
||||
import * as L from "leaflet"
|
||||
import {Layer} from "leaflet"
|
||||
import {GeoOperations} from "./GeoOperations";
|
||||
import {UIElement} from "../UI/UIElement";
|
||||
import State from "../State";
|
||||
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||
import Hash from "./Web/Hash";
|
||||
import LazyElement from "../UI/Base/LazyElement";
|
||||
|
||||
/***
|
||||
*
|
||||
*/
|
||||
export class FilteredLayer {
|
||||
|
||||
public readonly name: string | UIElement;
|
||||
public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true);
|
||||
public readonly layerDef: LayerConfig;
|
||||
|
||||
private readonly filters: TagsFilter;
|
||||
private readonly _maxAllowedOverlap: number;
|
||||
|
||||
/** The featurecollection from overpass
|
||||
*/
|
||||
private _dataFromOverpass: any[];
|
||||
/**
|
||||
* The leaflet layer object which should be removed on rerendering
|
||||
*/
|
||||
private _geolayer;
|
||||
|
||||
private _showOnPopup: (tags: UIEventSource<any>, feature: any) => UIElement;
|
||||
|
||||
|
||||
constructor(
|
||||
layerDef: LayerConfig,
|
||||
showOnPopup: ((tags: UIEventSource<any>, feature: any) => UIElement)
|
||||
) {
|
||||
this.layerDef = layerDef;
|
||||
|
||||
this._showOnPopup = showOnPopup;
|
||||
this.name = name;
|
||||
this.filters = layerDef.overpassTags;
|
||||
this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The main function to load data into this layer.
|
||||
* The data that is NOT used by this layer, is returned as a geojson object; the other data is rendered
|
||||
*/
|
||||
public SetApplicableData(features: any[]): any[] {
|
||||
const leftoverFeatures = [];
|
||||
const selfFeatures = [];
|
||||
for (let feature of features) {
|
||||
const tags = TagUtils.proprtiesToKV(feature.properties);
|
||||
const matches = this.filters.matches(tags);
|
||||
if (matches) {
|
||||
selfFeatures.push(feature);
|
||||
}
|
||||
if (!matches || this.layerDef.passAllFeatures) {
|
||||
leftoverFeatures.push(feature);
|
||||
}
|
||||
}
|
||||
|
||||
this.RenderLayer(selfFeatures)
|
||||
return leftoverFeatures;
|
||||
}
|
||||
|
||||
|
||||
private RenderLayer(features: any[]) {
|
||||
|
||||
if (this._geolayer !== undefined && this._geolayer !== null) {
|
||||
// Remove the old geojson layer from the map - we'll reshow all the elements later on anyway
|
||||
State.state.leafletMap.data.removeLayer(this._geolayer);
|
||||
}
|
||||
|
||||
// We fetch all the data we have to show:
|
||||
const data = {
|
||||
type: "FeatureCollection",
|
||||
features: features
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._geolayer = L.geoJSON(data, {
|
||||
style: feature => {
|
||||
const tagsSource = State.state.allElements.getEventSourceFor(feature);
|
||||
return self.layerDef.GenerateLeafletStyle(tagsSource, self._showOnPopup !== undefined);
|
||||
},
|
||||
pointToLayer: function (feature, latLng) {
|
||||
// Point to layer converts the 'point' to a layer object - as the geojson layer natively cannot handle points
|
||||
// Click handling is done in the next step
|
||||
const tagSource = State.state.allElements.getEventSourceFor(feature);
|
||||
|
||||
const style = self.layerDef.GenerateLeafletStyle(tagSource, self._showOnPopup !== undefined);
|
||||
let marker;
|
||||
if (style.icon === undefined) {
|
||||
marker = L.circle(latLng, {
|
||||
radius: 25,
|
||||
color: style.color
|
||||
});
|
||||
} else {
|
||||
marker = L.marker(latLng, {
|
||||
icon: L.divIcon({
|
||||
html: style.icon.html.Render(),
|
||||
className: style.icon.className,
|
||||
iconAnchor: style.icon.iconAnchor,
|
||||
iconUrl: style.icon.iconUrl,
|
||||
popupAnchor: style.icon.popupAnchor,
|
||||
iconSize: style.icon.iconSize
|
||||
})
|
||||
});
|
||||
}
|
||||
return marker;
|
||||
},
|
||||
onEachFeature: function (feature, layer: Layer) {
|
||||
|
||||
if (self._showOnPopup === undefined) {
|
||||
// No popup contents defined -> don't do anything
|
||||
return;
|
||||
}
|
||||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
closeOnEscapeKey: true,
|
||||
}, layer);
|
||||
|
||||
|
||||
const eventSource = State.state.allElements.getEventSourceFor(feature);
|
||||
let uiElement: LazyElement = new LazyElement(() => self._showOnPopup(eventSource, feature));
|
||||
popup.setContent(uiElement.Render());
|
||||
layer.bindPopup(popup);
|
||||
// We first render the UIelement (which'll still need an update later on...)
|
||||
// But at least it'll be visible already
|
||||
|
||||
|
||||
layer.on("click", (e) => {
|
||||
// We set the element as selected...
|
||||
uiElement.Activate();
|
||||
State.state.selectedElement.setData(feature);
|
||||
});
|
||||
|
||||
if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
|
||||
// This element is in the URL, so this is a share link
|
||||
// We already open it
|
||||
uiElement.Activate();
|
||||
popup.setContent(uiElement.Render());
|
||||
|
||||
const center = GeoOperations.centerpoint(feature).geometry.coordinates;
|
||||
popup.setLatLng({lat: center[1], lng: center[0]});
|
||||
popup.openOn(State.state.leafletMap.data);
|
||||
State.state.selectedElement.setData(feature);
|
||||
uiElement.Update();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
this._geolayer.addTo(State.state.leafletMap.data);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -8,7 +8,7 @@ import Svg from "../../Svg";
|
|||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||
import Img from "../../UI/Base/Img";
|
||||
|
||||
export class UserDetails {
|
||||
export default class UserDetails {
|
||||
|
||||
public loggedIn = false;
|
||||
public name = "Not logged in";
|
||||
|
@ -21,15 +21,15 @@ export class UserDetails {
|
|||
}
|
||||
|
||||
export class OsmConnection {
|
||||
|
||||
|
||||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
_dryRun: boolean;
|
||||
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
|
||||
private _onLoggedIn : ((userDetails: UserDetails) => void)[] = [];
|
||||
|
||||
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||
|
||||
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
|
@ -44,11 +44,11 @@ export class OsmConnection {
|
|||
} catch (e) {
|
||||
console.warn("Detecting standalone mode failed", e, ". Assuming in browser and not worrying furhter")
|
||||
}
|
||||
|
||||
|
||||
const iframeMode = window !== window.top;
|
||||
|
||||
|
||||
if ( iframeMode || pwaStandAloneMode || !singlePage) {
|
||||
if (iframeMode || pwaStandAloneMode || !singlePage) {
|
||||
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
||||
// Same for an iframe...
|
||||
this.auth = new osmAuth({
|
||||
|
@ -74,7 +74,7 @@ export class OsmConnection {
|
|||
this._dryRun = dryRun;
|
||||
|
||||
this.preferencesHandler = new OsmPreferences(this.auth, this);
|
||||
|
||||
|
||||
this.changesetHandler = new ChangesetHandler(layoutName, dryRun, this, this.auth);
|
||||
if (oauth_token.data !== undefined) {
|
||||
console.log(oauth_token.data)
|
||||
|
@ -86,7 +86,7 @@ export class OsmConnection {
|
|||
}, this.auth);
|
||||
|
||||
oauth_token.setData(undefined);
|
||||
|
||||
|
||||
}
|
||||
if (this.auth.authenticated()) {
|
||||
this.AttemptLogin(); // Also updates the user badge
|
||||
|
@ -100,7 +100,8 @@ export class OsmConnection {
|
|||
layout: LayoutConfig,
|
||||
allElements: ElementStorage,
|
||||
generateChangeXML: (csid: string) => string,
|
||||
continuation: () => void = () => {}) {
|
||||
continuation: () => void = () => {
|
||||
}) {
|
||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
|
||||
}
|
||||
|
||||
|
@ -112,10 +113,10 @@ export class OsmConnection {
|
|||
return this.preferencesHandler.GetLongPreference(key, prefix);
|
||||
}
|
||||
|
||||
public OnLoggedIn(action: (userDetails: UserDetails) => void){
|
||||
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
|
||||
this._onLoggedIn.push(action);
|
||||
}
|
||||
|
||||
|
||||
public LogOut() {
|
||||
this.auth.logout();
|
||||
this.userDetails.data.loggedIn = false;
|
||||
|
@ -132,7 +133,7 @@ export class OsmConnection {
|
|||
method: 'GET',
|
||||
path: '/api/0.6/user/details'
|
||||
}, function (err, details) {
|
||||
if(err != null){
|
||||
if (err != null) {
|
||||
console.log(err);
|
||||
return;
|
||||
}
|
||||
|
@ -140,9 +141,9 @@ export class OsmConnection {
|
|||
if (details == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
self.CheckForMessagesContinuously();
|
||||
|
||||
|
||||
// details is an XML DOM of user details
|
||||
let userInfo = details.getElementsByTagName("user")[0];
|
||||
|
||||
|
@ -177,7 +178,7 @@ export class OsmConnection {
|
|||
action(self.userDetails.data);
|
||||
}
|
||||
self._onLoggedIn = [];
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -189,7 +190,7 @@ export class OsmConnection {
|
|||
console.log("Checking for messages")
|
||||
this.AttemptLogin();
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue