forked from MapComplete/MapComplete
reset to previous commit
This commit is contained in:
parent
d5b614fc44
commit
196d40084d
90 changed files with 4953 additions and 1922 deletions
|
@ -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;
|
||||
|
|
|
@ -1,265 +1,271 @@
|
|||
import * as L from "leaflet";
|
||||
import { UIEventSource } from "../UIEventSource";
|
||||
import { Utils } from "../../Utils";
|
||||
import {UIEventSource} from "../UIEventSource";
|
||||
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 { CenterFlexedElement } from "../../UI/Base/CenterFlexedElement";
|
||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
|
||||
import {CenterFlexedElement} from "../../UI/Base/CenterFlexedElement";
|
||||
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
super(
|
||||
hasLocation.map(
|
||||
(hasLocation) => {
|
||||
if (hasLocation) {
|
||||
return new CenterFlexedElement(
|
||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
||||
); // crosshair_blue_ui()
|
||||
}
|
||||
if (isActive.data) {
|
||||
return new CenterFlexedElement(
|
||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
||||
); // crosshair_blue_center_ui
|
||||
}
|
||||
return new CenterFlexedElement(
|
||||
Img.AsImageElement(Svg.location, "", "width:1.25rem;height:1.25rem")
|
||||
); //crosshair_ui
|
||||
},
|
||||
[isActive]
|
||||
)
|
||||
);
|
||||
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;
|
||||
/**
|
||||
* The callback over the permission API
|
||||
* @private
|
||||
*/
|
||||
private readonly _permission: UIEventSource<string>;
|
||||
|
||||
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);
|
||||
});
|
||||
/***
|
||||
* 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>;
|
||||
|
||||
this.onClick(() => self.init(true));
|
||||
this.init(false);
|
||||
}
|
||||
|
||||
private init(askPermission: boolean) {
|
||||
const self = this;
|
||||
const map = this._leafletMap.data;
|
||||
/**
|
||||
* 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;
|
||||
|
||||
this._currentGPSLocation.addCallback((location) => {
|
||||
self._previousLocationGrant.setData("granted");
|
||||
|
||||
const timeSinceRequest =
|
||||
(new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
|
||||
if (timeSinceRequest < 30) {
|
||||
self.MoveToCurrentLoction(16);
|
||||
}
|
||||
/**
|
||||
* 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>;
|
||||
|
||||
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("");
|
||||
self.StartGeolocating(false);
|
||||
}
|
||||
}
|
||||
|
||||
private locate() {
|
||||
const self = this;
|
||||
const map: any = this._leafletMap.data;
|
||||
|
||||
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 = 16) {
|
||||
const location = this._currentGPSLocation.data;
|
||||
this._lastUserRequest = undefined;
|
||||
|
||||
if (
|
||||
this._currentGPSLocation.data.latlng[0] === 0 &&
|
||||
this._currentGPSLocation.data.latlng[1] === 0
|
||||
constructor(
|
||||
currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||
leafletMap: UIEventSource<L.Map>,
|
||||
layoutToUse: UIEventSource<LayoutConfig>
|
||||
) {
|
||||
console.debug("Not moving to GPS-location: it is null island");
|
||||
return;
|
||||
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);
|
||||
super(
|
||||
hasLocation.map(
|
||||
(hasLocationData) => {
|
||||
let icon: string;
|
||||
|
||||
if (isLocked.data) {
|
||||
icon = Svg.crosshair_locked;
|
||||
} else if (hasLocationData) {
|
||||
icon = Svg.crosshair_blue;
|
||||
} else if (isActive.data) {
|
||||
icon = Svg.crosshair_blue_center;
|
||||
} else {
|
||||
icon = Svg.crosshair;
|
||||
}
|
||||
|
||||
return new CenterFlexedElement(
|
||||
Img.AsImageElement(icon, "", "width:1.25rem;height:1.25rem")
|
||||
);
|
||||
|
||||
},
|
||||
[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 currentPointer = this._isActive.map(
|
||||
(isActive) => {
|
||||
if (isActive && !self._hasLocation.data) {
|
||||
return "cursor-wait";
|
||||
}
|
||||
return "cursor-pointer";
|
||||
},
|
||||
[this._hasLocation]
|
||||
);
|
||||
currentPointer.addCallbackAndRun((pointerClass) => {
|
||||
self.SetClass(pointerClass);
|
||||
});
|
||||
|
||||
this.onClick(() => {
|
||||
if (self._hasLocation.data) {
|
||||
self._isLocked.setData(!self._isLocked.data);
|
||||
}
|
||||
self.init(true);
|
||||
});
|
||||
this.init(false);
|
||||
|
||||
|
||||
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"
|
||||
);
|
||||
} 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 map = self._leafletMap.data;
|
||||
|
||||
const newMarker = L.marker(location.latlng, {icon: icon});
|
||||
newMarker.addTo(map);
|
||||
|
||||
if (self._marker !== undefined) {
|
||||
map.removeLayer(self._marker);
|
||||
}
|
||||
self._marker = newMarker;
|
||||
});
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
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;
|
||||
private init(askPermission: boolean) {
|
||||
const self = this;
|
||||
if (self._isActive.data) {
|
||||
self.MoveToCurrentLoction(16);
|
||||
return;
|
||||
}
|
||||
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("");
|
||||
self.StartGeolocating(false);
|
||||
}
|
||||
this.locate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private MoveToCurrentLoction(targetZoom = 16) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,12 @@ export default class StrayClickHandler {
|
|||
popupAnchor: [0, -45]
|
||||
})
|
||||
});
|
||||
const popup = L.popup().setContent("<div id='strayclick'></div>");
|
||||
const popup = L.popup({
|
||||
autoPan: true,
|
||||
autoPanPaddingTopLeft: [15,15],
|
||||
closeOnEscapeKey: true,
|
||||
autoClose: true
|
||||
}).setContent("<div id='strayclick' style='height: 65vh'></div>");
|
||||
self._lastMarker.addTo(leafletMap.data);
|
||||
self._lastMarker.bindPopup(popup);
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import RegisteringFeatureSource from "./RegisteringFeatureSource";
|
|||
|
||||
export default class FeaturePipeline implements FeatureSource {
|
||||
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> ;
|
||||
public features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||
|
||||
public readonly name = "FeaturePipeline"
|
||||
|
||||
|
@ -29,7 +29,7 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
selectedElement: UIEventSource<any>) {
|
||||
|
||||
const allLoadedFeatures = new UIEventSource<{ feature: any; freshness: Date }[]>([])
|
||||
|
||||
|
||||
// first we metatag, then we save to get the metatags into storage too
|
||||
// Note that we need to register before we do metatagging (as it expects the event sources)
|
||||
|
||||
|
@ -46,8 +46,11 @@ export default class FeaturePipeline implements FeatureSource {
|
|||
const geojsonSources: FeatureSource [] = GeoJsonSource
|
||||
.ConstructMultiSource(flayers.data, locationControl)
|
||||
.map(geojsonSource => {
|
||||
let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource));
|
||||
if(!geojsonSource.isOsmCache){
|
||||
let source = new RegisteringFeatureSource(
|
||||
new FeatureDuplicatorPerLayer(flayers,
|
||||
geojsonSource
|
||||
));
|
||||
if (!geojsonSource.isOsmCache) {
|
||||
source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features);
|
||||
}
|
||||
return source
|
||||
|
|
|
@ -1,9 +1,45 @@
|
|||
import {UIEventSource} from "../UIEventSource";
|
||||
import {Utils} from "../../Utils";
|
||||
|
||||
export default interface FeatureSource {
|
||||
features: UIEventSource<{feature: any, freshness: Date}[]>;
|
||||
features: UIEventSource<{ feature: any, freshness: Date }[]>;
|
||||
/**
|
||||
* Mainly used for debuging
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export class FeatureSourceUtils {
|
||||
|
||||
/**
|
||||
* Exports given featurePipeline as a geojson FeatureLists (downloads as a json)
|
||||
* @param featurePipeline The FeaturePipeline you want to export
|
||||
* @param options The options object
|
||||
* @param options.metadata True if you want to include the MapComplete metadata, false otherwise
|
||||
*/
|
||||
public static extractGeoJson(featurePipeline: FeatureSource, options: { metadata?: boolean } = {}) {
|
||||
let defaults = {
|
||||
metadata: false,
|
||||
}
|
||||
options = Utils.setDefaults(options, defaults);
|
||||
|
||||
// Select all features, ignore the freshness and other data
|
||||
let featureList: any[] = featurePipeline.features.data.map((feature) => feature.feature);
|
||||
|
||||
if (!options.metadata) {
|
||||
for (let i = 0; i < featureList.length; i++) {
|
||||
let feature = featureList[i];
|
||||
for (let property in feature.properties) {
|
||||
if (property[0] == "_") {
|
||||
delete featureList[i]["properties"][property];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {type: "FeatureCollection", features: featureList}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -175,7 +175,7 @@ export default class GeoJsonSource implements FeatureSource {
|
|||
|
||||
let freshness: Date = time;
|
||||
if (feature.properties["_last_edit:timestamp"] !== undefined) {
|
||||
freshness = new Date(feature["_last_edit:timestamp"])
|
||||
freshness = new Date(feature.properties["_last_edit:timestamp"])
|
||||
}
|
||||
|
||||
newFeatures.push({feature: feature, freshness: freshness})
|
||||
|
|
|
@ -6,11 +6,14 @@ export class GeoOperations {
|
|||
return turf.area(feature);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a GeoJSon feature to a point feature
|
||||
* @param feature
|
||||
*/
|
||||
static centerpoint(feature: any) {
|
||||
const newFeature = turf.center(feature);
|
||||
newFeature.properties = feature.properties;
|
||||
newFeature.id = feature.id;
|
||||
|
||||
return newFeature;
|
||||
}
|
||||
|
||||
|
@ -273,6 +276,14 @@ export class GeoOperations {
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Generates the closest point on a way from a given point
|
||||
* @param way The road on which you want to find a point
|
||||
* @param point Point defined as [lon, lat]
|
||||
*/
|
||||
public static nearestPoint(way, point: [number, number]){
|
||||
return turf.nearestPointOnLine(way, point, {units: "kilometers"});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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,16 +82,16 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new node element at the given lat/long.
|
||||
* An internal OsmObject is created to upload later on, a geojson represention is returned.
|
||||
|
@ -93,12 +99,12 @@ 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(Changes._nextId);
|
||||
const newId = Changes._nextId;
|
||||
Changes._nextId--;
|
||||
|
||||
const id = "node/" + osmNode.id;
|
||||
osmNode.lat = lat;
|
||||
osmNode.lon = lon;
|
||||
const id = "node/" + newId;
|
||||
|
||||
|
||||
const properties = {id: id};
|
||||
|
||||
const geojson = {
|
||||
|
@ -118,35 +124,49 @@ export class Changes implements FeatureSource{
|
|||
// 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"
|
||||
}
|
||||
properties[kv.key] = kv.value;
|
||||
changes.push({elementId: 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: newId, lat: lat, lon: lon})
|
||||
this.pending.data.push(...changes)
|
||||
this.pending.ping();
|
||||
this.newObjects.ping();
|
||||
return geojson;
|
||||
}
|
||||
|
||||
private uploadChangesWithLatestVersions(
|
||||
knownElements: OsmObject[], newElements: OsmObject[], pending: { elementId: string; key: string; value: string }[]) {
|
||||
knownElements: OsmObject[]) {
|
||||
const knownById = new Map<string, OsmObject>();
|
||||
|
||||
knownElements.forEach(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) {
|
||||
|
@ -168,9 +188,17 @@ 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;
|
||||
}
|
||||
const self = this;
|
||||
|
||||
if (this.isUploading.data) {
|
||||
return;
|
||||
}
|
||||
this.isUploading.setData(true)
|
||||
|
||||
console.log("Beginning upload...");
|
||||
// At last, we build the changeset and upload
|
||||
|
@ -213,17 +241,22 @@ export class Changes implements FeatureSource{
|
|||
changes += "</osmChange>";
|
||||
|
||||
return changes;
|
||||
});
|
||||
},
|
||||
() => {
|
||||
console.log("Upload successfull!")
|
||||
self.newObjects.setData([])
|
||||
self.pending.setData([]);
|
||||
self.isUploading.setData(false)
|
||||
},
|
||||
() => self.isUploading.setData(false)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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;
|
||||
|
@ -236,8 +269,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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
@ -47,6 +47,7 @@ export class OsmConnection {
|
|||
public auth;
|
||||
public userDetails: UIEventSource<UserDetails>;
|
||||
public isLoggedIn: UIEventSource<boolean>
|
||||
private fakeUser: boolean;
|
||||
_dryRun: boolean;
|
||||
public preferencesHandler: OsmPreferences;
|
||||
public changesetHandler: ChangesetHandler;
|
||||
|
@ -59,20 +60,31 @@ export class OsmConnection {
|
|||
url: string
|
||||
};
|
||||
|
||||
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
|
||||
constructor(dryRun: boolean,
|
||||
fakeUser: boolean,
|
||||
oauth_token: UIEventSource<string>,
|
||||
// Used to keep multiple changesets open and to write to the correct changeset
|
||||
layoutName: string,
|
||||
singlePage: boolean = true,
|
||||
osmConfiguration: "osm" | "osm-test" = 'osm'
|
||||
) {
|
||||
this.fakeUser = fakeUser;
|
||||
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;
|
||||
|
||||
this.userDetails = new UIEventSource<UserDetails>(new UserDetails(this._oauth_config.url), "userDetails");
|
||||
this.userDetails.data.dryRun = dryRun;
|
||||
this.userDetails.data.dryRun = dryRun || fakeUser;
|
||||
if(fakeUser){
|
||||
const ud = this.userDetails.data;
|
||||
ud.csCount = 5678
|
||||
ud.loggedIn= true;
|
||||
ud.unreadMessages = 0
|
||||
ud.name = "Fake user"
|
||||
ud.totalMessages = 42;
|
||||
}
|
||||
const self =this;
|
||||
this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => {
|
||||
if(self.userDetails.data.loggedIn == false && isLoggedIn == true){
|
||||
|
@ -110,8 +122,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> {
|
||||
|
@ -136,6 +150,10 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public AttemptLogin() {
|
||||
if(this.fakeUser){
|
||||
console.log("AttemptLogin called, but ignored as fakeUser is set")
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
console.log("Trying to log in...");
|
||||
this.updateAuthObject();
|
||||
|
|
|
@ -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[]>>();
|
||||
|
@ -37,15 +38,15 @@ export abstract class OsmObject {
|
|||
}
|
||||
|
||||
static DownloadObject(id: string, forceRefresh: boolean = false): UIEventSource<OsmObject> {
|
||||
let src : UIEventSource<OsmObject>;
|
||||
let src: UIEventSource<OsmObject>;
|
||||
if (OsmObject.objectCache.has(id)) {
|
||||
src = OsmObject.objectCache.get(id)
|
||||
if(forceRefresh){
|
||||
if (forceRefresh) {
|
||||
src.setData(undefined)
|
||||
}else{
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
src = new UIEventSource<OsmObject>(undefined)
|
||||
}
|
||||
const splitted = id.split("/");
|
||||
|
@ -157,7 +158,7 @@ export abstract class OsmObject {
|
|||
const minlat = bounds[1][0]
|
||||
const maxlat = bounds[0][0];
|
||||
const url = `${OsmObject.backendURL}api/0.6/map.json?bbox=${minlon},${minlat},${maxlon},${maxlat}`
|
||||
Utils.downloadJson(url).then( data => {
|
||||
Utils.downloadJson(url).then(data => {
|
||||
const elements: any[] = data.elements;
|
||||
const objects = OsmObject.ParseObjects(elements)
|
||||
callback(objects);
|
||||
|
@ -291,6 +292,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,
|
||||
|
@ -299,6 +301,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);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -83,7 +83,8 @@ export default class SimpleMetaTagger {
|
|||
|
||||
},
|
||||
(feature => {
|
||||
const units = State.state.layoutToUse.data.units ?? [];
|
||||
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 === undefined && !unit.eraseInvalid) {
|
||||
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();
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue