Merge branch 'develop' of github.com:pietervdvn/MapComplete into develop

This commit is contained in:
Pieter Vander Vennet 2021-10-22 13:37:40 +02:00
commit 9623afeec9
46 changed files with 1300 additions and 1032 deletions

View file

@ -23,11 +23,11 @@ export default class AvailableBaseLayers {
private static implementation: AvailableBaseLayersObj
static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
return AvailableBaseLayers.implementation.AvailableLayersAt(location);
return AvailableBaseLayers.implementation?.AvailableLayersAt(location) ?? new UIEventSource<BaseLayer[]>([]);
}
static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.implementation.SelectBestLayerAccordingTo(location, preferedCategory);
return AvailableBaseLayers.implementation?.SelectBestLayerAccordingTo(location, preferedCategory) ?? new UIEventSource<BaseLayer>(undefined);
}

View file

@ -2,6 +2,7 @@ import {UIEventSource} from "../UIEventSource";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "./AvailableBaseLayers";
import Loc from "../../Models/Loc";
import {Utils} from "../../Utils";
/**
* Sets the current background layer to a layer that is actually available
@ -12,6 +13,11 @@ export default class BackgroundLayerResetter {
location: UIEventSource<Loc>,
availableLayers: UIEventSource<BaseLayer[]>,
defaultLayerId: string = undefined) {
if(Utils.runningFromConsole){
return
}
defaultLayerId = defaultLayerId ?? AvailableBaseLayers.osmCarto.id;
// Change the baselayer back to OSM if we go out of the current range of the layer

View file

@ -29,7 +29,7 @@ export default class OverpassFeatureSource implements FeatureSource {
private readonly retries: UIEventSource<number> = new UIEventSource<number>(0);
private readonly state: {
readonly locationControl: UIEventSource<Loc>,
readonly layoutToUse: LayoutConfig,
@ -39,6 +39,7 @@ export default class OverpassFeatureSource implements FeatureSource {
}
private readonly _isActive: UIEventSource<boolean>;
private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void;
constructor(
state: {
readonly locationControl: UIEventSource<Loc>,
@ -90,7 +91,7 @@ export default class OverpassFeatureSource implements FeatureSource {
}
const self = this;
this.updateAsync(paddedZoomLevel).then(bboxDate => {
if(bboxDate === undefined || self.onBboxLoaded === undefined){
if (bboxDate === undefined || self.onBboxLoaded === undefined) {
return;
}
const [bbox, date, layers] = bboxDate
@ -109,41 +110,43 @@ export default class OverpassFeatureSource implements FeatureSource {
return undefined;
}
const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel);
if (bounds === undefined) {
return undefined;
}
const self = this;
const layersToDownload = []
for (const layer of this.state.layoutToUse.layers) {
if (typeof (layer) === "string") {
throw "A layer was not expanded!"
}
if(this.state.locationControl.data.zoom < layer.minzoom){
continue;
}
if (layer.doNotDownload) {
continue;
}
if (layer.source.geojsonSource !== undefined) {
// Not our responsibility to download this layer!
continue;
}
layersToDownload.push(layer)
}
let data: any = undefined
let date: Date = undefined
const overpassUrls = self.state.overpassUrl.data
let lastUsed = 0;
const layersToDownload = []
for (const layer of this.state.layoutToUse.layers) {
if (typeof (layer) === "string") {
throw "A layer was not expanded!"
}
if (this.state.locationControl.data.zoom < layer.minzoom) {
continue;
}
if (layer.doNotDownload) {
continue;
}
if (layer.source.geojsonSource !== undefined) {
// Not our responsibility to download this layer!
continue;
}
layersToDownload.push(layer)
}
const self = this;
const overpassUrls = self.state.overpassUrl.data
let bounds : BBox
do {
try {
bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel);
if (bounds === undefined) {
return undefined;
}
const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload);
if (overpass === undefined) {
@ -175,16 +178,21 @@ export default class OverpassFeatureSource implements FeatureSource {
}
}
}
} while (data === undefined);
} while (data === undefined && this._isActive.data);
self.retries.setData(0);
try {
if(data === undefined){
return undefined
}
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined));
self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
return [bounds, date, layersToDownload];
} catch (e) {
console.error("Got the overpass response, but could not process it: ", e, e.stack)
return undefined
} finally {
self.retries.setData(0);
self.runningQuery.setData(false);
}

View file

@ -1,6 +1,7 @@
import {Changes} from "../Osm/Changes";
import Constants from "../../Models/Constants";
import {UIEventSource} from "../UIEventSource";
import {Utils} from "../../Utils";
export default class PendingChangesUploader {
@ -30,6 +31,10 @@ export default class PendingChangesUploader {
}
});
if(Utils.runningFromConsole){
return;
}
document.addEventListener('mouseout', e => {
// @ts-ignore
if (!e.toElement && !e.relatedTarget) {

View file

@ -9,6 +9,13 @@ import {OsmConnection} from "../Osm/OsmConnection";
export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set(["timestamp",
"version",
"changeset",
"user",
"uid",
"id"] )
constructor(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
@ -18,15 +25,15 @@ export default class SelectedElementTagsUpdater {
state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => {
if(isLoggedIn){
if (isLoggedIn) {
SelectedElementTagsUpdater.installCallback(state)
return true;
}
})
}
private static installCallback(state: {
public static installCallback(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
@ -36,80 +43,99 @@ export default class SelectedElementTagsUpdater {
state.selectedElement.addCallbackAndRunD(s => {
let id = s.properties?.id
const backendUrl = state.osmConnection._oauth_config.url
if(id.startsWith(backendUrl)){
if (id.startsWith(backendUrl)) {
id = id.substring(backendUrl.length)
}
if(!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))){
if (!(id.startsWith("way") || id.startsWith("node") || id.startsWith("relation"))) {
// This object is _not_ from OSM, so we skip it!
return;
}
if(id.indexOf("-") >= 0){
if (id.indexOf("-") >= 0) {
// This is a new object
return;
}
OsmObject.DownloadPropertiesOf(id).then(tags => {
SelectedElementTagsUpdater.applyUpdate(state, tags, id)
}).catch(e => {
console.error("Could not update tags of ", id, "due to", e)
OsmObject.DownloadPropertiesOf(id).then(latestTags => {
SelectedElementTagsUpdater.applyUpdate(state, latestTags, id)
})
});
}
private static applyUpdate(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection
}, latestTags: any, id: string
public static applyUpdate(state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
changes: Changes,
osmConnection: OsmConnection
}, latestTags: any, id: string
) {
const pendingChanges = state.changes.pendingChanges.data
.filter(change => change.type +"/"+ change.id === id)
.filter(change => change.tags !== undefined);
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags;
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
try {
const pendingChanges = state.changes.pendingChanges.data
.filter(change => change.type + "/" + change.id === id)
.filter(change => change.tags !== undefined);
for (const pendingChange of pendingChanges) {
const tagChanges = pendingChange.tags;
for (const tagChange of tagChanges) {
const key = tagChange.k
const v = tagChange.v
if (v === undefined || v === "") {
delete latestTags[key]
} else {
latestTags[key] = v
}
}
}
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false;
const currentTagsSource = state.allElements.getEventSourceById(id);
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if(typeof osmValue === "number"){
osmValue = ""+osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
console.log("Local value for ", key ,":", localValue, "upstream", osmValue)
somethingChanged = true;
currentTags[key] = osmValue
}
}
if (somethingChanged) {
console.log("Detected upstream changes to the object when opening it, updating...")
currentTagsSource.ping()
}else{
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
// With the changes applied, we merge them onto the upstream object
let somethingChanged = false;
const currentTagsSource = state.allElements.getEventSourceById(id);
const currentTags = currentTagsSource.data
for (const key in latestTags) {
let osmValue = latestTags[key]
if (typeof osmValue === "number") {
osmValue = "" + osmValue
}
const localValue = currentTags[key]
if (localValue !== osmValue) {
console.log("Local value for ", key, ":", localValue, "upstream", osmValue)
somethingChanged = true;
currentTags[key] = osmValue
}
}
for (const currentKey in currentTags) {
if (currentKey.startsWith("_")) {
continue
}
if(this.metatags.has(currentKey)){
continue
}
if (currentKey in latestTags) {
continue
}
console.log("Removing key as deleted upstream", currentKey)
delete currentTags[currentKey]
somethingChanged = true
}
if (somethingChanged) {
console.log("Detected upstream changes to the object when opening it, updating...")
currentTagsSource.ping()
} else {
console.debug("Fetched latest tags for ", id, "but detected no changes")
}
} catch (e) {
console.error("Updating the tags of selected element ", id, "failed due to", e)
}
}

View file

@ -3,15 +3,20 @@ import {OsmObject} from "../Osm/OsmObject";
import Loc from "../../Models/Loc";
import {ElementStorage} from "../ElementStorage";
import FeaturePipeline from "../FeatureSource/FeaturePipeline";
import {GeoOperations} from "../GeoOperations";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
/**
* Makes sure the hash shows the selected element and vice-versa.
*/
export default class SelectedFeatureHandler {
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined])
hash: UIEventSource<string>;
private readonly hash: UIEventSource<string>;
private readonly state: {
selectedElement: UIEventSource<any>
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
locationControl: UIEventSource<Loc>,
layoutToUse: LayoutConfig
}
constructor(
@ -19,7 +24,9 @@ export default class SelectedFeatureHandler {
state: {
selectedElement: UIEventSource<any>,
allElements: ElementStorage,
featurePipeline: FeaturePipeline
featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>,
layoutToUse: LayoutConfig
}
) {
this.hash = hash;
@ -27,30 +34,9 @@ export default class SelectedFeatureHandler {
// If the hash changes, set the selected element correctly
function setSelectedElementFromHash(h){
if (h === undefined || h === "") {
// Hash has been cleared - we clear the selected element
state.selectedElement.setData(undefined);
}else{
// we search the element to select
const feature = state.allElements.ContainingFeatures.get(h)
if(feature === undefined){
return;
}
const currentlySeleced = state.selectedElement.data
if(currentlySeleced === undefined){
state.selectedElement.setData(feature)
return;
}
if(currentlySeleced.properties?.id === feature.properties.id){
// We already have the right feature
return;
}
state.selectedElement.setData(feature)
}
}
hash.addCallback(setSelectedElementFromHash)
const self = this;
hash.addCallback(() => self.setSelectedElementFromHash())
// IF the selected element changes, set the hash correctly
@ -66,41 +52,103 @@ export default class SelectedFeatureHandler {
hash.setData(h)
}
})
state.featurePipeline.newDataLoadedSignal.addCallbackAndRunD(_ => {
state.featurePipeline?.newDataLoadedSignal?.addCallbackAndRunD(_ => {
// New data was loaded. In initial startup, the hash might be set (via the URL) but might not be selected yet
if(hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)){
if (hash.data === undefined || SelectedFeatureHandler._no_trigger_on.has(hash.data)) {
// This is an invalid hash anyway
return;
}
if(state.selectedElement.data !== undefined){
if (state.selectedElement.data !== undefined) {
// We already have something selected
return;
}
setSelectedElementFromHash(hash.data)
self.setSelectedElementFromHash()
})
this.initialLoad()
}
/**
* On startup: check if the hash is loaded and eventually zoom to it
* @private
*/
private initialLoad() {
const hash = this.hash.data
if (hash === undefined || hash === "" || hash.indexOf("-") >= 0) {
return;
}
if (SelectedFeatureHandler._no_trigger_on.has(hash)) {
return;
}
OsmObject.DownloadObjectAsync(hash).then(obj => {
try {
console.log("Downloaded selected object from OSM-API for initial load: ", hash)
const geojson = obj.asGeoJson()
this.state.allElements.addOrGetElement(geojson)
this.state.selectedElement.setData(geojson)
this.zoomToSelectedFeature()
} catch (e) {
console.error(e)
}
})
}
// If a feature is selected via the hash, zoom there
public zoomToSelectedFeature(location: UIEventSource<Loc>) {
const hash = this.hash.data;
if (hash === undefined || SelectedFeatureHandler._no_trigger_on.has(hash)) {
return; // No valid feature selected
}
// We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure
try {
OsmObject.DownloadObject(hash).addCallbackAndRunD(element => {
const centerpoint = element.centerpoint();
console.log("Zooming to location for select point: ", centerpoint)
location.data.lat = centerpoint[0]
location.data.lon = centerpoint[1]
location.ping();
})
} catch (e) {
console.error("Could not download OSM-object with id", hash, " - probably a weird hash")
private setSelectedElementFromHash() {
const state = this.state
const h = this.hash.data
if (h === undefined || h === "") {
// Hash has been cleared - we clear the selected element
state.selectedElement.setData(undefined);
} else {
// we search the element to select
const feature = state.allElements.ContainingFeatures.get(h)
if (feature === undefined) {
return;
}
const currentlySeleced = state.selectedElement.data
if (currentlySeleced === undefined) {
state.selectedElement.setData(feature)
return;
}
if (currentlySeleced.properties?.id === feature.properties.id) {
// We already have the right feature
return;
}
state.selectedElement.setData(feature)
}
}
// If a feature is selected via the hash, zoom there
private zoomToSelectedFeature() {
const selected = this.state.selectedElement.data
if(selected === undefined){
return
}
const centerpoint= GeoOperations.centerpointCoordinates(selected)
const location = this.state.locationControl;
location.data.lon = centerpoint[0]
location.data.lat = centerpoint[1]
const minZoom = Math.max(14, ...(this.state.layoutToUse?.layers?.map(l => l.minzoomVisible) ?? []))
if(location.data.zoom < minZoom ){
location.data.zoom = minZoom
}
location.ping();
}
}

View file

@ -5,6 +5,7 @@ import TagRenderingAnswer from "../../UI/Popup/TagRenderingAnswer";
import Combine from "../../UI/Base/Combine";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {ElementStorage} from "../ElementStorage";
import {Utils} from "../../Utils";
export default class TitleHandler {
constructor(state : {
@ -38,6 +39,9 @@ export default class TitleHandler {
currentTitle.addCallbackAndRunD(title => {
if(Utils.runningFromConsole){
return
}
document.title = title
})
}

View file

@ -5,8 +5,6 @@ import State from "../State";
import BaseUIElement from "../UI/BaseUIElement";
import List from "../UI/Base/List";
import Title from "../UI/Base/Title";
import {UIEventSourceTools} from "./UIEventSource";
import AspectedRouting from "./Osm/aspectedRouting";
import {BBox} from "./BBox";
export interface ExtraFuncParams {

View file

@ -21,11 +21,13 @@ export default class AllImageProviders {
)
]
public static defaultKeys = [].concat(AllImageProviders.ImageAttributionSource.map(provider => provider.defaultKeyPrefixes))
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<string, UIEventSource<ProvidedImage[]>>()
public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string): UIEventSource<ProvidedImage[]> {
public static LoadImagesFor(tags: UIEventSource<any>, tagKey?: string[]): UIEventSource<ProvidedImage[]> {
if (tags.data.id === undefined) {
return undefined;
}
@ -44,7 +46,7 @@ export default class AllImageProviders {
let prefixes = imageProvider.defaultKeyPrefixes
if(tagKey !== undefined){
prefixes = [...prefixes, tagKey]
prefixes = tagKey
}
const singleSource = imageProvider.GetRelevantUrls(tags, {

View file

@ -27,7 +27,8 @@ export default class ImgurUploader {
files,
function (url) {
console.log("File saved at", url);
self.success.setData([...self.success.data, url]);
self.success.data.push(url)
self.success.ping();
self._handleSuccessUrl(url);
},
function () {

View file

@ -1,11 +1,9 @@
import ImageProvider, {ProvidedImage} from "./ImageProvider";
import BaseUIElement from "../../UI/BaseUIElement";
import {UIEventSource} from "../UIEventSource";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import {LicenseInfo} from "./LicenseInfo";
import Constants from "../../Models/Constants";
import {fail} from "assert";
export class Mapillary extends ImageProvider {
@ -13,7 +11,7 @@ export class Mapillary extends ImageProvider {
public static readonly singleton = new Mapillary();
private static readonly valuePrefix = "https://a.mapillary.com"
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com","https://mapillary.com"]
public static readonly valuePrefixes = [Mapillary.valuePrefix, "http://mapillary.com","https://mapillary.com","http://www.mapillary.com","https://www.mapillary.com"]
private static ExtractKeyFromURL(value: string, failIfNoMath = false): {
key: string,

View file

@ -97,6 +97,7 @@ export class Changes {
console.log("Is already uploading... Abort")
return;
}
console.log("Uploading changes due to: ", flushreason)
this.isUploading.setData(true)
this.flushChangesAsync()
@ -287,7 +288,7 @@ export class Changes {
v = undefined;
}
const oldV = obj.type[k]
const oldV = obj.tags[k]
if (oldV === v) {
continue;
}

View file

@ -8,7 +8,6 @@ import Svg from "../../Svg";
import Img from "../../UI/Base/Img";
import {Utils} from "../../Utils";
import {OsmObject} from "./OsmObject";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "./Changes";
export default class UserDetails {
@ -97,7 +96,6 @@ export class OsmConnection {
self.AttemptLogin()
}
});
this.isLoggedIn.addCallbackAndRunD(li => console.log("User is logged in!", li))
this._dryRun = options.dryRun;
this.updateAuthObject();

View file

@ -263,7 +263,7 @@ export abstract class OsmObject {
continue;
}
const v = this.tags[key];
if (v !== "") {
if (v !== "" && v !== undefined) {
tags += ' <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\n'
}
}

View file

@ -5,6 +5,7 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {UIEventSource} from "../UIEventSource";
import {QueryParameters} from "../Web/QueryParameters";
import Constants from "../../Models/Constants";
import {Utils} from "../../Utils";
export default class FeatureSwitchState {
@ -137,7 +138,7 @@ export default class FeatureSwitchState {
let testingDefaultValue = false;
if (this.featureSwitchApiURL.data !== "osm-test" &&
if (this.featureSwitchApiURL.data !== "osm-test" && !Utils.runningFromConsole &&
(location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
testingDefaultValue = true
}

View file

@ -286,6 +286,9 @@ export class UIEventSource<T> {
}
public stabilized(millisToStabilize): UIEventSource<T> {
if(Utils.runningFromConsole){
return this;
}
const newSource = new UIEventSource<T>(this.data);
@ -334,21 +337,4 @@ export class UIEventSource<T> {
}
)
}
}
export class UIEventSourceTools {
private static readonly _download_cache = new Map<string, UIEventSource<any>>()
public static downloadJsonCached(url: string): UIEventSource<any> {
const cached = UIEventSourceTools._download_cache.get(url)
if (cached !== undefined) {
return cached;
}
const src = new UIEventSource<any>(undefined)
UIEventSourceTools._download_cache.set(url, src)
Utils.downloadJson(url).then(r => src.setData(r))
return src;
}
}