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

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

View file

@ -39,6 +39,7 @@ export default class OverpassFeatureSource implements FeatureSource {
} }
private readonly _isActive: UIEventSource<boolean>; private readonly _isActive: UIEventSource<boolean>;
private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void; private readonly onBboxLoaded: (bbox: BBox, date: Date, layers: LayerConfig[], zoomlevel: number) => void;
constructor( constructor(
state: { state: {
readonly locationControl: UIEventSource<Loc>, readonly locationControl: UIEventSource<Loc>,
@ -90,7 +91,7 @@ export default class OverpassFeatureSource implements FeatureSource {
} }
const self = this; const self = this;
this.updateAsync(paddedZoomLevel).then(bboxDate => { this.updateAsync(paddedZoomLevel).then(bboxDate => {
if(bboxDate === undefined || self.onBboxLoaded === undefined){ if (bboxDate === undefined || self.onBboxLoaded === undefined) {
return; return;
} }
const [bbox, date, layers] = bboxDate const [bbox, date, layers] = bboxDate
@ -109,12 +110,10 @@ export default class OverpassFeatureSource implements FeatureSource {
return undefined; return undefined;
} }
const bounds = this.state.currentBounds.data?.pad(this.state.layoutToUse.widenFactor)?.expandToTileBounds(padToZoomLevel); let data: any = undefined
let date: Date = undefined
let lastUsed = 0;
if (bounds === undefined) {
return undefined;
}
const self = this;
const layersToDownload = [] const layersToDownload = []
@ -123,7 +122,7 @@ export default class OverpassFeatureSource implements FeatureSource {
if (typeof (layer) === "string") { if (typeof (layer) === "string") {
throw "A layer was not expanded!" throw "A layer was not expanded!"
} }
if(this.state.locationControl.data.zoom < layer.minzoom){ if (this.state.locationControl.data.zoom < layer.minzoom) {
continue; continue;
} }
if (layer.doNotDownload) { if (layer.doNotDownload) {
@ -136,14 +135,18 @@ export default class OverpassFeatureSource implements FeatureSource {
layersToDownload.push(layer) layersToDownload.push(layer)
} }
let data: any = undefined const self = this;
let date: Date = undefined
const overpassUrls = self.state.overpassUrl.data const overpassUrls = self.state.overpassUrl.data
let lastUsed = 0; let bounds : BBox
do { do {
try { 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); const overpass = this.GetFilter(overpassUrls[lastUsed], layersToDownload);
if (overpass === undefined) { 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 { try {
if(data === undefined){
return undefined
}
data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined)); data.features.forEach(feature => SimpleMetaTagger.objectMetaInfo.applyMetaTagsOnFeature(feature, date, undefined));
self.features.setData(data.features.map(f => ({feature: f, freshness: date}))); self.features.setData(data.features.map(f => ({feature: f, freshness: date})));
return [bounds, date, layersToDownload]; return [bounds, date, layersToDownload];
} catch (e) { } catch (e) {
console.error("Got the overpass response, but could not process it: ", e, e.stack) console.error("Got the overpass response, but could not process it: ", e, e.stack)
return undefined
} finally { } finally {
self.retries.setData(0);
self.runningQuery.setData(false); self.runningQuery.setData(false);
} }

View file

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

View file

@ -9,6 +9,13 @@ import {OsmConnection} from "../Osm/OsmConnection";
export default class SelectedElementTagsUpdater { export default class SelectedElementTagsUpdater {
private static readonly metatags = new Set(["timestamp",
"version",
"changeset",
"user",
"uid",
"id"] )
constructor(state: { constructor(state: {
selectedElement: UIEventSource<any>, selectedElement: UIEventSource<any>,
allElements: ElementStorage, allElements: ElementStorage,
@ -18,7 +25,7 @@ export default class SelectedElementTagsUpdater {
state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => { state.osmConnection.isLoggedIn.addCallbackAndRun(isLoggedIn => {
if(isLoggedIn){ if (isLoggedIn) {
SelectedElementTagsUpdater.installCallback(state) SelectedElementTagsUpdater.installCallback(state)
return true; return true;
} }
@ -26,7 +33,7 @@ export default class SelectedElementTagsUpdater {
} }
private static installCallback(state: { public static installCallback(state: {
selectedElement: UIEventSource<any>, selectedElement: UIEventSource<any>,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
@ -38,38 +45,38 @@ export default class SelectedElementTagsUpdater {
let id = s.properties?.id let id = s.properties?.id
const backendUrl = state.osmConnection._oauth_config.url const backendUrl = state.osmConnection._oauth_config.url
if(id.startsWith(backendUrl)){ if (id.startsWith(backendUrl)) {
id = id.substring(backendUrl.length) 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! // This object is _not_ from OSM, so we skip it!
return; return;
} }
if(id.indexOf("-") >= 0){ if (id.indexOf("-") >= 0) {
// This is a new object // This is a new object
return; return;
} }
OsmObject.DownloadPropertiesOf(id).then(latestTags => {
OsmObject.DownloadPropertiesOf(id).then(tags => { SelectedElementTagsUpdater.applyUpdate(state, latestTags, id)
SelectedElementTagsUpdater.applyUpdate(state, tags, id)
}).catch(e => {
console.error("Could not update tags of ", id, "due to", e)
}) })
}); });
} }
private static applyUpdate(state: { public static applyUpdate(state: {
selectedElement: UIEventSource<any>, selectedElement: UIEventSource<any>,
allElements: ElementStorage, allElements: ElementStorage,
changes: Changes, changes: Changes,
osmConnection: OsmConnection osmConnection: OsmConnection
}, latestTags: any, id: string }, latestTags: any, id: string
) { ) {
try {
const pendingChanges = state.changes.pendingChanges.data const pendingChanges = state.changes.pendingChanges.data
.filter(change => change.type +"/"+ change.id === id) .filter(change => change.type + "/" + change.id === id)
.filter(change => change.tags !== undefined); .filter(change => change.tags !== undefined);
for (const pendingChange of pendingChanges) { for (const pendingChange of pendingChanges) {
@ -92,24 +99,43 @@ export default class SelectedElementTagsUpdater {
for (const key in latestTags) { for (const key in latestTags) {
let osmValue = latestTags[key] let osmValue = latestTags[key]
if(typeof osmValue === "number"){ if (typeof osmValue === "number") {
osmValue = ""+osmValue osmValue = "" + osmValue
} }
const localValue = currentTags[key] const localValue = currentTags[key]
if (localValue !== osmValue) { if (localValue !== osmValue) {
console.log("Local value for ", key ,":", localValue, "upstream", osmValue) console.log("Local value for ", key, ":", localValue, "upstream", osmValue)
somethingChanged = true; somethingChanged = true;
currentTags[key] = osmValue 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) { if (somethingChanged) {
console.log("Detected upstream changes to the object when opening it, updating...") console.log("Detected upstream changes to the object when opening it, updating...")
currentTagsSource.ping() currentTagsSource.ping()
}else{ } else {
console.debug("Fetched latest tags for ", id, "but detected no changes") 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 Loc from "../../Models/Loc";
import {ElementStorage} from "../ElementStorage"; import {ElementStorage} from "../ElementStorage";
import FeaturePipeline from "../FeatureSource/FeaturePipeline"; 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. * Makes sure the hash shows the selected element and vice-versa.
*/ */
export default class SelectedFeatureHandler { export default class SelectedFeatureHandler {
private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined]) private static readonly _no_trigger_on = new Set(["welcome", "copyright", "layers", "new", "", undefined])
hash: UIEventSource<string>; private readonly hash: UIEventSource<string>;
private readonly state: { private readonly state: {
selectedElement: UIEventSource<any> selectedElement: UIEventSource<any>,
allElements: ElementStorage,
locationControl: UIEventSource<Loc>,
layoutToUse: LayoutConfig
} }
constructor( constructor(
@ -19,7 +24,9 @@ export default class SelectedFeatureHandler {
state: { state: {
selectedElement: UIEventSource<any>, selectedElement: UIEventSource<any>,
allElements: ElementStorage, allElements: ElementStorage,
featurePipeline: FeaturePipeline featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>,
layoutToUse: LayoutConfig
} }
) { ) {
this.hash = hash; this.hash = hash;
@ -27,30 +34,9 @@ export default class SelectedFeatureHandler {
// If the hash changes, set the selected element correctly // 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 // IF the selected element changes, set the hash correctly
@ -67,40 +53,102 @@ export default class SelectedFeatureHandler {
} }
}) })
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 // 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 // This is an invalid hash anyway
return; return;
} }
if(state.selectedElement.data !== undefined){ if (state.selectedElement.data !== undefined) {
// We already have something selected // We already have something selected
return; 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)
}
})
}
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 // If a feature is selected via the hash, zoom there
public zoomToSelectedFeature(location: UIEventSource<Loc>) { private zoomToSelectedFeature() {
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 selected = this.state.selectedElement.data
const centerpoint = element.centerpoint(); if(selected === undefined){
console.log("Zooming to location for select point: ", centerpoint) return
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")
} }
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 Combine from "../../UI/Base/Combine";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {ElementStorage} from "../ElementStorage"; import {ElementStorage} from "../ElementStorage";
import {Utils} from "../../Utils";
export default class TitleHandler { export default class TitleHandler {
constructor(state : { constructor(state : {
@ -38,6 +39,9 @@ export default class TitleHandler {
currentTitle.addCallbackAndRunD(title => { currentTitle.addCallbackAndRunD(title => {
if(Utils.runningFromConsole){
return
}
document.title = title document.title = title
}) })
} }

View file

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

View file

@ -22,10 +22,12 @@ 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[]>>() 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) { if (tags.data.id === undefined) {
return undefined; return undefined;
} }
@ -44,7 +46,7 @@ export default class AllImageProviders {
let prefixes = imageProvider.defaultKeyPrefixes let prefixes = imageProvider.defaultKeyPrefixes
if(tagKey !== undefined){ if(tagKey !== undefined){
prefixes = [...prefixes, tagKey] prefixes = tagKey
} }
const singleSource = imageProvider.GetRelevantUrls(tags, { const singleSource = imageProvider.GetRelevantUrls(tags, {

View file

@ -27,7 +27,8 @@ export default class ImgurUploader {
files, files,
function (url) { function (url) {
console.log("File saved at", 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); self._handleSuccessUrl(url);
}, },
function () { function () {

View file

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

View file

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

View file

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

View file

@ -263,7 +263,7 @@ export abstract class OsmObject {
continue; continue;
} }
const v = this.tags[key]; const v = this.tags[key];
if (v !== "") { if (v !== "" && v !== undefined) {
tags += ' <tag k="' + Utils.EncodeXmlValue(key) + '" v="' + Utils.EncodeXmlValue(this.tags[key]) + '"/>\n' 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 {UIEventSource} from "../UIEventSource";
import {QueryParameters} from "../Web/QueryParameters"; import {QueryParameters} from "../Web/QueryParameters";
import Constants from "../../Models/Constants"; import Constants from "../../Models/Constants";
import {Utils} from "../../Utils";
export default class FeatureSwitchState { export default class FeatureSwitchState {
@ -137,7 +138,7 @@ export default class FeatureSwitchState {
let testingDefaultValue = false; 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")) { (location.hostname === "localhost" || location.hostname === "127.0.0.1")) {
testingDefaultValue = true testingDefaultValue = true
} }

View file

@ -286,6 +286,9 @@ export class UIEventSource<T> {
} }
public stabilized(millisToStabilize): UIEventSource<T> { public stabilized(millisToStabilize): UIEventSource<T> {
if(Utils.runningFromConsole){
return this;
}
const newSource = new UIEventSource<T>(this.data); const newSource = new UIEventSource<T>(this.data);
@ -335,20 +338,3 @@ 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;
}
}

View file

@ -2,7 +2,7 @@ import {Utils} from "../Utils";
export default class Constants { export default class Constants {
public static vNumber = "0.11.0-rc-1"; public static vNumber = "0.11.0";
public static ImgurApiKey = '7070e7167f0a25a' public static ImgurApiKey = '7070e7167f0a25a'
public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2'
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"

View file

@ -113,9 +113,9 @@ export default class SimpleAddUI extends Toggle {
new Toggle( new Toggle(
new Toggle( new Toggle(
new Toggle( new Toggle(
addUi,
Translations.t.general.add.stillLoading.Clone().SetClass("alert"), Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
state.featurePipeline.somethingLoaded addUi,
state.featurePipeline.runningQuery
), ),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"), Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints) state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)

View file

@ -1,45 +0,0 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
/**
* Shows that 'images are uploading', 'all images are uploaded' as relevant...
*/
export default class UploadFlowStateUI extends VariableUiElement {
constructor(queue: UIEventSource<string[]>, failed: UIEventSource<string[]>, success: UIEventSource<string[]>) {
const t = Translations.t.image;
super(
queue.map(queue => {
const failedReasons = failed.data
const successCount = success.data.length
const pendingCount = queue.length - successCount - failedReasons.length;
let stateMessages: BaseUIElement[] = []
if (pendingCount == 1) {
stateMessages.push(t.uploadingPicture.Clone().SetClass("alert"))
}
if (pendingCount > 1) {
stateMessages.push(t.uploadingMultiple.Subs({count: "" + pendingCount}).SetClass("alert"))
}
if (failedReasons.length > 0) {
stateMessages.push(t.uploadFailed.Clone().SetClass("alert"))
}
if (successCount > 0 && pendingCount == 0) {
stateMessages.push(t.uploadDone.SetClass("thanks"))
}
stateMessages.forEach(msg => msg.SetStyle("display: block ruby"))
return stateMessages
}, [failed, success])
);
}
}

View file

@ -23,6 +23,7 @@ import ScrollableFullScreen from "./Base/ScrollableFullScreen";
import Translations from "./i18n/Translations"; import Translations from "./i18n/Translations";
import SimpleAddUI from "./BigComponents/SimpleAddUI"; import SimpleAddUI from "./BigComponents/SimpleAddUI";
import StrayClickHandler from "../Logic/Actors/StrayClickHandler"; import StrayClickHandler from "../Logic/Actors/StrayClickHandler";
import Lazy from "./Base/Lazy";
export class DefaultGuiState { export class DefaultGuiState {
public readonly welcomeMessageIsOpened; public readonly welcomeMessageIsOpened;
@ -81,12 +82,22 @@ export default class DefaultGUI {
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) { constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state; this.state = state;
this._guiState = guiState; this._guiState = guiState;
const self = this;
if (state.layoutToUse.customCss !== undefined) { if (state.layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(state.layoutToUse.customCss); Utils.LoadCustomCss(state.layoutToUse.customCss);
} }
this.SetupUIElements();
this.SetupMap()
}
private SetupMap(){
const state = this.state;
const guiState = this._guiState;
// Attach the map // Attach the map
state.mainMapObject.SetClass("w-full h-full") state.mainMapObject.SetClass("w-full h-full")
.AttachTo("leafletDiv") .AttachTo("leafletDiv")
@ -96,8 +107,28 @@ export default class DefaultGUI {
state state
) )
this.InitWelcomeMessage();
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
features: state.homeLocation,
enablePopups: false,
})
state.leafletMap.addCallbackAndRunD(_ => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
State.state.locationControl.ping();
return true;
})
}
private SetupUIElements(){
const state = this.state;
const guiState = this._guiState;
const self =this
Toggle.If(state.featureSwitchUserbadge, Toggle.If(state.featureSwitchUserbadge,
() => new UserBadge(state) () => new UserBadge(state)
).AttachTo("userbadge") ).AttachTo("userbadge")
@ -119,36 +150,21 @@ export default class DefaultGUI {
} }
new Toggle(self.InitWelcomeMessage(), new Toggle(new Lazy(() => self.InitWelcomeMessage()),
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout), Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
state.featureSwitchWelcomeMessage state.featureSwitchWelcomeMessage
).AttachTo("messagesbox"); ).AttachTo("messagesbox");
new LeftControls(state, guiState).AttachTo("bottom-left"); new LeftControls(state, guiState).AttachTo("bottom-left");
new RightControls(state).AttachTo("bottom-right"); new RightControls(state).AttachTo("bottom-right");
State.state.locationControl.ping();
new CenterMessageBox(state).AttachTo("centermessage"); new CenterMessageBox(state).AttachTo("centermessage");
document document
.getElementById("centermessage") .getElementById("centermessage")
.classList.add("pointer-events-none"); .classList.add("pointer-events-none");
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
features: state.homeLocation,
enablePopups: false,
})
state.leafletMap.addCallbackAndRunD(_ => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
return true;
})
} }
private InitWelcomeMessage() { private InitWelcomeMessage() : BaseUIElement{
const isOpened = this._guiState.welcomeMessageIsOpened const isOpened = this._guiState.welcomeMessageIsOpened
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state); const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
@ -180,7 +196,7 @@ export default class DefaultGUI {
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) { public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) {
function setup(){ function setup() {
let presetCount = 0; let presetCount = 0;
for (const layer of state.layoutToUse.layers) { for (const layer of state.layoutToUse.layers) {
for (const preset of layer.presets) { for (const preset of layer.presets) {

View file

@ -9,7 +9,9 @@ import ImageProvider from "../../Logic/ImageProviders/ImageProvider";
export class ImageCarousel extends Toggle { export class ImageCarousel extends Toggle {
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>, tags: UIEventSource<any>) { constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>,
tags: UIEventSource<any>,
keys: string[]) {
const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => { const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => {
const uiElements: BaseUIElement[] = []; const uiElements: BaseUIElement[] = [];
for (const url of imageURLS) { for (const url of imageURLS) {

View file

@ -9,14 +9,23 @@ import LicensePicker from "../BigComponents/LicensePicker";
import Toggle from "../Input/Toggle"; import Toggle from "../Input/Toggle";
import FileSelectorButton from "../Input/FileSelectorButton"; import FileSelectorButton from "../Input/FileSelectorButton";
import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader";
import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {FixedUiElement} from "../Base/FixedUiElement"; import {FixedUiElement} from "../Base/FixedUiElement";
import {VariableUiElement} from "../Base/VariableUIElement";
export class ImageUploadFlow extends Toggle { export class ImageUploadFlow extends Toggle {
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) { constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
const perId = ImageUploadFlow.uploadCountsPerId
const id = tagsSource.data.id
if(!perId.has(id)){
perId.set(id, new UIEventSource<number>(0))
}
const uploadedCount = perId.get(id)
const uploader = new ImgurUploader(url => { const uploader = new ImgurUploader(url => {
// A file was uploaded - we add it to the tags of the object // A file was uploaded - we add it to the tags of the object
@ -30,6 +39,8 @@ export class ImageUploadFlow extends Toggle {
key = imagePrefix + ":" + freeIndex; key = imagePrefix + ":" + freeIndex;
} }
console.log("Adding image:" + key, url); console.log("Adding image:" + key, url);
uploadedCount.data ++
uploadedCount.ping()
Promise.resolve(State.state.changes Promise.resolve(State.state.changes
.applyAction(new ChangeTagAction( .applyAction(new ChangeTagAction(
tags.id, new Tag(key, url), tagsSource.data, tags.id, new Tag(key, url), tagsSource.data,
@ -40,10 +51,6 @@ export class ImageUploadFlow extends Toggle {
))) )))
}) })
uploader.queue.addCallbackD(q => console.log("Image upload queue is ", q))
uploader.failed.addCallbackD(q => console.log("Image upload fail list is ", q))
uploader.success.addCallbackD(q => console.log("Image upload success list is ", q))
const licensePicker = new LicensePicker() const licensePicker = new LicensePicker()
const t = Translations.t.image; const t = Translations.t.image;
@ -105,10 +112,33 @@ export class ImageUploadFlow extends Toggle {
}) })
const uploadStateUi = new UploadFlowStateUI(uploader.queue, uploader.failed, uploader.success)
const uploadFlow: BaseUIElement = new Combine([ const uploadFlow: BaseUIElement = new Combine([
uploadStateUi, new VariableUiElement(uploader.queue.map(q => q.length).map(l => {
if(l == 0){
return undefined;
}
if(l == 1){
return t.uploadingPicture.Clone().SetClass("alert")
}else{
return t.uploadingMultiple.Subs({count: "" + l}).SetClass("alert")
}
})),
new VariableUiElement(uploader.failed.map(q => q.length).map(l => {
if(l==0){
return undefined
}
return t.uploadFailed.Clone().SetClass("alert");
})),
new VariableUiElement(uploadedCount.map(l => {
if(l == 0){
return undefined;
}
if(l == 1){
return t.uploadDone.Clone().SetClass("thanks");
}
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks")
})),
fileSelector, fileSelector,
Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"), Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"),
licensePicker licensePicker

View file

@ -83,15 +83,15 @@ export default class SpecialVisualizations {
docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)",
args: [{ args: [{
name: "image key/prefix (multiple values allowed if comma-seperated)", name: "image key/prefix (multiple values allowed if comma-seperated)",
defaultValue: "image", defaultValue: AllImageProviders.defaultKeys.join(","),
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... " doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... "
}], }],
constr: (state: State, tags, args) => { constr: (state: State, tags, args) => {
let imagePrefixes = undefined; let imagePrefixes: string[] = undefined;
if(args.length > 0){ if(args.length > 0){
imagePrefixes = args; imagePrefixes = [].concat(...args.map(a => a.split(",")));
} }
return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags); return new ImageCarousel(AllImageProviders.LoadImagesFor(tags, imagePrefixes), tags, imagePrefixes);
} }
}, },
{ {

View file

@ -330,7 +330,7 @@ export class Utils {
return cached.promise return cached.promise
} }
} }
const promise = Utils.downloadJson(url, headers) const promise = /*NO AWAIT as we work with the promise directly */Utils.downloadJson(url, headers)
Utils._download_cache.set(url, {promise, timestamp: new Date().getTime()}) Utils._download_cache.set(url, {promise, timestamp: new Date().getTime()})
return await promise return await promise
} }

View file

@ -174,8 +174,8 @@
"condition": "service:bicycle:diy=yes", "condition": "service:bicycle:diy=yes",
"render": "<img src='./assets/layers/bike_shop/tools.svg'/>" "render": "<img src='./assets/layers/bike_shop/tools.svg'/>"
}, },
{ "condition": "service:bicycle:cleaning=yes", {
"condition": "service:bicycle:cleaning=yes",
"render": "<img src='./assets/layers/bike_cleaning/bike_cleaning_icon.svg'/>" "render": "<img src='./assets/layers/bike_cleaning/bike_cleaning_icon.svg'/>"
}, },
"defaults" "defaults"

View file

@ -668,8 +668,8 @@
] ]
}, },
"question": { "question": {
"en": "What is the carriage width of this road (in meters)?", "en": "What is the carriage width of this road (in meters)?<br/><span class='subtle'>This is measured curb to curb and thus includes the width of parallell parking lanes</span>",
"nl": "Hoe breed is de rijbaan in deze straat (in meters)?" "nl": "Hoe breed is de rijbaan in deze straat (in meters)?<br/><span class='subtle'>Dit is </span><br/><span class='subtle'>Meet dit van stoepsteen tot stoepsteen, dus inclusief een parallelle parkeerstrook</span>"
}, },
"id": "width:carriageway" "id": "width:carriageway"
}, },

View file

@ -15,6 +15,9 @@
"startLat": 51.25634, "startLat": 51.25634,
"startLon": 3.195682, "startLon": 3.195682,
"startZoom": 12, "startZoom": 12,
"clustering": {
"maxZoom": 0
},
"layers": [ "layers": [
"defibrillator", "defibrillator",
{ {

View file

@ -1,7 +1,6 @@
{ {
"id": "buurtnatuur", "id": "buurtnatuur",
"title": { "title": {
"#": "DO NOT TRANSLATE THIS THEME - this one is only meant to be in dutch!",
"nl": "Breng jouw buurtnatuur in kaart" "nl": "Breng jouw buurtnatuur in kaart"
}, },
"shortDescription": { "shortDescription": {

View file

@ -207,7 +207,8 @@
"overrideAll": { "overrideAll": {
"allowSplit": true, "allowSplit": true,
"tagRenderings+": [ "tagRenderings+": [
{"id": "is_cyclestreet", {
"id": "is_cyclestreet",
"question": { "question": {
"nl": "Is deze straat een fietsstraat?", "nl": "Is deze straat een fietsstraat?",
"en": "Is this street a cyclestreet?", "en": "Is this street a cyclestreet?",
@ -283,7 +284,8 @@
} }
] ]
}, },
{"id": "future_cyclestreet", {
"id": "future_cyclestreet",
"question": { "question": {
"nl": "Wanneer wordt deze straat een fietsstraat?", "nl": "Wanneer wordt deze straat een fietsstraat?",
"en": "When will this street become a cyclestreet?", "en": "When will this street become a cyclestreet?",

View file

@ -10,7 +10,8 @@
"ccb": "under the CC-BY-license", "ccb": "under the CC-BY-license",
"uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.", "uploadFailed": "Could not upload your picture. Are you connected to the Internet, and allow third party API's? The Brave browser or the uMatrix plugin might block them.",
"respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.", "respectPrivacy": "Do not photograph people nor license plates. Do not upload Google Maps, Google Streetview or other copyrighted sources.",
"uploadDone": "<span class='thanks'>Your picture has been added. Thanks for helping out!</span>", "uploadDone": "Your picture has been added. Thanks for helping out!",
"uploadMultipleDone": "{count} pictures have been added. Thanks for helping out!",
"dontDelete": "Cancel", "dontDelete": "Cancel",
"doDelete": "Remove image", "doDelete": "Remove image",
"isDeleted": "Deleted", "isDeleted": "Deleted",

View file

@ -2376,7 +2376,7 @@
"question": "Is this street lit?" "question": "Is this street lit?"
}, },
"width:carriageway": { "width:carriageway": {
"question": "What is the carriage width of this road (in meters)?", "question": "What is the carriage width of this road (in meters)?<br/><span class='subtle'>This is measured curb to curb and thus includes the width of parallell parking lanes</span>",
"render": "The carriage width of this road is <strong>{width:carriageway}m</strong>" "render": "The carriage width of this road is <strong>{width:carriageway}m</strong>"
} }
}, },

View file

@ -2329,7 +2329,7 @@
"question": "Is deze weg verlicht?" "question": "Is deze weg verlicht?"
}, },
"width:carriageway": { "width:carriageway": {
"question": "Hoe breed is de rijbaan in deze straat (in meters)?", "question": "Hoe breed is de rijbaan in deze straat (in meters)?<br/><span class='subtle'>Dit is </span><br/><span class='subtle'>Meet dit van stoepsteen tot stoepsteen, dus inclusief een parallelle parkeerstrook</span>",
"render": "De breedte van deze rijbaan in deze straat is <strong>{width:carriageway}m</strong>" "render": "De breedte van deze rijbaan in deze straat is <strong>{width:carriageway}m</strong>"
} }
}, },

View file

@ -233,7 +233,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "Wer betreibt diesen Ort?", "question": "Wer betreibt diesen Ort?",
"render": "Dieser Ort wird betrieben von {operator}" "render": "Dieser Ort wird betrieben von {operator}"
@ -249,6 +250,7 @@
}, },
"question": "Hat dieser Ort eine Stromversorgung?" "question": "Hat dieser Ort eine Stromversorgung?"
} }
}
}, },
"shortDescription": "Finden Sie Plätze zum Übernachten mit Ihrem Wohnmobil", "shortDescription": "Finden Sie Plätze zum Übernachten mit Ihrem Wohnmobil",
"title": "Wohnmobilstellplätze" "title": "Wohnmobilstellplätze"
@ -428,20 +430,7 @@
} }
}, },
"overrideAll": { "overrideAll": {
"units+": { "tagRenderings+": {
"0": {
"applicableUnits": {
"0": {
"human": " Meter"
},
"1": {
"human": " Fuß"
}
}
}
}
},
"roamingRenderings": {
"0": { "0": {
"question": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?" "question": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?"
}, },
@ -548,6 +537,19 @@
"question": "Gibt es hier eine Speedkletter-Wand?" "question": "Gibt es hier eine Speedkletter-Wand?"
} }
}, },
"units+": {
"0": {
"applicableUnits": {
"0": {
"human": " Meter"
},
"1": {
"human": " Fuß"
}
}
}
}
},
"title": "Offene Kletterkarte" "title": "Offene Kletterkarte"
}, },
"cycle_highways": { "cycle_highways": {
@ -594,7 +596,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -616,6 +619,7 @@
"question": "Wann wird diese Straße eine Fahrradstraße?", "question": "Wann wird diese Straße eine Fahrradstraße?",
"render": "Diese Straße wird am {cyclestreet:start_date} zu einer Fahrradstraße" "render": "Diese Straße wird am {cyclestreet:start_date} zu einer Fahrradstraße"
} }
}
}, },
"shortDescription": "Eine Karte von Fahrradstraßen", "shortDescription": "Eine Karte von Fahrradstraßen",
"title": "Fahrradstraßen" "title": "Fahrradstraßen"

View file

@ -233,7 +233,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "Who operates this place?", "question": "Who operates this place?",
"render": "This place is operated by {operator}" "render": "This place is operated by {operator}"
@ -249,6 +250,7 @@
}, },
"question": "Does this place have a power supply?" "question": "Does this place have a power supply?"
} }
}
}, },
"shortDescription": "Find sites to spend the night with your camper", "shortDescription": "Find sites to spend the night with your camper",
"title": "Campersites" "title": "Campersites"
@ -452,20 +454,7 @@
} }
}, },
"overrideAll": { "overrideAll": {
"units+": { "tagRenderings+": {
"0": {
"applicableUnits": {
"0": {
"human": " meter"
},
"1": {
"human": " feet"
}
}
}
}
},
"roamingRenderings": {
"0": { "0": {
"question": "Is there a (unofficial) website with more informations (e.g. topos)?" "question": "Is there a (unofficial) website with more informations (e.g. topos)?"
}, },
@ -588,6 +577,19 @@
"question": "Is there a speed climbing wall?" "question": "Is there a speed climbing wall?"
} }
}, },
"units+": {
"0": {
"applicableUnits": {
"0": {
"human": " meter"
},
"1": {
"human": " feet"
}
}
}
}
},
"title": "Open Climbing Map" "title": "Open Climbing Map"
}, },
"cycle_highways": { "cycle_highways": {
@ -634,7 +636,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -656,6 +659,7 @@
"question": "When will this street become a cyclestreet?", "question": "When will this street become a cyclestreet?",
"render": "This street will become a cyclestreet at {cyclestreet:start_date}" "render": "This street will become a cyclestreet at {cyclestreet:start_date}"
} }
}
}, },
"shortDescription": "A map of cyclestreets", "shortDescription": "A map of cyclestreets",
"title": "Cyclestreets" "title": "Cyclestreets"

View file

@ -225,7 +225,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "Qui est lexploitant du site ?", "question": "Qui est lexploitant du site ?",
"render": "Ce site est exploité par {operator}" "render": "Ce site est exploité par {operator}"
@ -241,6 +242,7 @@
}, },
"question": "Ce site a-til une source délectricité ?" "question": "Ce site a-til une source délectricité ?"
} }
}
}, },
"shortDescription": "Trouver des sites pour passer la nuit avec votre camping-car", "shortDescription": "Trouver des sites pour passer la nuit avec votre camping-car",
"title": "Campings" "title": "Campings"
@ -439,20 +441,7 @@
} }
}, },
"overrideAll": { "overrideAll": {
"units+": { "tagRenderings+": {
"0": {
"applicableUnits": {
"0": {
"human": " mètres"
},
"1": {
"human": " pieds"
}
}
}
}
},
"roamingRenderings": {
"0": { "0": {
"question": "Existe-til un site avec plus dinformations (ex : topographie) ?" "question": "Existe-til un site avec plus dinformations (ex : topographie) ?"
}, },
@ -533,6 +522,19 @@
"question": "Est-il possible descalader à la moulinette ?" "question": "Est-il possible descalader à la moulinette ?"
} }
}, },
"units+": {
"0": {
"applicableUnits": {
"0": {
"human": " mètres"
},
"1": {
"human": " pieds"
}
}
}
}
},
"title": "Open Climbing Map" "title": "Open Climbing Map"
}, },
"cyclofix": { "cyclofix": {

View file

@ -52,7 +52,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"1": { "1": {
"mappings": { "mappings": {
"0": { "0": {
@ -64,6 +65,7 @@
} }
} }
} }
}
}, },
"charging_stations": { "charging_stations": {
"title": "Stasiun pengisian daya" "title": "Stasiun pengisian daya"

View file

@ -225,7 +225,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "Chi gestisce questo luogo?", "question": "Chi gestisce questo luogo?",
"render": "Questo luogo è gestito da {operator}" "render": "Questo luogo è gestito da {operator}"
@ -241,6 +242,7 @@
}, },
"question": "Questo luogo fornisce corrente elettrica?" "question": "Questo luogo fornisce corrente elettrica?"
} }
}
}, },
"shortDescription": "Trova aree dove passare la notte con il tuo camper", "shortDescription": "Trova aree dove passare la notte con il tuo camper",
"title": "Aree camper" "title": "Aree camper"
@ -310,7 +312,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"3": { "3": {
@ -323,6 +326,7 @@
"render": "Questa strada diventerà una strada ciclabile dal {cyclestreet:start_date}" "render": "Questa strada diventerà una strada ciclabile dal {cyclestreet:start_date}"
} }
} }
}
}, },
"cyclofix": { "cyclofix": {
"description": "Questa mappa offre a chi va in bici una soluzione semplice per trovare tutte le infrastrutture di cui ha bisogno.<br><br>Puoi tracciare la tua posizione esatta (solo su mobile) e selezionare i livelli che ti interessano nell'angolo in basso a sinistra. Puoi anche usare questo strumento per aggiungere o modificare punti di interesse alla mappa e aggiungere nuove informazioni rispendendo alle domande.<br><br>Tutte le modifiche che apporterai saranno automaticamente salvate nel database mondiale di OpenStreetMap e potranno essere liberamente riutilizzate da tutti e tutte.<br><br>Per maggiori informazioni sul progetto ciclofix, visita <a href='https://cyclofix.osm.be/'>cyclofix.osm.be</a>.", "description": "Questa mappa offre a chi va in bici una soluzione semplice per trovare tutte le infrastrutture di cui ha bisogno.<br><br>Puoi tracciare la tua posizione esatta (solo su mobile) e selezionare i livelli che ti interessano nell'angolo in basso a sinistra. Puoi anche usare questo strumento per aggiungere o modificare punti di interesse alla mappa e aggiungere nuove informazioni rispendendo alle domande.<br><br>Tutte le modifiche che apporterai saranno automaticamente salvate nel database mondiale di OpenStreetMap e potranno essere liberamente riutilizzate da tutti e tutte.<br><br>Per maggiori informazioni sul progetto ciclofix, visita <a href='https://cyclofix.osm.be/'>cyclofix.osm.be</a>.",

View file

@ -225,7 +225,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "この店は誰が経営しているんですか?", "question": "この店は誰が経営しているんですか?",
"render": "この場所は{operator}によって運営されます" "render": "この場所は{operator}によって運営されます"
@ -241,6 +242,7 @@
}, },
"question": "この場所に電源はありますか?" "question": "この場所に電源はありますか?"
} }
}
}, },
"shortDescription": "キャンパーと夜を共にするキャンプサイトを見つける", "shortDescription": "キャンパーと夜を共にするキャンプサイトを見つける",
"title": "キャンプサイト" "title": "キャンプサイト"
@ -379,7 +381,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "もっと情報のある(非公式の)ウェブサイトはありますか(例えば、topos)?" "question": "もっと情報のある(非公式の)ウェブサイトはありますか(例えば、topos)?"
}, },
@ -468,6 +471,7 @@
}, },
"question": "スピードクライミングウォールはありますか?" "question": "スピードクライミングウォールはありますか?"
} }
}
}, },
"title": "登山地図を開く" "title": "登山地図を開く"
}, },
@ -498,7 +502,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -520,6 +525,7 @@
"question": "この通りはいつcyclestreetになるんですか?", "question": "この通りはいつcyclestreetになるんですか?",
"render": "この通りは{cyclestreet:start_date}に、cyclestreetになります" "render": "この通りは{cyclestreet:start_date}に、cyclestreetになります"
} }
}
}, },
"shortDescription": "cyclestreetsの地図", "shortDescription": "cyclestreetsの地図",
"title": "Cyclestreets" "title": "Cyclestreets"

View file

@ -108,7 +108,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"7": { "7": {
"mappings": { "mappings": {
"0": { "0": {
@ -120,6 +121,7 @@
}, },
"question": "Er buldring mulig her?" "question": "Er buldring mulig her?"
} }
}
}, },
"title": "Åpent klatrekart" "title": "Åpent klatrekart"
}, },
@ -136,7 +138,8 @@
"name": "Alle gater" "name": "Alle gater"
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -154,6 +157,7 @@
}, },
"question": "Er denne gaten en sykkelvei?" "question": "Er denne gaten en sykkelvei?"
} }
}
}, },
"shortDescription": "Et kart over sykkelveier" "shortDescription": "Et kart over sykkelveier"
}, },

View file

@ -97,7 +97,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -159,6 +160,7 @@
"question": "Wat is de naam van dit gebied?", "question": "Wat is de naam van dit gebied?",
"render": "Dit gebied heet {name}" "render": "Dit gebied heet {name}"
} }
}
}, },
"shortDescription": "Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje", "shortDescription": "Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje",
"title": "Breng jouw buurtnatuur in kaart" "title": "Breng jouw buurtnatuur in kaart"
@ -368,20 +370,7 @@
} }
}, },
"overrideAll": { "overrideAll": {
"units+": { "tagRenderings+": {
"0": {
"applicableUnits": {
"0": {
"human": " meter"
},
"1": {
"human": " voet"
}
}
}
}
},
"roamingRenderings": {
"0": { "0": {
"question": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?" "question": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?"
}, },
@ -481,6 +470,19 @@
"question": "Is er een snelklimmuur (speed climbing)?" "question": "Is er een snelklimmuur (speed climbing)?"
} }
}, },
"units+": {
"0": {
"applicableUnits": {
"0": {
"human": " meter"
},
"1": {
"human": " voet"
}
}
}
}
},
"title": "Open klimkaart" "title": "Open klimkaart"
}, },
"cycle_infra": { "cycle_infra": {
@ -515,7 +517,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"mappings": { "mappings": {
"0": { "0": {
@ -537,6 +540,7 @@
"question": "Wanneer wordt deze straat een fietsstraat?", "question": "Wanneer wordt deze straat een fietsstraat?",
"render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}" "render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}"
} }
}
}, },
"shortDescription": "Een kaart met alle gekende fietsstraten", "shortDescription": "Een kaart met alle gekende fietsstraten",
"title": "Fietsstraten" "title": "Fietsstraten"

View file

@ -269,7 +269,8 @@
} }
} }
}, },
"roamingRenderings": { "overrideAll": {
"tagRenderings+": {
"0": { "0": {
"question": "Есть ли (неофициальный) веб-сайт с более подробной информацией (напр., topos)?" "question": "Есть ли (неофициальный) веб-сайт с более подробной информацией (напр., topos)?"
}, },
@ -290,6 +291,7 @@
} }
} }
} }
}
}, },
"title": "Открытая карта скалолазания" "title": "Открытая карта скалолазания"
}, },

14
package-lock.json generated
View file

@ -30,7 +30,7 @@
"jspdf": "^2.3.1", "jspdf": "^2.3.1",
"latlon2country": "^1.1.3", "latlon2country": "^1.1.3",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-providers": "^1.10.2", "leaflet-providers": "^1.13.0",
"leaflet-simple-map-screenshoter": "^0.4.4", "leaflet-simple-map-screenshoter": "^0.4.4",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"libphonenumber": "0.0.10", "libphonenumber": "0.0.10",
@ -10035,9 +10035,9 @@
"integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==" "integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
}, },
"node_modules/leaflet-providers": { "node_modules/leaflet-providers": {
"version": "1.12.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.12.0.tgz", "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.13.0.tgz",
"integrity": "sha512-pU/mR4B+NbayBGCg5/88dmRq7t1EGiNPhsVGV3yqHuDn594vIwus4CiPVW0RtiKJNKg8Vf1pILAbFl0i+yk+lQ==" "integrity": "sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg=="
}, },
"node_modules/leaflet-simple-map-screenshoter": { "node_modules/leaflet-simple-map-screenshoter": {
"version": "0.4.4", "version": "0.4.4",
@ -25965,9 +25965,9 @@
"integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==" "integrity": "sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw=="
}, },
"leaflet-providers": { "leaflet-providers": {
"version": "1.12.0", "version": "1.13.0",
"resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.12.0.tgz", "resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.13.0.tgz",
"integrity": "sha512-pU/mR4B+NbayBGCg5/88dmRq7t1EGiNPhsVGV3yqHuDn594vIwus4CiPVW0RtiKJNKg8Vf1pILAbFl0i+yk+lQ==" "integrity": "sha512-f/sN5wdgBbVA2jcCYzScIfYNxKdn2wBJP9bu+5cRX9Xj6g8Bt1G9Sr8WgJAt/ckIFIc3LVVxCBNFpSCfTuUElg=="
}, },
"leaflet-simple-map-screenshoter": { "leaflet-simple-map-screenshoter": {
"version": "0.4.4", "version": "0.4.4",

View file

@ -77,7 +77,7 @@
"jspdf": "^2.3.1", "jspdf": "^2.3.1",
"latlon2country": "^1.1.3", "latlon2country": "^1.1.3",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"leaflet-providers": "^1.10.2", "leaflet-providers": "^1.13.0",
"leaflet-simple-map-screenshoter": "^0.4.4", "leaflet-simple-map-screenshoter": "^0.4.4",
"leaflet.markercluster": "^1.4.1", "leaflet.markercluster": "^1.4.1",
"libphonenumber": "0.0.10", "libphonenumber": "0.0.10",

143
test/Actors.spec.ts Normal file
View file

@ -0,0 +1,143 @@
import T from "./TestHelper";
import State from "../State";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import SelectedElementTagsUpdater from "../Logic/Actors/SelectedElementTagsUpdater";
import UserRelatedState from "../Logic/State/UserRelatedState";
import {Utils} from "../Utils";
import ScriptUtils from "../scripts/ScriptUtils";
import SelectedFeatureHandler from "../Logic/Actors/SelectedFeatureHandler";
import {UIEventSource} from "../Logic/UIEventSource";
import {ElementStorage} from "../Logic/ElementStorage";
import Loc from "../Models/Loc";
export default class ActorsSpec extends T {
constructor() {
const latestTags = {
"amenity": "public_bookcase",
"books": "children;adults",
"capacity": "25",
"description": "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem",
"image:0": "https://i.imgur.com/Z8a69UG.jpg",
"name": "Stubbekwartier-buurtbibliotheek",
"nobrand": "yes",
"opening_hours": "24/7",
"operator": "Huisbewoner",
"public_bookcase:type": "reading_box"
}
Utils.injectJsonDownloadForTests(
"https://www.openstreetmap.org/api/0.6/node/5568693115",
{
"version": "0.6",
"generator": "CGImap 0.8.5 (1815943 spike-06.openstreetmap.org)",
"copyright": "OpenStreetMap and contributors",
"attribution": "http://www.openstreetmap.org/copyright",
"license": "http://opendatacommons.org/licenses/odbl/1-0/",
"elements": [{
"type": "node",
"id": 5568693115,
"lat": 51.2179199,
"lon": 3.2154662,
"timestamp": "2021-08-21T16:22:55Z",
"version": 6,
"changeset": 110034454,
"user": "Pieter Vander Vennet",
"uid": 3818858,
"tags": latestTags
}]
}
)
super("Actors", [
[
"download latest version",
() => {
const state = new UserRelatedState(AllKnownLayouts.allKnownLayouts.get("bookcases"))
const feature = {
"type": "Feature",
"id": "node/5568693115",
"properties": {
"amenity": "public_bookcase",
"books": "children;adults",
"capacity": "25",
"description": "Deze boekenruilkast vindt je recht tegenover de Pim Pam Poem",
"image:0": "https://i.imgur.com/Z8a69UG.jpg",
"name": "OUTDATED NAME",
"nobrand": "yes",
"opening_hours": "24/7",
"operator": "Huisbewoner",
"public_bookcase:type": "reading_box",
"id": "node/5568693115",
"_lat": "51.2179199",
"_lon": "3.2154662",
"fixme": "SOME FIXME"
},
"geometry": {
"type": "Point",
"coordinates": [
3.2154662,
51.2179199
]
},
"bbox": {
"maxLat": 51.2179199,
"maxLon": 3.2154662,
"minLat": 51.2179199,
"minLon": 3.2154662
},
"_lon": 3.2154662,
"_lat": 51.2179199
}
state.allElements.addOrGetElement(feature)
SelectedElementTagsUpdater.installCallback(state)
// THis should trigger a download of the latest feaures and update the tags
// However, this doesn't work with ts-node for some reason
state.selectedElement.setData(feature)
SelectedElementTagsUpdater.applyUpdate(state, latestTags, feature.properties.id)
// The name should be updated
T.equals("Stubbekwartier-buurtbibliotheek", feature.properties.name)
// The fixme should be removed
T.equals(undefined, feature.properties.fixme)
}],
["Hash without selected element should download geojson from OSM-API", async () => {
const hash = new UIEventSource("node/5568693115")
const selected = new UIEventSource(undefined)
const loc = new UIEventSource<Loc>({
lat: 0,
lon: 0,
zoom: 0
})
loc.addCallback(_ => {
T.equals("node/5568693115", selected.data.properties.id)
T.equals(14, loc.data.zoom)
T.equals( 51.2179199, loc.data.lat)
})
new SelectedFeatureHandler(hash, {
selectedElement: selected,
allElements: new ElementStorage(),
featurePipeline: undefined,
locationControl: loc,
layoutToUse: undefined
})
}]
]);
}
}

View file

@ -12,6 +12,7 @@ import {Utils} from "../Utils";
import TileFreshnessCalculatorSpec from "./TileFreshnessCalculator.spec"; import TileFreshnessCalculatorSpec from "./TileFreshnessCalculator.spec";
import WikidataSpecTest from "./Wikidata.spec.test"; import WikidataSpecTest from "./Wikidata.spec.test";
import ImageProviderSpec from "./ImageProvider.spec"; import ImageProviderSpec from "./ImageProvider.spec";
import ActorsSpec from "./Actors.spec";
ScriptUtils.fixUtils() ScriptUtils.fixUtils()
@ -27,7 +28,8 @@ const allTests = [
new SplitActionSpec(), new SplitActionSpec(),
new TileFreshnessCalculatorSpec(), new TileFreshnessCalculatorSpec(),
new WikidataSpecTest(), new WikidataSpecTest(),
new ImageProviderSpec() new ImageProviderSpec(),
new ActorsSpec()
] ]
Utils.externalDownloadFunction = async (url) => { Utils.externalDownloadFunction = async (url) => {

View file

@ -19,6 +19,7 @@ export default class T {
try { try {
test(); test();
} catch (e) { } catch (e) {
console.log("ERROR: ", e, e.stack)
failures.push({testsuite: this.name, name: name, msg: "" + e}); failures.push({testsuite: this.name, name: name, msg: "" + e});
} }
} }