Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-09-29 00:28:14 +02:00
commit 477ef56e00
202 changed files with 5785 additions and 2386 deletions

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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 }>
}

View file

@ -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"]

View file

@ -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()
}
}

View file

@ -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) {

View file

@ -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

View file

@ -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,
}
);
)
}
}

View file

@ -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
}
}

View file

@ -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(/&lt;/g,'<')?.replace(/&gt;/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(/&lt;/g, "<")?.replace(/&gt;/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"
}
}

View file

@ -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()
}
}