Merge develop

This commit is contained in:
Pieter Vander Vennet 2021-07-15 00:39:11 +02:00
commit 1010b159e5
87 changed files with 2292 additions and 718 deletions

View file

@ -1,11 +1,12 @@
import * as editorlayerindex from "../../assets/editor-layer-index.json"
import BaseLayer from "../../Models/BaseLayer";
import * as L from "leaflet";
import {TileLayer} from "leaflet";
import * as X from "leaflet-providers";
import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations";
import {TileLayer} from "leaflet";
import {Utils} from "../../Utils";
import Loc from "../../Models/Loc";
/**
* Calculates which layers are available at the current location
@ -24,45 +25,87 @@ export default class AvailableBaseLayers {
false, false),
feature: null,
max_zoom: 19,
min_zoom: 0
min_zoom: 0,
isBest: false, // This is a lie! Of course OSM is the best map! (But not in this context)
category: "osmbasedmap"
}
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
public availableEditorLayers: UIEventSource<BaseLayer[]>;
constructor(location: UIEventSource<{ lat: number, lon: number, zoom: number }>) {
const self = this;
this.availableEditorLayers =
location.map(
(currentLocation) => {
public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const source = location.map(
(currentLocation) => {
if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview;
}
if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview;
}
const currentLayers = self.availableEditorLayers?.data;
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
const currentLayers = source?.data; // A bit unorthodox - I know
const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
if (currentLayers === undefined) {
if (currentLayers === undefined) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
}
return currentLayers;
});
}
return currentLayers;
});
return source;
}
private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] {
public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.AvailableLayersAt(location).map(available => {
// First float all 'best layers' to the top
available.sort((a, b) => {
if (a.isBest && b.isBest) {
return 0;
}
if (!a.isBest) {
return 1
}
return -1;
}
)
if (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
}
prefered.reverse();
for (const category of prefered) {
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
available.sort((a, b) => {
if (a.category === category && b.category === category) {
return 0;
}
if (a.category !== category) {
return 1
}
return -1;
}
)
}
return available[0]
})
}
private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [AvailableBaseLayers.osmCarto]
const globalLayers = [];
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
@ -140,7 +183,9 @@ export default class AvailableBaseLayers {
min_zoom: props.min_zoom ?? 1,
name: props.name,
layer: leafletLayer,
feature: layer
feature: layer,
isBest: props.best ?? false,
category: props.category
});
}
return layers;
@ -152,15 +197,16 @@ export default class AvailableBaseLayers {
function l(id: string, name: string): BaseLayer {
try {
const layer: any = () => L.tileLayer.provider(id, undefined);
const baseLayer: BaseLayer = {
return {
feature: null,
id: id,
name: name,
layer: layer,
min_zoom: layer.minzoom,
max_zoom: layer.maxzoom
max_zoom: layer.maxzoom,
category: "osmbasedmap",
isBest: false
}
return baseLayer
} catch (e) {
console.error("Could not find provided layer", name, e);
return null;

View file

@ -1,244 +1,262 @@
import * as L from "leaflet";
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
import { UIEventSource } from "../UIEventSource";
import { Utils } from "../../Utils";
import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import { LocalStorageSource } from "../Web/LocalStorageSource";
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
import { VariableUiElement } from "../../UI/Base/VariableUIElement";
export default class GeoLocationHandler extends VariableUiElement {
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
*/
private readonly _isActive: UIEventSource<boolean>;
/**
* Wether or not the geolocation is active, aka the user requested the current location
* @private
*/
private readonly _isActive: UIEventSource<boolean>;
/**
* Wether or not the geolocation is locked, aka the user requested the current location and wants the crosshair to follow the user
* @private
*/
private readonly _isLocked: UIEventSource<boolean>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
/**
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
* @private
*/
private _lastUserRequest: Date;
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
/**
* The callback over the permission API
* @private
*/
private readonly _permission: UIEventSource<string>;
/***
* The marker on the map, in order to update it
* @private
*/
private _marker: L.Marker;
/**
* Literally: _currentGPSLocation.data != undefined
* @private
*/
private readonly _hasLocation: UIEventSource<boolean>;
private readonly _currentGPSLocation: UIEventSource<{
latlng: any;
accuracy: number;
}>;
/**
* Kept in order to update the marker
* @private
*/
private readonly _leafletMap: UIEventSource<L.Map>;
/**
* The date when the user requested the geolocation. If we have a location, it'll autozoom to it the first 30 secs
* @private
*/
private _lastUserRequest: Date;
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<string>;
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
constructor(
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>
) {
const hasLocation = currentGPSLocation.map(
(location) => location !== undefined
);
const previousLocationGrant = LocalStorageSource.Get(
"geolocation-permissions"
);
const isActive = new UIEventSource<boolean>(false);
const isLocked = new UIEventSource<boolean>(false);
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
leafletMap: UIEventSource<L.Map>,
layoutToUse: UIEventSource<LayoutConfig>) {
super(
hasLocation.map(
(hasLocationData) => {
if (isLocked.data) {
return Svg.crosshair_locked_ui();
} else if (hasLocationData) {
return Svg.crosshair_blue_ui();
} else if (isActive.data) {
return Svg.crosshair_blue_center_ui();
} else {
return Svg.crosshair_ui();
}
},
[isActive, isLocked]
)
);
this._isActive = isActive;
this._isLocked = isLocked;
this._permission = new UIEventSource<string>("");
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
const hasLocation = currentGPSLocation.map((location) => location !== undefined);
const previousLocationGrant = LocalStorageSource.Get("geolocation-permissions")
const isActive = new UIEventSource<boolean>(false);
const currentPointer = this._isActive.map(
(isActive) => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait";
}
return "cursor-pointer";
},
[this._hasLocation]
);
currentPointer.addCallbackAndRun((pointerClass) => {
self.SetClass(pointerClass);
});
super(
hasLocation.map(hasLocation => {
this.onClick(() => {
if (self._hasLocation.data) {
self._isLocked.setData(!self._isLocked.data);
}
self.init(true);
});
this.init(false);
if (hasLocation) {
return Svg.crosshair_blue_ui()
}
if (isActive.data) {
return Svg.crosshair_blue_center_ui();
}
return Svg.crosshair_ui();
}, [isActive])
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest =
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
self.MoveToCurrentLoction(16);
} else if (self._isLocked.data) {
self.MoveToCurrentLoction();
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue(
"--catch-detail-color"
);
this._isActive = isActive;
this._permission = new UIEventSource<string>("")
this._previousLocationGrant = previousLocationGrant;
this._currentGPSLocation = currentGPSLocation;
this._leafletMap = leafletMap;
this._layoutToUse = layoutToUse;
this._hasLocation = hasLocation;
const self = this;
} catch (e) {
console.error(e);
}
const icon = L.icon({
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
});
const currentPointer = this._isActive.map(isActive => {
if (isActive && !self._hasLocation.data) {
return "cursor-wait"
}
return "cursor-pointer"
}, [this._hasLocation])
currentPointer.addCallbackAndRun(pointerClass => {
self.SetClass(pointerClass);
})
const map = self._leafletMap.data;
const newMarker = L.marker(location.latlng, { icon: icon });
newMarker.addTo(map);
this.onClick(() => self.init(true))
this.init(false)
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
}
private init(askPermission: boolean) {
const self = this;
if (self._isActive.data) {
self.MoveToCurrentLoction(16);
return;
}
private init(askPermission: boolean) {
const self = this;
const map = this._leafletMap.data;
this._currentGPSLocation.addCallback((location) => {
self._previousLocationGrant.setData("granted");
const timeSinceRequest = (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
if (timeSinceRequest < 30) {
self.MoveToCurrentLoction(16)
}
let color = "#1111cc";
try {
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
} catch (e) {
console.error(e)
}
const icon = L.icon(
{
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
})
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);
if (self._marker !== undefined) {
map.removeLayer(self._marker);
}
self._marker = newMarker;
});
try {
navigator?.permissions?.query({name: 'geolocation'})
?.then(function (status) {
console.log("Geolocation is already", status)
if (status.state === "granted") {
self.StartGeolocating(false);
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
}
});
} catch (e) {
console.error(e)
}
if (askPermission) {
self.StartGeolocating(true);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
try {
navigator?.permissions
?.query({ name: "geolocation" })
?.then(function (status) {
console.log("Geolocation is already", status);
if (status.state === "granted") {
self.StartGeolocating(false);
}
}
self._permission.setData(status.state);
status.onchange = function () {
self._permission.setData(status.state);
};
});
} catch (e) {
console.error(e);
}
private locate() {
const self = this;
const map: any = this._leafletMap.data;
if (askPermission) {
self.StartGeolocating(true);
} else if (this._previousLocationGrant.data === "granted") {
this._previousLocationGrant.setData("");
self.StartGeolocating(false);
}
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
self._currentGPSLocation.setData({
latlng: [position.coords.latitude, position.coords.longitude],
accuracy: position.coords.accuracy
});
}, function () {
console.warn("Could not get location with navigator.geolocation")
});
return;
} else {
map.findAccuratePosition({
maxWait: 10000, // defaults to 10000
desiredAccuracy: 50 // defaults to 20
});
}
private MoveToCurrentLoction(targetZoom?: number) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
if (
this._currentGPSLocation.data.latlng[0] === 0 &&
this._currentGPSLocation.data.latlng[1] === 0
) {
console.debug("Not moving to GPS-location: it is null island");
return;
}
private MoveToCurrentLoction(targetZoom = 16) {
const location = this._currentGPSLocation.data;
this._lastUserRequest = undefined;
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.data.lockLocation;
let inRange = true;
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange =
b[0][0] <= location.latlng[0] &&
location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] &&
location.latlng[1] <= b[1][1];
}
}
if (!inRange) {
console.log(
"Not zooming to GPS location: out of bounds",
b,
location.latlng
);
} else {
this._leafletMap.data.setView(location.latlng, targetZoom);
}
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
console.log("Starting geolocation");
if (this._currentGPSLocation.data.latlng[0] === 0 && this._currentGPSLocation.data.latlng[1] === 0) {
console.debug("Not moving to GPS-location: it is null island")
return;
}
// We check that the GPS location is not out of bounds
const b = this._layoutToUse.data.lockLocation
let inRange = true;
if (b) {
if (b !== true) {
// B is an array with our locklocation
inRange = b[0][0] <= location.latlng[0] && location.latlng[0] <= b[1][0] &&
b[0][1] <= location.latlng[1] && location.latlng[1] <= b[1][1];
}
}
if (!inRange) {
console.log("Not zooming to GPS location: out of bounds", b, location.latlng)
} else {
this._leafletMap.data.setView(
location.latlng, targetZoom
);
}
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16);
}
private StartGeolocating(zoomToGPS = true) {
const self = this;
console.log("Starting geolocation")
console.log("Searching location using GPS");
this._lastUserRequest = zoomToGPS ? new Date() : new Date(0);
if (self._permission.data === "denied") {
self._previousLocationGrant.setData("");
return "";
}
if (this._currentGPSLocation.data !== undefined) {
this.MoveToCurrentLoction(16)
}
console.log("Searching location using GPS")
this.locate();
if (!self._isActive.data) {
self._isActive.setData(true);
Utils.DoEvery(60000, () => {
if (document.visibilityState !== "visible") {
console.log("Not starting gps: document not visible")
return;
}
this.locate();
})
}
if (self._isActive.data) {
return;
}
self._isActive.setData(true);
}
navigator.geolocation.watchPosition(
function (position) {
self._currentGPSLocation.setData({
latlng: [position.coords.latitude, position.coords.longitude],
accuracy: position.coords.accuracy,
});
},
function () {
console.warn("Could not get location with navigator.geolocation");
}
);
}
}

View file

@ -6,31 +6,38 @@ import Constants from "../../Models/Constants";
import FeatureSource from "../FeatureSource/FeatureSource";
import {TagsFilter} from "../Tags/TagsFilter";
import {Tag} from "../Tags/Tag";
import {OsmConnection} from "./OsmConnection";
import {LocalStorageSource} from "../Web/LocalStorageSource";
/**
* Handles all changes made to OSM.
* Needs an authenticator via OsmConnection
*/
export class Changes implements FeatureSource{
export class Changes implements FeatureSource {
private static _nextId = -1; // Newly assigned ID's are negative
public readonly name = "Newly added features"
/**
* The newly created points, as a FeatureSource
*/
public features = new UIEventSource<{feature: any, freshness: Date}[]>([]);
private static _nextId = -1; // Newly assigned ID's are negative
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
/**
* All the pending changes
*/
public readonly pending: UIEventSource<{ elementId: string, key: string, value: string }[]> =
new UIEventSource<{elementId: string; key: string; value: string}[]>([]);
public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", [])
/**
* All the pending new objects to upload
*/
private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", [])
private readonly isUploading = new UIEventSource(false);
/**
* Adds a change to the pending changes
*/
private static checkChange(kv: {k: string, v: string}): { k: string, v: string } {
private static checkChange(kv: { k: string, v: string }): { k: string, v: string } {
const key = kv.k;
const value = kv.v;
if (key === undefined || key === null) {
@ -49,8 +56,7 @@ export class Changes implements FeatureSource{
return {k: key.trim(), v: value.trim()};
}
addTag(elementId: string, tagsFilter: TagsFilter,
tags?: UIEventSource<any>) {
const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId);
@ -59,7 +65,7 @@ export class Changes implements FeatureSource{
if (changes.length == 0) {
return;
}
for (const change of changes) {
if (elementTags[change.k] !== change.v) {
elementTags[change.k] = change.v;
@ -76,15 +82,14 @@ export class Changes implements FeatureSource{
* Uploads all the pending changes in one go.
* Triggered by the 'PendingChangeUploader'-actor in Actors
*/
public flushChanges(flushreason: string = undefined){
if(this.pending.data.length === 0){
public flushChanges(flushreason: string = undefined) {
if (this.pending.data.length === 0) {
return;
}
if(flushreason !== undefined){
if (flushreason !== undefined) {
console.log(flushreason)
}
this.uploadAll([], this.pending.data);
this.pending.setData([]);
this.uploadAll();
}
/**
@ -101,17 +106,13 @@ export class Changes implements FeatureSource{
*/
public createElement(basicTags: Tag[], lat: number, lon: number) {
console.log("Creating a new element with ", basicTags)
const osmNode = new OsmNode(this.getNewID());
const id = "node/" + osmNode.id;
osmNode.lat = lat;
osmNode.lon = lon;
const properties = {id: id};
const properties = {id: osmNode.id};
const geojson = {
"type": "Feature",
"properties": properties,
"id": id,
"id": properties.id,
"geometry": {
"type": "Point",
"coordinates": [
@ -121,46 +122,54 @@ export class Changes implements FeatureSource{
}
}
const changes = Changes.createTagChangeList(basicTags, properties, id);
// The basictags are COPIED, the id is included in the properties
// The tags are not yet written into the OsmObject, but this is applied onto a
const changes = [];
for (const kv of basicTags) {
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
}
properties[kv.key] = kv.value;
changes.push({elementId:properties.id, key: kv.key, value: kv.value})
}
console.log("New feature added and pinged")
this.features.data.push({feature:geojson, freshness: new Date()});
this.features.data.push({feature: geojson, freshness: new Date()});
this.features.ping();
State.state.allElements.addOrGetElement(geojson).ping();
this.uploadAll([osmNode], changes);
if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) {
properties["_backend"] = State.state.osmConnection.userDetails.data.backend
}
this.newObjects.data.push({id: osmNode.id, lat: lat, lon: lon})
this.pending.data.push(...changes)
this.pending.ping();
this.newObjects.ping();
return geojson;
}
private static createTagChangeList(basicTags: Tag[], properties: { id: string }, id: string) {
// The basictags are COPIED, the id is included in the properties
// The tags are not yet written into the OsmObject, but this is applied onto a
const changes = [];
for (const kv of basicTags) {
properties[kv.key] = kv.value;
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
}
changes.push({elementId: id, key: kv.key, value: kv.value})
}
return changes;
}
private uploadChangesWithLatestVersions(
knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
knownElements: OsmObject[]) {
const knownById = new Map<string, OsmObject>();
knownElements.forEach(knownElement => {
console.log("Setting ",knownElement.type + knownElement.id, knownElement)
knownById.set(knownElement.type + "/" + knownElement.id, knownElement)
})
const newElements: OsmNode [] = this.newObjects.data.map(spec => {
const newElement = new OsmNode(spec.id);
newElement.lat = spec.lat;
newElement.lon = spec.lon;
return newElement
})
// Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
// We apply the changes on them
for (const change of pending) {
for (const change of this.pending.data) {
if (parseInt(change.elementId.split("/")[1]) < 0) {
// This is a new element - we should apply this on one of the new elements
for (const newElement of newElements) {
@ -169,8 +178,6 @@ export class Changes implements FeatureSource{
}
}
} else {
console.log(knownById, change.elementId)
knownById.get(change.elementId).addTag(change.key, change.value);
}
}
@ -184,17 +191,35 @@ export class Changes implements FeatureSource{
}
}
if (changedElements.length == 0 && newElements.length == 0) {
console.log("No changes in any object");
console.log("No changes in any object - clearing");
this.pending.setData([])
this.newObjects.setData([])
return;
}
if (this.isUploading.data) {
return;
}
this.isUploading.setData(true)
console.log("Beginning upload...");
// At last, we build the changeset and upload
const self = this;
State.state.osmConnection.UploadChangeset(
State.state.layoutToUse.data,
State.state.allElements,
(csId) => Changes.createChangesetFor(csId,changedElements, newElements )
(csId) => Changes.createChangesetFor(csId,changedElements, newElements ),
() => {
// When done
console.log("Upload successfull!")
self.newObjects.setData([])
self.pending.setData([]);
self.isUploading.setData(false)
},
() => self.isUploading.setData(false) // Failed - mark to try again
);
};
@ -220,26 +245,21 @@ export class Changes implements FeatureSource{
creations +
"</create>";
}
if (modifications.length > 0) {
changes +=
"\n<modify>\n" +
modifications +
"\n</modify>";
}
changes += "</osmChange>";
return changes;
}
private uploadAll(
newElements: OsmObject[],
pending: { elementId: string; key: string; value: string }[]
) {
private uploadAll() {
const self = this;
const pending = this.pending.data;
let neededIds: string[] = [];
for (const change of pending) {
const id = change.elementId;
@ -252,8 +272,7 @@ export class Changes implements FeatureSource{
neededIds = Utils.Dedup(neededIds);
OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => {
console.log("KnownElements:", knownElements)
self.uploadChangesWithLatestVersions(knownElements, newElements, pending)
self.uploadChangesWithLatestVersions(knownElements)
})
}

View file

@ -27,7 +27,7 @@ export class ChangesetHandler {
}
}
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage) {
private static parseUploadChangesetResponse(response: XMLDocument, allElements: ElementStorage): void {
const nodes = response.getElementsByTagName("node");
// @ts-ignore
for (const node of nodes) {
@ -69,7 +69,9 @@ export class ChangesetHandler {
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string) {
generateChangeXML: (csid: string) => string,
whenDone: (csId: string) => void,
onFail: () => void) {
if (this.userDetails.data.csCount == 0) {
// The user became a contributor!
@ -80,6 +82,7 @@ export class ChangesetHandler {
if (this._dryRun) {
const changesetXML = generateChangeXML("123456");
console.log(changesetXML);
whenDone("123456")
return;
}
@ -93,12 +96,14 @@ export class ChangesetHandler {
console.log(changeset);
self.AddChange(csId, changeset,
allElements,
() => {
},
whenDone,
(e) => {
console.error("UPLOADING FAILED!", e)
onFail()
}
)
}, {
onFail: onFail
})
} else {
// There still exists an open changeset (or at least we hope so)
@ -107,15 +112,13 @@ export class ChangesetHandler {
csId,
generateChangeXML(csId),
allElements,
() => {
},
whenDone,
(e) => {
console.warn("Could not upload, changeset is probably closed: ", e);
// Mark the CS as closed...
this.currentChangeset.setData("");
// ... and try again. As the cs is closed, no recursive loop can exist
self.UploadChangeset(layout, allElements, generateChangeXML);
self.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
}
)
@ -161,18 +164,22 @@ export class ChangesetHandler {
const self = this;
this.OpenChangeset(layout, (csId: string) => {
// The cs is open - let us actually upload!
const changes = generateChangeXML(csId)
// The cs is open - let us actually upload!
const changes = generateChangeXML(csId)
self.AddChange(csId, changes, allElements, (csId) => {
console.log("Successfully deleted ", object.id)
self.CloseChangeset(csId, continuation)
}, (csId) => {
alert("Deletion failed... Should not happend")
// FAILED
self.CloseChangeset(csId, continuation)
})
}, true, reason)
self.AddChange(csId, changes, allElements, (csId) => {
console.log("Successfully deleted ", object.id)
self.CloseChangeset(csId, continuation)
}, (csId) => {
alert("Deletion failed... Should not happend")
// FAILED
self.CloseChangeset(csId, continuation)
})
}, {
isDeletionCS: true,
deletionReason: reason
}
)
}
private CloseChangeset(changesetId: string = undefined, continuation: (() => void) = () => {
@ -204,15 +211,20 @@ export class ChangesetHandler {
private OpenChangeset(
layout: LayoutConfig,
continuation: (changesetId: string) => void,
isDeletionCS: boolean = false,
deletionReason: string = undefined) {
options?: {
isDeletionCS?: boolean,
deletionReason?: string,
onFail?: () => void
}
) {
options = options ?? {}
options.isDeletionCS = options.isDeletionCS ?? false
const commentExtra = layout.changesetmessage !== undefined ? " - " + layout.changesetmessage : "";
let comment = `Adding data with #MapComplete for theme #${layout.id}${commentExtra}`
if (isDeletionCS) {
if (options.isDeletionCS) {
comment = `Deleting a point with #MapComplete for theme #${layout.id}${commentExtra}`
if (deletionReason) {
comment += ": " + deletionReason;
if (options.deletionReason) {
comment += ": " + options.deletionReason;
}
}
@ -221,7 +233,7 @@ export class ChangesetHandler {
const metadata = [
["created_by", `MapComplete ${Constants.vNumber}`],
["comment", comment],
["deletion", isDeletionCS ? "yes" : undefined],
["deletion", options.isDeletionCS ? "yes" : undefined],
["theme", layout.id],
["language", Locale.language.data],
["host", window.location.host],
@ -244,7 +256,9 @@ export class ChangesetHandler {
}, function (err, response) {
if (response === undefined) {
console.log("err", err);
alert("Could not upload change (opening failed). Please file a bug report")
if(options.onFail){
options.onFail()
}
return;
} else {
continuation(response);
@ -265,7 +279,7 @@ export class ChangesetHandler {
private AddChange(changesetId: string,
changesetXML: string,
allElements: ElementStorage,
continuation: ((changesetId: string, idMapping: any) => void),
continuation: ((changesetId: string) => void),
onFail: ((changesetId: string, reason: string) => void) = undefined) {
this.auth.xhr({
method: 'POST',
@ -280,9 +294,9 @@ export class ChangesetHandler {
}
return;
}
const mapping = ChangesetHandler.parseUploadChangesetResponse(response, allElements);
ChangesetHandler.parseUploadChangesetResponse(response, allElements);
console.log("Uploaded changeset ", changesetId);
continuation(changesetId, mapping);
continuation(changesetId);
});
}

View file

@ -30,7 +30,7 @@ export default class UserDetails {
export class OsmConnection {
public static readonly _oauth_configs = {
public static readonly oauth_configs = {
"osm": {
oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
@ -66,7 +66,7 @@ export class OsmConnection {
osmConfiguration: "osm" | "osm-test" = 'osm'
) {
this._singlePage = singlePage;
this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm;
console.debug("Using backend", this._oauth_config.url)
OsmObject.SetBackendUrl(this._oauth_config.url + "/")
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
@ -110,8 +110,10 @@ export class OsmConnection {
public UploadChangeset(
layout: LayoutConfig,
allElements: ElementStorage,
generateChangeXML: (csid: string) => string) {
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML);
generateChangeXML: (csid: string) => string,
whenDone: (csId: string) => void,
onFail: () => {}) {
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, whenDone, onFail);
}
public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {

View file

@ -5,7 +5,8 @@ import {UIEventSource} from "../UIEventSource";
export abstract class OsmObject {
protected static backendURL = "https://www.openstreetmap.org/"
private static defaultBackend = "https://www.openstreetmap.org/"
protected static backendURL = OsmObject.defaultBackend;
private static polygonFeatures = OsmObject.constructPolygonFeatures()
private static objectCache = new Map<string, UIEventSource<OsmObject>>();
private static referencingWaysCache = new Map<string, UIEventSource<OsmWay[]>>();
@ -36,15 +37,22 @@ export abstract class OsmObject {
this.backendURL = url;
}
static DownloadObject(id): UIEventSource<OsmObject> {
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
let src: UIEventSource<OsmObject>;
if (OsmObject.objectCache.has(id)) {
return OsmObject.objectCache.get(id)
src = OsmObject.objectCache.get(id)
if (forceRefresh) {
src.setData(undefined)
} else {
return src;
}
} else {
src = new UIEventSource<OsmObject>(undefined)
}
const splitted = id.split("/");
const type = splitted[0];
const idN = splitted[1];
const idN = Number(splitted[1]);
const src = new UIEventSource<OsmObject>(undefined)
OsmObject.objectCache.set(id, src);
const newContinuation = (element: OsmObject) => {
src.setData(element)
@ -160,11 +168,11 @@ export abstract class OsmObject {
})
}
public static DownloadAll(neededIds): UIEventSource<OsmObject[]> {
public static DownloadAll(neededIds, forceRefresh = true): UIEventSource<OsmObject[]> {
// local function which downloads all the objects one by one
// this is one big loop, running one download, then rerunning the entire function
const allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id))
const allSources: UIEventSource<OsmObject> [] = neededIds.map(id => OsmObject.DownloadObject(id, forceRefresh))
const allCompleted = new UIEventSource(undefined).map(_ => {
return !allSources.some(uiEventSource => uiEventSource.data === undefined)
}, allSources)
@ -172,7 +180,7 @@ export abstract class OsmObject {
if (completed) {
return allSources.map(src => src.data)
}
return []
return undefined
});
}
@ -286,6 +294,7 @@ export abstract class OsmObject {
self.LoadData(element)
self.SaveExtraData(element, nodes);
const meta = {
"_last_edit:contributor": element.user,
"_last_edit:contributor:uid": element.uid,
@ -294,6 +303,11 @@ export abstract class OsmObject {
"_version_number": element.version
}
if (OsmObject.backendURL !== OsmObject.defaultBackend) {
self.tags["_backend"] = OsmObject.backendURL
meta["_backend"] = OsmObject.backendURL;
}
continuation(self, meta);
}
);
@ -348,7 +362,7 @@ export class OsmNode extends OsmObject {
lat: number;
lon: number;
constructor(id) {
constructor(id: number) {
super("node", id);
}
@ -401,7 +415,7 @@ export class OsmWay extends OsmObject {
lat: number;
lon: number;
constructor(id) {
constructor(id: number) {
super("way", id);
}
@ -467,11 +481,10 @@ export class OsmWay extends OsmObject {
export class OsmRelation extends OsmObject {
members;
public members;
constructor(id) {
constructor(id: number) {
super("relation", id);
}
centerpoint(): [number, number] {

View file

@ -84,6 +84,7 @@ export default class SimpleMetaTagger {
},
(feature => {
const units = State.state.layoutToUse.data.units ?? [];
let rewritten = false;
for (const key in feature.properties) {
if (!feature.properties.hasOwnProperty(key)) {
continue;
@ -95,16 +96,23 @@ export default class SimpleMetaTagger {
const value = feature.properties[key]
const [, denomination] = unit.findDenomination(value)
let canonical = denomination?.canonicalValue(value) ?? undefined;
console.log("Rewritten ", key, " from", value, "into", canonical)
if(canonical === value){
break;
}
console.log("Rewritten ", key, ` from '${value}' into '${canonical}'`)
if(canonical === undefined && !unit.eraseInvalid) {
break;
}
feature.properties[key] = canonical;
rewritten = true;
break;
}
}
if(rewritten){
State.state.allElements.getEventSourceById(feature.id).ping();
}
})
)

View file

@ -4,6 +4,22 @@ import {UIEventSource} from "../UIEventSource";
* UIEventsource-wrapper around localStorage
*/
export class LocalStorageSource {
static GetParsed<T>(key: string, defaultValue : T) : UIEventSource<T>{
return LocalStorageSource.Get(key).map(
str => {
if(str === undefined){
return defaultValue
}
try{
return JSON.parse(str)
}catch{
return defaultValue
}
}, [],
value => JSON.stringify(value)
)
}
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
try {