forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
477ef56e00
202 changed files with 5785 additions and 2386 deletions
|
@ -1,5 +1,5 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource";
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers";
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
|
||||
/**
|
||||
* Selects the appropriate raster layer as background for the given query parameter, theme setting, user preference or default value.
|
||||
|
@ -7,40 +7,47 @@ import { RasterLayerPolygon } from "../../Models/RasterLayers";
|
|||
* It the requested layer is not available, a layer of the same type will be selected.
|
||||
*/
|
||||
export class PreferredRasterLayerSelector {
|
||||
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>;
|
||||
private readonly _availableLayers: Store<RasterLayerPolygon[]>;
|
||||
private readonly _preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>;
|
||||
private readonly _queryParameter: UIEventSource<string>;
|
||||
private readonly _rasterLayerSetting: UIEventSource<RasterLayerPolygon>
|
||||
private readonly _availableLayers: Store<RasterLayerPolygon[]>
|
||||
private readonly _preferredBackgroundLayer: UIEventSource<
|
||||
string | "photo" | "map" | "osmbasedmap" | undefined
|
||||
>
|
||||
private readonly _queryParameter: UIEventSource<string>
|
||||
|
||||
constructor(rasterLayerSetting: UIEventSource<RasterLayerPolygon>, availableLayers: Store<RasterLayerPolygon[]>, queryParameter: UIEventSource<string>, preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>) {
|
||||
this._rasterLayerSetting = rasterLayerSetting;
|
||||
this._availableLayers = availableLayers;
|
||||
this._queryParameter = queryParameter;
|
||||
this._preferredBackgroundLayer = preferredBackgroundLayer;
|
||||
const self = this;
|
||||
constructor(
|
||||
rasterLayerSetting: UIEventSource<RasterLayerPolygon>,
|
||||
availableLayers: Store<RasterLayerPolygon[]>,
|
||||
queryParameter: UIEventSource<string>,
|
||||
preferredBackgroundLayer: UIEventSource<
|
||||
string | "photo" | "map" | "osmbasedmap" | undefined
|
||||
>
|
||||
) {
|
||||
this._rasterLayerSetting = rasterLayerSetting
|
||||
this._availableLayers = availableLayers
|
||||
this._queryParameter = queryParameter
|
||||
this._preferredBackgroundLayer = preferredBackgroundLayer
|
||||
const self = this
|
||||
|
||||
this._rasterLayerSetting.addCallbackD(layer => {
|
||||
this._rasterLayerSetting.addCallbackD((layer) => {
|
||||
if (layer.properties.id !== this._queryParameter.data) {
|
||||
this._queryParameter.setData(undefined);
|
||||
return true;
|
||||
this._queryParameter.setData(undefined)
|
||||
return true
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
this._queryParameter.addCallbackAndRunD(_ => {
|
||||
const isApplied = self.updateLayer();
|
||||
this._queryParameter.addCallbackAndRunD((_) => {
|
||||
const isApplied = self.updateLayer()
|
||||
if (!isApplied) {
|
||||
// A different layer was set as background
|
||||
// We remove this queryParameter instead
|
||||
self._queryParameter.setData(undefined);
|
||||
return true; // Unregister
|
||||
self._queryParameter.setData(undefined)
|
||||
return true // Unregister
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
this._preferredBackgroundLayer.addCallbackD(_ => self.updateLayer());
|
||||
|
||||
this._availableLayers.addCallbackD(_ => self.updateLayer());
|
||||
this._preferredBackgroundLayer.addCallbackD((_) => self.updateLayer())
|
||||
|
||||
this._availableLayers.addCallbackD((_) => self.updateLayer())
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,20 +55,19 @@ export class PreferredRasterLayerSelector {
|
|||
* @private
|
||||
*/
|
||||
private updateLayer() {
|
||||
|
||||
// What is the ID of the layer we have to (try to) load?
|
||||
const targetLayerId = this._queryParameter.data ?? this._preferredBackgroundLayer.data;
|
||||
const available = this._availableLayers.data;
|
||||
const isCategory = targetLayerId === "photo" || targetLayerId === "osmbasedmap" || targetLayerId === "map"
|
||||
const foundLayer = isCategory ? available.find(l => l.properties.category === targetLayerId) : available.find(l => l.properties.id === targetLayerId);
|
||||
const targetLayerId = this._queryParameter.data ?? this._preferredBackgroundLayer.data
|
||||
const available = this._availableLayers.data
|
||||
const isCategory =
|
||||
targetLayerId === "photo" || targetLayerId === "osmbasedmap" || targetLayerId === "map"
|
||||
const foundLayer = isCategory
|
||||
? available.find((l) => l.properties.category === targetLayerId)
|
||||
: available.find((l) => l.properties.id === targetLayerId)
|
||||
if (foundLayer) {
|
||||
this._rasterLayerSetting.setData(foundLayer);
|
||||
return true;
|
||||
this._rasterLayerSetting.setData(foundLayer)
|
||||
return true
|
||||
}
|
||||
|
||||
// The current layer is not in view
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,159 +1,159 @@
|
|||
import { ImageUploader } from "./ImageUploader";
|
||||
import LinkImageAction from "../Osm/Actions/LinkImageAction";
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
||||
import { OsmId, OsmTags } from "../../Models/OsmFeature";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import { Store, UIEventSource } from "../UIEventSource";
|
||||
import { OsmConnection } from "../Osm/OsmConnection";
|
||||
import { Changes } from "../Osm/Changes";
|
||||
import Translations from "../../UI/i18n/Translations";
|
||||
import NoteCommentElement from "../../UI/Popup/NoteCommentElement";
|
||||
|
||||
import { ImageUploader } from "./ImageUploader"
|
||||
import LinkImageAction from "../Osm/Actions/LinkImageAction"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import { OsmId, OsmTags } from "../../Models/OsmFeature"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { Changes } from "../Osm/Changes"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import NoteCommentElement from "../../UI/Popup/NoteCommentElement"
|
||||
|
||||
/**
|
||||
* The ImageUploadManager has a
|
||||
*/
|
||||
export class ImageUploadManager {
|
||||
private readonly _uploader: ImageUploader
|
||||
private readonly _featureProperties: FeaturePropertiesStore
|
||||
private readonly _layout: LayoutConfig
|
||||
|
||||
private readonly _uploader: ImageUploader;
|
||||
private readonly _featureProperties: FeaturePropertiesStore;
|
||||
private readonly _layout: LayoutConfig;
|
||||
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map()
|
||||
private readonly _osmConnection: OsmConnection
|
||||
private readonly _changes: Changes
|
||||
|
||||
private readonly _uploadStarted: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadFinished: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadFailed: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadRetried: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _uploadRetriedSuccess: Map<string, UIEventSource<number>> = new Map();
|
||||
private readonly _osmConnection: OsmConnection;
|
||||
private readonly _changes: Changes;
|
||||
|
||||
constructor(layout: LayoutConfig, uploader: ImageUploader, featureProperties: FeaturePropertiesStore, osmConnection: OsmConnection, changes: Changes) {
|
||||
this._uploader = uploader;
|
||||
this._featureProperties = featureProperties;
|
||||
this._layout = layout;
|
||||
this._osmConnection = osmConnection;
|
||||
this._changes = changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets various counters.
|
||||
* Note that counters can only increase
|
||||
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
|
||||
* @param featureId: the id of the feature you want information for. '*' has a global counter
|
||||
*/
|
||||
public getCountsFor(featureId: string | "*"): {
|
||||
retried: Store<number>;
|
||||
uploadStarted: Store<number>;
|
||||
retrySuccess: Store<number>;
|
||||
failed: Store<number>;
|
||||
uploadFinished: Store<number>
|
||||
} {
|
||||
return {
|
||||
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
|
||||
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
|
||||
retried: this.getCounterFor(this._uploadRetried, featureId),
|
||||
failed: this.getCounterFor(this._uploadFailed, featureId),
|
||||
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given image, applies the correct title and license for the known user.
|
||||
* Will then add this image to the OSM-feature or the OSM-note
|
||||
*/
|
||||
public async uploadImageAndApply(file: File, tagsStore: UIEventSource<OsmTags>) : Promise<void>{
|
||||
|
||||
const sizeInBytes = file.size;
|
||||
const tags= tagsStore.data
|
||||
const featureId = <OsmId>tags.id;
|
||||
const self = this;
|
||||
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
this.increaseCountFor(this._uploadStarted, featureId);
|
||||
this.increaseCountFor(this._uploadFailed, featureId);
|
||||
throw (
|
||||
Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: self._uploader.maxFileSizeInMegabytes + "MB"
|
||||
}).txt
|
||||
);
|
||||
constructor(
|
||||
layout: LayoutConfig,
|
||||
uploader: ImageUploader,
|
||||
featureProperties: FeaturePropertiesStore,
|
||||
osmConnection: OsmConnection,
|
||||
changes: Changes
|
||||
) {
|
||||
this._uploader = uploader
|
||||
this._featureProperties = featureProperties
|
||||
this._layout = layout
|
||||
this._osmConnection = osmConnection
|
||||
this._changes = changes
|
||||
}
|
||||
|
||||
|
||||
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0");
|
||||
const license = licenseStore?.data ?? "CC0";
|
||||
|
||||
const matchingLayer = this._layout?.getMatchingLayer(tags);
|
||||
|
||||
const title =
|
||||
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ??
|
||||
tags.name ??
|
||||
"https//osm.org/" + tags.id;
|
||||
const description = [
|
||||
"author:" + this._osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id
|
||||
].join("\n");
|
||||
|
||||
console.log("Upload done, creating ");
|
||||
const action = await this.uploadImageWithLicense(featureId, title, description, file);
|
||||
if(!isNaN(Number( featureId))){
|
||||
// THis is a map note
|
||||
const url = action._url
|
||||
await this._osmConnection.addCommentToNote(featureId, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>> tagsStore, {osmConnection: this._osmConnection})
|
||||
return
|
||||
/**
|
||||
* Gets various counters.
|
||||
* Note that counters can only increase
|
||||
* If a retry was a success, both 'retrySuccess' _and_ 'uploadFinished' will be increased
|
||||
* @param featureId: the id of the feature you want information for. '*' has a global counter
|
||||
*/
|
||||
public getCountsFor(featureId: string | "*"): {
|
||||
retried: Store<number>
|
||||
uploadStarted: Store<number>
|
||||
retrySuccess: Store<number>
|
||||
failed: Store<number>
|
||||
uploadFinished: Store<number>
|
||||
} {
|
||||
return {
|
||||
uploadStarted: this.getCounterFor(this._uploadStarted, featureId),
|
||||
uploadFinished: this.getCounterFor(this._uploadFinished, featureId),
|
||||
retried: this.getCounterFor(this._uploadRetried, featureId),
|
||||
failed: this.getCounterFor(this._uploadFailed, featureId),
|
||||
retrySuccess: this.getCounterFor(this._uploadRetriedSuccess, featureId),
|
||||
}
|
||||
}
|
||||
await this._changes.applyAction(action);
|
||||
}
|
||||
|
||||
private async uploadImageWithLicense(
|
||||
featureId: OsmId,
|
||||
title: string, description: string, blob: File
|
||||
): Promise<LinkImageAction> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId);
|
||||
const properties = this._featureProperties.getStore(featureId);
|
||||
let key: string;
|
||||
let value: string;
|
||||
try {
|
||||
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||
} catch (e) {
|
||||
this.increaseCountFor(this._uploadRetried, featureId);
|
||||
console.error("Could not upload image, trying again:", e);
|
||||
try {
|
||||
/**
|
||||
* Uploads the given image, applies the correct title and license for the known user.
|
||||
* Will then add this image to the OSM-feature or the OSM-note
|
||||
*/
|
||||
public async uploadImageAndApply(file: File, tagsStore: UIEventSource<OsmTags>): Promise<void> {
|
||||
const sizeInBytes = file.size
|
||||
const tags = tagsStore.data
|
||||
const featureId = <OsmId>tags.id
|
||||
const self = this
|
||||
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
this.increaseCountFor(this._uploadFailed, featureId)
|
||||
throw Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: self._uploader.maxFileSizeInMegabytes + "MB",
|
||||
}).txt
|
||||
}
|
||||
|
||||
({ key, value } = await this._uploader.uploadImage(title, description, blob));
|
||||
this.increaseCountFor(this._uploadRetriedSuccess, featureId);
|
||||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e);
|
||||
this.increaseCountFor(this._uploadFailed, featureId);
|
||||
}
|
||||
const licenseStore = this._osmConnection?.GetPreference("pictures-license", "CC0")
|
||||
const license = licenseStore?.data ?? "CC0"
|
||||
|
||||
const matchingLayer = this._layout?.getMatchingLayer(tags)
|
||||
|
||||
const title =
|
||||
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.textFor("en") ??
|
||||
tags.name ??
|
||||
"https//osm.org/" + tags.id
|
||||
const description = [
|
||||
"author:" + this._osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id,
|
||||
].join("\n")
|
||||
|
||||
console.log("Upload done, creating ")
|
||||
const action = await this.uploadImageWithLicense(featureId, title, description, file)
|
||||
if (!isNaN(Number(featureId))) {
|
||||
// THis is a map note
|
||||
const url = action._url
|
||||
await this._osmConnection.addCommentToNote(featureId, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tagsStore, {
|
||||
osmConnection: this._osmConnection,
|
||||
})
|
||||
return
|
||||
}
|
||||
await this._changes.applyAction(action)
|
||||
}
|
||||
console.log("Uploading done, creating action for", featureId);
|
||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||
theme: this._layout.id,
|
||||
changeType: "add-image"
|
||||
});
|
||||
this.increaseCountFor(this._uploadFinished, featureId);
|
||||
return action;
|
||||
}
|
||||
|
||||
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
if (this._featureProperties.aliases.has(key)) {
|
||||
key = this._featureProperties.aliases.get(key);
|
||||
private async uploadImageWithLicense(
|
||||
featureId: OsmId,
|
||||
title: string,
|
||||
description: string,
|
||||
blob: File
|
||||
): Promise<LinkImageAction> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
const properties = this._featureProperties.getStore(featureId)
|
||||
let key: string
|
||||
let value: string
|
||||
try {
|
||||
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
|
||||
} catch (e) {
|
||||
this.increaseCountFor(this._uploadRetried, featureId)
|
||||
console.error("Could not upload image, trying again:", e)
|
||||
try {
|
||||
;({ key, value } = await this._uploader.uploadImage(title, description, blob))
|
||||
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
|
||||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e)
|
||||
this.increaseCountFor(this._uploadFailed, featureId)
|
||||
}
|
||||
}
|
||||
console.log("Uploading done, creating action for", featureId)
|
||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||
theme: this._layout.id,
|
||||
changeType: "add-image",
|
||||
})
|
||||
this.increaseCountFor(this._uploadFinished, featureId)
|
||||
return action
|
||||
}
|
||||
if (!collection.has(key)) {
|
||||
collection.set(key, new UIEventSource<number>(0));
|
||||
|
||||
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
if (this._featureProperties.aliases.has(key)) {
|
||||
key = this._featureProperties.aliases.get(key)
|
||||
}
|
||||
if (!collection.has(key)) {
|
||||
collection.set(key, new UIEventSource<number>(0))
|
||||
}
|
||||
return collection.get(key)
|
||||
}
|
||||
return collection.get(key);
|
||||
}
|
||||
|
||||
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
const counter = this.getCounterFor(collection, key);
|
||||
counter.setData(counter.data + 1);
|
||||
const global = this.getCounterFor(collection, "*");
|
||||
global.setData(counter.data + 1);
|
||||
}
|
||||
|
||||
private increaseCountFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
const counter = this.getCounterFor(collection, key)
|
||||
counter.setData(counter.data + 1)
|
||||
const global = this.getCounterFor(collection, "*")
|
||||
global.setData(counter.data + 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export interface ImageUploader {
|
||||
maxFileSizeInMegabytes?: number;
|
||||
maxFileSizeInMegabytes?: number
|
||||
/**
|
||||
* Uploads the 'blob' as image, with some metadata.
|
||||
* Returns the URL to be linked + the appropriate key to add this to OSM
|
||||
|
@ -11,5 +11,5 @@ export interface ImageUploader {
|
|||
title: string,
|
||||
description: string,
|
||||
blob: File
|
||||
): Promise<{ key: string, value: string }>;
|
||||
): Promise<{ key: string; value: string }>
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import ImageProvider, { ProvidedImage } from "./ImageProvider";
|
||||
import BaseUIElement from "../../UI/BaseUIElement";
|
||||
import { Utils } from "../../Utils";
|
||||
import Constants from "../../Models/Constants";
|
||||
import { LicenseInfo } from "./LicenseInfo";
|
||||
import { ImageUploader } from "./ImageUploader";
|
||||
import Img from "../../UI/Base/Img";
|
||||
import ImageProvider, { ProvidedImage } from "./ImageProvider"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { LicenseInfo } from "./LicenseInfo"
|
||||
import { ImageUploader } from "./ImageUploader"
|
||||
|
||||
export class Imgur extends ImageProvider implements ImageUploader {
|
||||
public static readonly defaultValuePrefix = ["https://i.imgur.com"]
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import ChangeTagAction from "./ChangeTagAction";
|
||||
import { Tag } from "../../Tags/Tag";
|
||||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import { Changes } from "../Changes";
|
||||
import { ChangeDescription } from "./ChangeDescription";
|
||||
import { Store } from "../../UIEventSource";
|
||||
import ChangeTagAction from "./ChangeTagAction"
|
||||
import { Tag } from "../../Tags/Tag"
|
||||
import OsmChangeAction from "./OsmChangeAction"
|
||||
import { Changes } from "../Changes"
|
||||
import { ChangeDescription } from "./ChangeDescription"
|
||||
import { Store } from "../../UIEventSource"
|
||||
|
||||
export default class LinkImageAction extends OsmChangeAction {
|
||||
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string;
|
||||
public readonly _url: string;
|
||||
private readonly _currentTags: Store<Record<string, string>>;
|
||||
private readonly _meta: { theme: string; changeType: "add-image" | "link-image" };
|
||||
private readonly _proposedKey: "image" | "mapillary" | "wiki_commons" | string
|
||||
public readonly _url: string
|
||||
private readonly _currentTags: Store<Record<string, string>>
|
||||
private readonly _meta: { theme: string; changeType: "add-image" | "link-image" }
|
||||
|
||||
/**
|
||||
* Adds an image-link to a feature
|
||||
|
@ -31,10 +31,10 @@ export default class LinkImageAction extends OsmChangeAction {
|
|||
}
|
||||
) {
|
||||
super(elementId, true)
|
||||
this._proposedKey = proposedKey;
|
||||
this._url = url;
|
||||
this._currentTags = currentTags;
|
||||
this._meta = meta;
|
||||
this._proposedKey = proposedKey
|
||||
this._url = url
|
||||
this._currentTags = currentTags
|
||||
this._meta = meta
|
||||
}
|
||||
|
||||
protected CreateChangeDescriptions(): Promise<ChangeDescription[]> {
|
||||
|
@ -46,9 +46,12 @@ export default class LinkImageAction extends OsmChangeAction {
|
|||
key = this._proposedKey + ":" + i
|
||||
i++
|
||||
}
|
||||
const tagChangeAction = new ChangeTagAction ( this.mainObjectId, new Tag(key, url), currentTags, this._meta)
|
||||
const tagChangeAction = new ChangeTagAction(
|
||||
this.mainObjectId,
|
||||
new Tag(key, url),
|
||||
currentTags,
|
||||
this._meta
|
||||
)
|
||||
return tagChangeAction.CreateChangeDescriptions()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ export default abstract class OsmChangeAction {
|
|||
constructor(mainObjectId: string, trackStatistics: boolean = true) {
|
||||
this.trackStatistics = trackStatistics
|
||||
this.mainObjectId = mainObjectId
|
||||
if(mainObjectId === undefined || mainObjectId === null){
|
||||
throw "OsmObject received '"+mainObjectId+"' as mainObjectId"
|
||||
}
|
||||
}
|
||||
|
||||
public async Perform(changes: Changes) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import Locale from "../../UI/i18n/Locale"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { Changes } from "./Changes"
|
||||
import { Utils } from "../../Utils"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore";
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
|
||||
export interface ChangesetTag {
|
||||
key: string
|
||||
|
@ -30,11 +30,14 @@ export class ChangesetHandler {
|
|||
constructor(
|
||||
dryRun: Store<boolean>,
|
||||
osmConnection: OsmConnection,
|
||||
allElements: FeaturePropertiesStore | { addAlias: (id0: string, id1: string) => void } | undefined,
|
||||
allElements:
|
||||
| FeaturePropertiesStore
|
||||
| { addAlias: (id0: string, id1: string) => void }
|
||||
| undefined,
|
||||
changes: Changes
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this.allElements = <FeaturePropertiesStore> allElements
|
||||
this.allElements = <FeaturePropertiesStore>allElements
|
||||
this.changes = changes
|
||||
this._dryRun = dryRun
|
||||
this.userDetails = osmConnection.userDetails
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { UIEventSource } from "../UIEventSource";
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource";
|
||||
import { QueryParameters } from "../Web/QueryParameters";
|
||||
import { UIEventSource } from "../UIEventSource"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
|
||||
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
|
||||
|
||||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||
id: "gps";
|
||||
"user:location": "yes";
|
||||
date: string;
|
||||
id: "gps"
|
||||
"user:location": "yes"
|
||||
date: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,22 +23,22 @@ export class GeoLocationState {
|
|||
*/
|
||||
public readonly permission: UIEventSource<GeolocationPermissionState> = new UIEventSource(
|
||||
"prompt"
|
||||
);
|
||||
)
|
||||
|
||||
/**
|
||||
* Important to determine e.g. if we move automatically on fix or not
|
||||
*/
|
||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined);
|
||||
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
|
||||
/**
|
||||
* If true: the map will center (and re-center) to this location
|
||||
*/
|
||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true);
|
||||
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
|
||||
|
||||
/**
|
||||
* The latest GeoLocationCoordinates, as given by the WebAPI
|
||||
*/
|
||||
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
|
||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined);
|
||||
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
|
||||
|
||||
/**
|
||||
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
|
||||
|
@ -50,46 +50,46 @@ export class GeoLocationState {
|
|||
*/
|
||||
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
|
||||
LocalStorageSource.Get("geolocation-permissions")
|
||||
);
|
||||
)
|
||||
|
||||
/**
|
||||
* Used to detect a permission retraction
|
||||
*/
|
||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
|
||||
|
||||
constructor() {
|
||||
const self = this;
|
||||
const self = this
|
||||
|
||||
this.permission.addCallbackAndRunD(async (state) => {
|
||||
if (state === "granted") {
|
||||
self._previousLocationGrant.setData("true");
|
||||
self._grantedThisSession.setData(true);
|
||||
self._previousLocationGrant.setData("true")
|
||||
self._grantedThisSession.setData(true)
|
||||
}
|
||||
if (state === "prompt" && self._grantedThisSession.data) {
|
||||
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
|
||||
// This means that the rights have been revoked again!
|
||||
self._previousLocationGrant.setData("false");
|
||||
self.permission.setData("denied");
|
||||
self.currentGPSLocation.setData(undefined);
|
||||
console.warn("Detected a downgrade in permissions!");
|
||||
self._previousLocationGrant.setData("false")
|
||||
self.permission.setData("denied")
|
||||
self.currentGPSLocation.setData(undefined)
|
||||
console.warn("Detected a downgrade in permissions!")
|
||||
}
|
||||
if (state === "denied") {
|
||||
self._previousLocationGrant.setData("false");
|
||||
self._previousLocationGrant.setData("false")
|
||||
}
|
||||
});
|
||||
console.log("Previous location grant:", this._previousLocationGrant.data);
|
||||
})
|
||||
console.log("Previous location grant:", this._previousLocationGrant.data)
|
||||
if (this._previousLocationGrant.data === "true") {
|
||||
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
|
||||
|
||||
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
|
||||
this._previousLocationGrant.setData("false");
|
||||
console.log("Requesting access to GPS as this was previously granted");
|
||||
this._previousLocationGrant.setData("false")
|
||||
console.log("Requesting access to GPS as this was previously granted")
|
||||
const latLonGivenViaUrl =
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon");
|
||||
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
|
||||
if (!latLonGivenViaUrl) {
|
||||
this.requestMoment.setData(new Date());
|
||||
this.requestMoment.setData(new Date())
|
||||
}
|
||||
this.requestPermission();
|
||||
this.requestPermission()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,37 +101,36 @@ export class GeoLocationState {
|
|||
public requestPermission() {
|
||||
if (typeof navigator === "undefined") {
|
||||
// Not compatible with this browser
|
||||
this.permission.setData("denied");
|
||||
return;
|
||||
this.permission.setData("denied")
|
||||
return
|
||||
}
|
||||
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
|
||||
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
|
||||
// Hence that we continue the flow if it is "requested"
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
this.permission.setData("requested");
|
||||
this.permission.setData("requested")
|
||||
try {
|
||||
navigator?.permissions
|
||||
?.query({ name: "geolocation" })
|
||||
.then((status) => {
|
||||
const self = this;
|
||||
if(status.state === "granted" || status.state === "denied"){
|
||||
const self = this
|
||||
if (status.state === "granted" || status.state === "denied") {
|
||||
self.permission.setData(status.state)
|
||||
return
|
||||
}
|
||||
status.addEventListener("change", (e) => {
|
||||
self.permission.setData(status.state);
|
||||
|
||||
});
|
||||
self.permission.setData(status.state)
|
||||
})
|
||||
// The code above might have reset it to 'prompt', but we _did_ request permission!
|
||||
this.permission.setData("requested")
|
||||
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
|
||||
self.startWatching();
|
||||
self.startWatching()
|
||||
})
|
||||
.catch((e) => console.error("Could not get geopermission", e));
|
||||
.catch((e) => console.error("Could not get geopermission", e))
|
||||
} catch (e) {
|
||||
console.error("Could not get permission:", e);
|
||||
console.error("Could not get permission:", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,18 +139,18 @@ export class GeoLocationState {
|
|||
* @private
|
||||
*/
|
||||
private async startWatching() {
|
||||
const self = this;
|
||||
const self = this
|
||||
navigator.geolocation.watchPosition(
|
||||
function(position) {
|
||||
self.currentGPSLocation.setData(position.coords);
|
||||
self._previousLocationGrant.setData("true");
|
||||
function (position) {
|
||||
self.currentGPSLocation.setData(position.coords)
|
||||
self._previousLocationGrant.setData("true")
|
||||
},
|
||||
function() {
|
||||
console.warn("Could not get location with navigator.geolocation");
|
||||
function () {
|
||||
console.warn("Could not get location with navigator.geolocation")
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true
|
||||
enableHighAccuracy: true,
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import { OsmConnection } from "../Osm/OsmConnection";
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews";
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource";
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource";
|
||||
import { Feature } from "geojson";
|
||||
import { Utils } from "../../Utils";
|
||||
import translators from "../../assets/translators.json";
|
||||
import codeContributors from "../../assets/contributors.json";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
|
||||
import usersettings from "../../../src/assets/generated/layers/usersettings.json";
|
||||
import Locale from "../../UI/i18n/Locale";
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate";
|
||||
import FeatureSwitchState from "./FeatureSwitchState";
|
||||
import Constants from "../../Models/Constants";
|
||||
import { QueryParameters } from "../Web/QueryParameters";
|
||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging";
|
||||
import { MapProperties } from "../../Models/MapProperties";
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { MangroveIdentity } from "../Web/MangroveReviews"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Utils } from "../../Utils"
|
||||
import translators from "../../assets/translators.json"
|
||||
import codeContributors from "../../assets/contributors.json"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import usersettings from "../../../src/assets/generated/layers/usersettings.json"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
|
||||
import FeatureSwitchState from "./FeatureSwitchState"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
|
||||
/**
|
||||
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
|
||||
|
@ -42,8 +42,10 @@ export default class UserRelatedState {
|
|||
public readonly fixateNorth: UIEventSource<undefined | "yes">
|
||||
public readonly homeLocation: FeatureSource
|
||||
public readonly language: UIEventSource<string>
|
||||
public readonly preferredBackgroundLayer: UIEventSource<string | "photo" | "map" | "osmbasedmap" | undefined>
|
||||
public readonly imageLicense : UIEventSource<string>
|
||||
public readonly preferredBackgroundLayer: UIEventSource<
|
||||
string | "photo" | "map" | "osmbasedmap" | undefined
|
||||
>
|
||||
public readonly imageLicense: UIEventSource<string>
|
||||
/**
|
||||
* The number of seconds that the GPS-locations are stored in memory.
|
||||
* Time in seconds
|
||||
|
@ -61,7 +63,7 @@ export default class UserRelatedState {
|
|||
* Note: these are linked via OsmConnection.preferences which exports all preferences as UIEventSource
|
||||
*/
|
||||
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
|
||||
private readonly _mapProperties: MapProperties;
|
||||
private readonly _mapProperties: MapProperties
|
||||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
|
@ -71,7 +73,7 @@ export default class UserRelatedState {
|
|||
mapProperties?: MapProperties
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this._mapProperties = mapProperties;
|
||||
this._mapProperties = mapProperties
|
||||
{
|
||||
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
|
||||
this.osmConnection.GetPreference("translation-mode", "false")
|
||||
|
@ -104,12 +106,17 @@ export default class UserRelatedState {
|
|||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.GetLongPreference("identity", "mangrove")
|
||||
)
|
||||
this.preferredBackgroundLayer= this.osmConnection.GetPreference("preferred-background-layer", undefined, {
|
||||
documentation: "The ID of a layer or layer category that MapComplete uses by default"
|
||||
})
|
||||
this.preferredBackgroundLayer = this.osmConnection.GetPreference(
|
||||
"preferred-background-layer",
|
||||
undefined,
|
||||
{
|
||||
documentation:
|
||||
"The ID of a layer or layer category that MapComplete uses by default",
|
||||
}
|
||||
)
|
||||
|
||||
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
||||
documentation: "The license under which new images are uploaded"
|
||||
this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", {
|
||||
documentation: "The license under which new images are uploaded",
|
||||
})
|
||||
this.installedUserThemes = this.InitInstalledUserThemes()
|
||||
|
||||
|
@ -277,7 +284,6 @@ export default class UserRelatedState {
|
|||
amendedPrefs.data["__url_parameter_initialized:" + key] = "yes"
|
||||
}
|
||||
|
||||
|
||||
const osmConnection = this.osmConnection
|
||||
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
||||
for (const k in newPrefs) {
|
||||
|
@ -405,13 +411,11 @@ export default class UserRelatedState {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
this._mapProperties?.rasterLayer?.addCallbackAndRun(l => {
|
||||
this._mapProperties?.rasterLayer?.addCallbackAndRun((l) => {
|
||||
amendedPrefs.data["__current_background"] = l?.properties?.id
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
|
||||
|
||||
return amendedPrefs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,42 @@
|
|||
import { Utils } from "../../Utils"
|
||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||
export class ThemeMetaTagging {
|
||||
public static readonly themeName = "usersettings"
|
||||
public static readonly themeName = "usersettings"
|
||||
|
||||
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
||||
feat.properties._description
|
||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
||||
?.at(1)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_d",
|
||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_mastodon_candidate",
|
||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
||||
)
|
||||
feat.properties["__current_backgroun"] = "initial_value"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ export class MangroveIdentity {
|
|||
export default class FeatureReviews {
|
||||
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
|
||||
public readonly subjectUri: Store<string>
|
||||
public readonly average: Store<number | null>
|
||||
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||
new UIEventSource([])
|
||||
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||
|
@ -124,6 +125,23 @@ export default class FeatureReviews {
|
|||
console.log("Could not fetch reviews for partially incorrect query ", sub)
|
||||
}
|
||||
})
|
||||
this.average = this._reviews.map((reviews) => {
|
||||
if (!reviews) {
|
||||
return null
|
||||
}
|
||||
if (reviews.length === 0) {
|
||||
return null
|
||||
}
|
||||
let sum = 0
|
||||
let count = 0
|
||||
for (const review of reviews) {
|
||||
if (review.rating !== undefined) {
|
||||
count++
|
||||
sum += review.rating
|
||||
}
|
||||
}
|
||||
return Math.round(sum / count)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,6 +229,8 @@ export default class FeatureReviews {
|
|||
hasNew = true
|
||||
}
|
||||
if (hasNew) {
|
||||
self._reviews.data.sort((a, b) => b.iat - a.iat) // Sort with most recent first
|
||||
|
||||
self._reviews.ping()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue