forked from MapComplete/MapComplete
chore: automated housekeeping...
This commit is contained in:
parent
79b6927b56
commit
42ded4c1b1
328 changed files with 4062 additions and 1284 deletions
|
|
@ -8,11 +8,13 @@ import { FeatureSource, WritableFeatureSource } from "../FeatureSource/FeatureSo
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import StaticFeatureSource, { WritableStaticFeatureSource } from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import StaticFeatureSource, {
|
||||
WritableStaticFeatureSource,
|
||||
} from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
|
||||
("use strict")
|
||||
;("use strict")
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
* It'll move the map as appropriate given the state of the geolocation-API
|
||||
|
|
@ -29,7 +31,9 @@ export default class GeoLocationHandler {
|
|||
/**
|
||||
* All previously visited points (as 'Point'-objects), with their metadata
|
||||
*/
|
||||
public historicalUserLocations: WritableFeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||
public historicalUserLocations: WritableFeatureSource<
|
||||
Feature<Point, GeoLocationPointProperties>
|
||||
>
|
||||
|
||||
/**
|
||||
* A featureSource containing a single linestring which has the GPS-history of the user.
|
||||
|
|
@ -150,7 +154,8 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
private CopyGeolocationIntoMapstate() {
|
||||
const features: UIEventSource<Feature<Point, GeoLocationPointProperties>[]> = new UIEventSource<Feature<Point, GeoLocationPointProperties>[]>([])
|
||||
const features: UIEventSource<Feature<Point, GeoLocationPointProperties>[]> =
|
||||
new UIEventSource<Feature<Point, GeoLocationPointProperties>[]>([])
|
||||
this.currentUserLocation = new StaticFeatureSource(features)
|
||||
let i = 0
|
||||
this.geolocationState.currentGPSLocation.addCallbackAndRunD((location) => {
|
||||
|
|
@ -167,7 +172,7 @@ export default class GeoLocationHandler {
|
|||
altitudeAccuracy: location.altitudeAccuracy,
|
||||
heading: location.heading,
|
||||
alpha: Orientation.singleton.gotMeasurement.data
|
||||
? ("" + Orientation.singleton.alpha.data)
|
||||
? "" + Orientation.singleton.alpha.data
|
||||
: undefined,
|
||||
}
|
||||
i++
|
||||
|
|
@ -185,7 +190,10 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
private initUserLocationTrail() {
|
||||
const features = LocalStorageSource.getParsed<Feature<Point, GeoLocationPointProperties>[]>("gps_location_history", [])
|
||||
const features = LocalStorageSource.getParsed<Feature<Point, GeoLocationPointProperties>[]>(
|
||||
"gps_location_history",
|
||||
[]
|
||||
)
|
||||
const now = new Date().getTime()
|
||||
features.data = features.data.filter((ff) => {
|
||||
if (ff.properties === undefined) {
|
||||
|
|
@ -198,41 +206,45 @@ export default class GeoLocationHandler {
|
|||
)
|
||||
})
|
||||
features.ping()
|
||||
this.currentUserLocation?.features?.addCallbackAndRunD(([location]: [Feature<Point, GeoLocationPointProperties>]) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousLocation = <Feature<Point>>features.data[features.data.length - 1]
|
||||
if (previousLocation !== undefined) {
|
||||
const previousLocationFreshness = new Date(previousLocation.properties.date)
|
||||
const d = GeoOperations.distanceBetween(
|
||||
<[number, number]>previousLocation.geometry.coordinates,
|
||||
<[number, number]>location.geometry.coordinates
|
||||
)
|
||||
let timeDiff = Number.MAX_VALUE // in seconds
|
||||
const olderLocation = features.data[features.data.length - 2]
|
||||
|
||||
if (olderLocation !== undefined) {
|
||||
const olderLocationFreshness = new Date(olderLocation.properties.date)
|
||||
timeDiff =
|
||||
(new Date(previousLocationFreshness).getTime() -
|
||||
new Date(olderLocationFreshness).getTime()) /
|
||||
1000
|
||||
}
|
||||
if (d < 20 && timeDiff < 60) {
|
||||
// Do not append changes less then 20m - it's probably noise anyway
|
||||
this.currentUserLocation?.features?.addCallbackAndRunD(
|
||||
([location]: [Feature<Point, GeoLocationPointProperties>]) => {
|
||||
if (location === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const previousLocation = <Feature<Point>>features.data[features.data.length - 1]
|
||||
if (previousLocation !== undefined) {
|
||||
const previousLocationFreshness = new Date(previousLocation.properties.date)
|
||||
const d = GeoOperations.distanceBetween(
|
||||
<[number, number]>previousLocation.geometry.coordinates,
|
||||
<[number, number]>location.geometry.coordinates
|
||||
)
|
||||
let timeDiff = Number.MAX_VALUE // in seconds
|
||||
const olderLocation = features.data[features.data.length - 2]
|
||||
|
||||
if (olderLocation !== undefined) {
|
||||
const olderLocationFreshness = new Date(olderLocation.properties.date)
|
||||
timeDiff =
|
||||
(new Date(previousLocationFreshness).getTime() -
|
||||
new Date(olderLocationFreshness).getTime()) /
|
||||
1000
|
||||
}
|
||||
if (d < 20 && timeDiff < 60) {
|
||||
// Do not append changes less then 20m - it's probably noise anyway
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const feature = JSON.parse(JSON.stringify(location))
|
||||
feature.properties.id = "gps/" + features.data.length
|
||||
features.data.push(feature)
|
||||
features.ping()
|
||||
}
|
||||
)
|
||||
|
||||
const feature = JSON.parse(JSON.stringify(location))
|
||||
feature.properties.id = "gps/" + features.data.length
|
||||
features.data.push(feature)
|
||||
features.ping()
|
||||
})
|
||||
|
||||
this.historicalUserLocations = new WritableStaticFeatureSource<Feature<Point, GeoLocationPointProperties>>(features)
|
||||
this.historicalUserLocations = new WritableStaticFeatureSource<
|
||||
Feature<Point, GeoLocationPointProperties>
|
||||
>(features)
|
||||
|
||||
const asLine = features.map((allPoints) => {
|
||||
if (allPoints === undefined || allPoints.length < 2) {
|
||||
|
|
|
|||
|
|
@ -327,7 +327,11 @@ export class BBox {
|
|||
throw "BBOX has NAN"
|
||||
}
|
||||
if (this.minLat < -90 || this.maxLat > 90) {
|
||||
const msg = "Invalid BBOX detected: latitude is out of range. Did you swap lat & lon somewhere? min:" + this.minLat + "; max:" + this.maxLat
|
||||
const msg =
|
||||
"Invalid BBOX detected: latitude is out of range. Did you swap lat & lon somewhere? min:" +
|
||||
this.minLat +
|
||||
"; max:" +
|
||||
this.maxLat
|
||||
console.trace(msg)
|
||||
throw msg
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ export default class GeoIndexedStore implements FeatureSource {
|
|||
*/
|
||||
public GetFeaturesWithin(bbox: BBox): Feature[] {
|
||||
const bboxFeature = bbox.asGeojsonCached()
|
||||
return this.features.data.filter((f) =>
|
||||
GeoOperations.completelyWithin(f, bboxFeature))
|
||||
return this.features.data.filter((f) => GeoOperations.completelyWithin(f, bboxFeature))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export default class ChangeGeometryApplicator implements FeatureSource {
|
|||
|
||||
// Allright! We have a feature to rewrite!
|
||||
const copy = {
|
||||
...feature
|
||||
...feature,
|
||||
}
|
||||
// We only apply the last change as that one'll have the latest geometry
|
||||
const change = changesForFeature[changesForFeature.length - 1]
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import {
|
|||
MultiPolygon,
|
||||
Point,
|
||||
Polygon,
|
||||
Position
|
||||
Position,
|
||||
} from "geojson"
|
||||
import { Tiles } from "../Models/TileRange"
|
||||
import { Utils } from "../Utils"
|
||||
|
||||
("use strict")
|
||||
;("use strict")
|
||||
|
||||
export class GeoOperations {
|
||||
private static readonly _earthRadius: number = 6378137
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export default class AllImageProviders {
|
|||
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
|
||||
allSources.push(singleSource)
|
||||
}
|
||||
const source = Stores.fromStoresArray(allSources).map(result => {
|
||||
const source = Stores.fromStoresArray(allSources).map((result) => {
|
||||
const all = [].concat(...result)
|
||||
return Utils.DedupOnId(all, (i) => i?.id ?? i?.url)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ export interface ProvidedImage {
|
|||
}
|
||||
|
||||
export interface PanoramaView {
|
||||
url: string,
|
||||
url: string
|
||||
/**
|
||||
* 0 - 359
|
||||
* Degrees in which the picture is taken, with north = 0; going clockwise
|
||||
*/
|
||||
northOffset?: number,
|
||||
northOffset?: number
|
||||
pitchOffset?: number
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +54,6 @@ export interface HotspotProperties {
|
|||
pitch: number | "auto"
|
||||
|
||||
gotoPanorama: Feature<Point, PanoramaView>
|
||||
|
||||
}
|
||||
|
||||
export default abstract class ImageProvider {
|
||||
|
|
@ -125,7 +124,9 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract apiUrls(): string[]
|
||||
|
||||
public abstract getPanoramaInfo(image: { id: string }): Promise<Feature<Point, PanoramaView>> | undefined;
|
||||
public abstract getPanoramaInfo(image: {
|
||||
id: string
|
||||
}): Promise<Feature<Point, PanoramaView>> | undefined
|
||||
|
||||
public static async offerImageAsDownload(image: ProvidedImage) {
|
||||
const response = await fetch(image.url_hd ?? image.url)
|
||||
|
|
@ -134,5 +135,4 @@ export default abstract class ImageProvider {
|
|||
mimetype: "image/jpg",
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,16 @@ export class ImageUploadManager {
|
|||
* Keeps track of the _features_ for which an upload failed. Only used to give an indication to the user.
|
||||
* Every time an image upload fails, the featureID is added to the list. Not persisted (and should not be)
|
||||
*/
|
||||
private readonly _fails: UIEventSource<ImageUploadArguments[]> = new UIEventSource<ImageUploadArguments[]>([])
|
||||
public readonly fails: Store<string[]> = this._fails.map(args => args.map(a => a.featureId))
|
||||
private readonly _fails: UIEventSource<ImageUploadArguments[]> = new UIEventSource<
|
||||
ImageUploadArguments[]
|
||||
>([])
|
||||
public readonly fails: Store<string[]> = this._fails.map((args) => args.map((a) => a.featureId))
|
||||
/**
|
||||
* FeatureIDs of queued items
|
||||
*/
|
||||
public readonly queued: Store<string[]> = this._queue.imagesInQueue.map(queue => queue.map(q => q.featureId))
|
||||
public readonly queued: Store<string[]> = this._queue.imagesInQueue.map((queue) =>
|
||||
queue.map((q) => q.featureId)
|
||||
)
|
||||
public readonly queuedArgs = this._queue.imagesInQueue
|
||||
/**
|
||||
* The feature for which an upload is currently running
|
||||
|
|
@ -79,7 +83,7 @@ export class ImageUploadManager {
|
|||
if (sizeInBytes > this._uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
const error = Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: this._uploader.maxFileSizeInMegabytes + "MB"
|
||||
max_size: this._uploader.maxFileSizeInMegabytes + "MB",
|
||||
})
|
||||
return { error }
|
||||
}
|
||||
|
|
@ -118,7 +122,6 @@ export class ImageUploadManager {
|
|||
const tags: OsmTags = tagsStore.data
|
||||
const featureId = <OsmId | NoteId>tags.id
|
||||
|
||||
|
||||
const author = this._osmConnection?.userDetails?.data?.name ?? "Anonymous" // Might be a note upload
|
||||
|
||||
/**
|
||||
|
|
@ -134,13 +137,16 @@ export class ImageUploadManager {
|
|||
location,
|
||||
date: new Date().getTime(),
|
||||
layoutId: this._theme.id,
|
||||
author, blob: file, featureId, noblur, targetKey
|
||||
author,
|
||||
blob: file,
|
||||
featureId,
|
||||
noblur,
|
||||
targetKey,
|
||||
}
|
||||
console.log("Args are", args)
|
||||
|
||||
this._queue.add(args)
|
||||
this.uploadQueue()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -201,23 +207,29 @@ export class ImageUploadManager {
|
|||
this._fails.ping()
|
||||
return
|
||||
}
|
||||
this._fails.set(this._fails.data.filter(a => a !== args))
|
||||
let properties: UIEventSource<Record<string, string>> = this._featureProperties.getStore(args.featureId)
|
||||
this._fails.set(this._fails.data.filter((a) => a !== args))
|
||||
let properties: UIEventSource<Record<string, string>> = this._featureProperties.getStore(
|
||||
args.featureId
|
||||
)
|
||||
|
||||
if (args.featureId.startsWith("note/")) {
|
||||
// This is an OSM-note
|
||||
const url = result.absoluteUrl
|
||||
await this._osmConnection.addCommentToNote(args.featureId, url)
|
||||
const properties: UIEventSource<Record<string, string>> = this._featureProperties.getStore(args.featureId)
|
||||
const properties: UIEventSource<Record<string, string>> =
|
||||
this._featureProperties.getStore(args.featureId)
|
||||
if (properties) {
|
||||
// Properties will not be defined if the note isn't loaded, but that is no problem as the below code is only relevant if the note is shown
|
||||
NoteCommentElement.addCommentTo(url, properties, {
|
||||
osmConnection: this._osmConnection
|
||||
osmConnection: this._osmConnection,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (properties === undefined) {
|
||||
const downloaded = await new OsmObjectDownloader(this._osmConnection.Backend(), this._changes).DownloadObjectAsync(args.featureId)
|
||||
const downloaded = await new OsmObjectDownloader(
|
||||
this._osmConnection.Backend(),
|
||||
this._changes
|
||||
).DownloadObjectAsync(args.featureId)
|
||||
if (downloaded === "deleted") {
|
||||
this._queue.delete(args)
|
||||
return
|
||||
|
|
@ -232,7 +244,7 @@ export class ImageUploadManager {
|
|||
properties,
|
||||
{
|
||||
theme: properties?.data?.["_orig_theme"] ?? this._theme.id,
|
||||
changeType: "add-image"
|
||||
changeType: "add-image",
|
||||
}
|
||||
)
|
||||
await this._changes.applyAction(action)
|
||||
|
|
@ -240,7 +252,6 @@ export class ImageUploadManager {
|
|||
}
|
||||
|
||||
this._queue.delete(args)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -259,23 +270,15 @@ export class ImageUploadManager {
|
|||
* @private
|
||||
*/
|
||||
private async attemptSingleUpload(
|
||||
{
|
||||
featureId,
|
||||
author,
|
||||
blob,
|
||||
targetKey,
|
||||
noblur,
|
||||
location
|
||||
}: ImageUploadArguments,
|
||||
{ featureId, author, blob, targetKey, noblur, location }: ImageUploadArguments,
|
||||
reportOnFail: boolean
|
||||
): Promise<UploadResult | undefined> {
|
||||
|
||||
let key: string
|
||||
let value: string
|
||||
let absoluteUrl: string
|
||||
|
||||
try {
|
||||
({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
blob,
|
||||
location,
|
||||
author,
|
||||
|
|
@ -284,14 +287,13 @@ export class ImageUploadManager {
|
|||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e)
|
||||
if (reportOnFail) {
|
||||
|
||||
await this._reportError(
|
||||
e,
|
||||
JSON.stringify({
|
||||
ctx: "While uploading an image in the Image Upload Manager",
|
||||
featureId,
|
||||
author,
|
||||
targetKey
|
||||
targetKey,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
@ -304,5 +306,4 @@ export class ImageUploadManager {
|
|||
}
|
||||
return { key, absoluteUrl, value }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import { IdbLocalStorage } from "../Web/IdbLocalStorage"
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
|
||||
export interface ImageUploadArguments {
|
||||
featureId: string,
|
||||
readonly author: string,
|
||||
readonly blob: File,
|
||||
readonly targetKey: string | undefined,
|
||||
readonly noblur: boolean,
|
||||
readonly location: [number, number],
|
||||
featureId: string
|
||||
readonly author: string
|
||||
readonly blob: File
|
||||
readonly targetKey: string | undefined
|
||||
readonly noblur: boolean
|
||||
readonly location: [number, number]
|
||||
readonly layoutId: string
|
||||
readonly date: number
|
||||
}
|
||||
|
|
@ -17,14 +17,15 @@ export interface ImageUploadArguments {
|
|||
* It is backed up in the indexedDB as to not drop images in case of connection problems
|
||||
*/
|
||||
export default class ImageUploadQueue {
|
||||
|
||||
public static readonly singleton = new ImageUploadQueue()
|
||||
private readonly _imagesInQueue: UIEventSource<ImageUploadArguments[]>
|
||||
|
||||
public readonly imagesInQueue: Store<ImageUploadArguments[]>
|
||||
|
||||
private constructor() {
|
||||
this._imagesInQueue = IdbLocalStorage.Get<ImageUploadArguments[]>("failed-images-backup", { defaultValue: [] })
|
||||
this._imagesInQueue = IdbLocalStorage.Get<ImageUploadArguments[]>("failed-images-backup", {
|
||||
defaultValue: [],
|
||||
})
|
||||
this.imagesInQueue = this._imagesInQueue
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +45,6 @@ export default class ImageUploadQueue {
|
|||
}
|
||||
|
||||
applyRemapping(oldId: string, newId: string) {
|
||||
|
||||
let hasChange = false
|
||||
for (const img of this._imagesInQueue.data) {
|
||||
if (img.featureId === oldId) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export class Imgur extends ImageProvider {
|
|||
key: key,
|
||||
provider: this,
|
||||
id: value,
|
||||
isSpherical: false
|
||||
isSpherical: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export class Mapillary extends ImageProvider {
|
|||
"http://mapillary.com",
|
||||
"https://mapillary.com",
|
||||
"http://www.mapillary.com",
|
||||
"https://www.mapillary.com"
|
||||
"https://www.mapillary.com",
|
||||
]
|
||||
defaultKeyPrefixes = ["mapillary", "image"]
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export class Mapillary extends ImageProvider {
|
|||
lat: location?.lat,
|
||||
lng: location?.lon,
|
||||
z: location === undefined ? undefined : Math.max((zoom ?? 2) - 1, 1),
|
||||
pKey
|
||||
pKey,
|
||||
}
|
||||
const baselink = `https://www.mapillary.com/app/?`
|
||||
const paramsStr = Utils.NoNull(
|
||||
|
|
@ -140,41 +140,39 @@ export class Mapillary extends ImageProvider {
|
|||
return [img]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Download data necessary for the 360°-viewer
|
||||
* @param pkey
|
||||
* @constructor
|
||||
*/
|
||||
public async getPanoramaInfo(image: { id: number | string }): Promise<Feature<Point, PanoramaView>> {
|
||||
public async getPanoramaInfo(image: {
|
||||
id: number | string
|
||||
}): Promise<Feature<Point, PanoramaView>> {
|
||||
const pkey = image.id
|
||||
const metadataUrl =
|
||||
"https://graph.mapillary.com/" +
|
||||
pkey +
|
||||
"?fields=computed_compass_angle,geometry,is_pano,thumb_2048_url,thumb_original_url&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached<
|
||||
{
|
||||
computed_compass_angle: number,
|
||||
geometry: Point,
|
||||
const response = await Utils.downloadJsonCached<{
|
||||
computed_compass_angle: number
|
||||
geometry: Point
|
||||
|
||||
is_pano: boolean,
|
||||
thumb_2048_url: string,
|
||||
thumb_original_url: string,
|
||||
id: string,
|
||||
|
||||
}>(metadataUrl, 60 * 60)
|
||||
is_pano: boolean
|
||||
thumb_2048_url: string
|
||||
thumb_original_url: string
|
||||
id: string
|
||||
}>(metadataUrl, 60 * 60)
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: response.geometry,
|
||||
properties: {
|
||||
url: response.thumb_2048_url,
|
||||
northOffset: response.computed_compass_angle
|
||||
}
|
||||
northOffset: response.computed_compass_angle,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async DownloadAttribution(providedImage: { id: string }): Promise<LicenseInfo> {
|
||||
const mapillaryId = providedImage.id
|
||||
const metadataUrl =
|
||||
|
|
@ -183,7 +181,10 @@ export class Mapillary extends ImageProvider {
|
|||
"?fields=thumb_1024_url,thumb_original_url,captured_at,creator&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached<{
|
||||
thumb_1024_url: string, thumb_original_url: string, captured_at, creator: string
|
||||
thumb_1024_url: string
|
||||
thumb_original_url: string
|
||||
captured_at
|
||||
creator: string
|
||||
}>(metadataUrl, 60 * 60)
|
||||
|
||||
const license = new LicenseInfo()
|
||||
|
|
@ -207,13 +208,13 @@ export class Mapillary extends ImageProvider {
|
|||
"?fields=thumb_1024_url,thumb_original_url,captured_at,compass_angle,geometry,computed_geometry,creator,camera_type&access_token=" +
|
||||
Constants.mapillary_client_token_v4
|
||||
const response = await Utils.downloadJsonCached<{
|
||||
thumb_1024_url: string,
|
||||
thumb_original_url: string,
|
||||
captured_at,
|
||||
compass_angle: number,
|
||||
creator: string,
|
||||
computed_geometry: Point,
|
||||
geometry: Point,
|
||||
thumb_1024_url: string
|
||||
thumb_original_url: string
|
||||
captured_at
|
||||
compass_angle: number
|
||||
creator: string
|
||||
computed_geometry: Point
|
||||
geometry: Point
|
||||
camera_type: "equirectangular" | "spherical" | string
|
||||
}>(metadataUrl, 60 * 60)
|
||||
const url = <string>response["thumb_1024_url"]
|
||||
|
|
@ -230,9 +231,10 @@ export class Mapillary extends ImageProvider {
|
|||
date,
|
||||
key,
|
||||
rotation,
|
||||
isSpherical: response.camera_type === "spherical" || response.camera_type === "equirectangular",
|
||||
isSpherical:
|
||||
response.camera_type === "spherical" || response.camera_type === "equirectangular",
|
||||
lat: geometry.coordinates[1],
|
||||
lon: geometry.coordinates[0]
|
||||
lon: geometry.coordinates[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,7 +191,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
return new Panoramax(host)
|
||||
}
|
||||
|
||||
public async getPanoramaInfo(image: { id: string }): Promise<Feature<Point, PanoramaView>> | undefined {
|
||||
public async getPanoramaInfo(image: {
|
||||
id: string
|
||||
}): Promise<Feature<Point, PanoramaView>> | undefined {
|
||||
const imageInfo = await PanoramaxImageProvider.xyz.imageInfo(image.id)
|
||||
const url = (imageInfo.assets.sd ?? imageInfo.assets.thumb ?? imageInfo.assets.hd).href
|
||||
const northOffset = imageInfo.properties["view:azimuth"]
|
||||
|
|
@ -200,8 +202,10 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
type: "Feature",
|
||||
geometry: imageInfo.geometry,
|
||||
properties: {
|
||||
url, northOffset, pitchOffset
|
||||
}
|
||||
url,
|
||||
northOffset,
|
||||
pitchOffset,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ import { Utils } from "../../Utils"
|
|||
import { Feature, Point } from "geojson"
|
||||
|
||||
export class WikidataImageProvider extends ImageProvider {
|
||||
|
||||
|
||||
public static readonly singleton = new WikidataImageProvider()
|
||||
public readonly defaultKeyPrefixes = ["wikidata"]
|
||||
public readonly name = "Wikidata"
|
||||
private static readonly keyBlacklist: ReadonlySet<string> = new Set([
|
||||
"mapillary",
|
||||
...Utils.Times((i) => "mapillary:" + i, 10)
|
||||
...Utils.Times((i) => "mapillary:" + i, 10),
|
||||
])
|
||||
|
||||
private constructor() {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
key: undefined,
|
||||
provider: this,
|
||||
id: image,
|
||||
isSpherical: false
|
||||
isSpherical: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,9 @@ export class Changes {
|
|||
public readonly backend: string
|
||||
public readonly isUploading = new UIEventSource(false)
|
||||
public readonly errors = new UIEventSource<string[]>([], "upload-errors")
|
||||
private readonly historicalUserLocations?: FeatureSource<Feature<Point, GeoLocationPointProperties>>
|
||||
private readonly historicalUserLocations?: FeatureSource<
|
||||
Feature<Point, GeoLocationPointProperties>
|
||||
>
|
||||
private _nextId: number = 0 // Newly assigned ID's are negative
|
||||
private readonly previouslyCreated: OsmObject[] = []
|
||||
private readonly _leftRightSensitive: boolean
|
||||
|
|
@ -67,7 +69,7 @@ export class Changes {
|
|||
if (isNaN(this._nextId) && state.reportError !== undefined) {
|
||||
state.reportError(
|
||||
"Got a NaN as nextID. Pending changes IDs are:" +
|
||||
this.pendingChanges.data?.map((pch) => pch?.id).join(".")
|
||||
this.pendingChanges.data?.map((pch) => pch?.id).join(".")
|
||||
)
|
||||
this._nextId = -100
|
||||
}
|
||||
|
|
@ -91,19 +93,22 @@ export class Changes {
|
|||
return new Changes({
|
||||
osmConnection: new OsmConnection(),
|
||||
featureSwitches: {
|
||||
featureSwitchIsTesting: new ImmutableStore(true)
|
||||
}
|
||||
featureSwitchIsTesting: new ImmutableStore(true),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public static async createChangesetXMLForJosm(actions: OsmChangeAction[], osmConnection?: OsmConnection): Promise<string> {
|
||||
public static async createChangesetXMLForJosm(
|
||||
actions: OsmChangeAction[],
|
||||
osmConnection?: OsmConnection
|
||||
): Promise<string> {
|
||||
osmConnection ??= new OsmConnection()
|
||||
const changes = new Changes({
|
||||
osmConnection
|
||||
osmConnection,
|
||||
})
|
||||
const descriptions: ChangeDescription[] = []
|
||||
for (const action of actions) {
|
||||
descriptions.push(...await action.Perform(changes))
|
||||
descriptions.push(...(await action.Perform(changes)))
|
||||
}
|
||||
const downloader = new OsmObjectDownloader(osmConnection.Backend(), undefined)
|
||||
const downloaded: OsmObject[] = []
|
||||
|
|
@ -114,7 +119,10 @@ export class Changes {
|
|||
}
|
||||
downloaded.push(osmObj)
|
||||
}
|
||||
return Changes.buildChangesetXML("", changes.CreateChangesetObjects(descriptions, downloaded))
|
||||
return Changes.buildChangesetXML(
|
||||
"",
|
||||
changes.CreateChangesetObjects(descriptions, downloaded)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -179,50 +187,50 @@ export class Changes {
|
|||
[
|
||||
{
|
||||
key: "comment",
|
||||
docs: "The changeset comment. Will be a fixed string, mentioning the theme"
|
||||
docs: "The changeset comment. Will be a fixed string, mentioning the theme",
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
docs: "The name of the theme that was used to create this change. "
|
||||
docs: "The name of the theme that was used to create this change. ",
|
||||
},
|
||||
{
|
||||
key: "source",
|
||||
value: "survey",
|
||||
docs: "The contributor had their geolocation enabled while making changes"
|
||||
docs: "The contributor had their geolocation enabled while making changes",
|
||||
},
|
||||
{
|
||||
key: "change_within_{distance}",
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping"
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. This gives an indication of proximity and if they truly surveyed or were armchair-mapping",
|
||||
},
|
||||
{
|
||||
key: "change_over_{distance}",
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping"
|
||||
docs: "If the contributor enabled their geolocation, this will hint how far away they were from the objects they edited. If they were over 5000m away, the might have been armchair-mapping",
|
||||
},
|
||||
{
|
||||
key: "created_by",
|
||||
value: "MapComplete <version>",
|
||||
docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number"
|
||||
docs: "The piece of software used to create this changeset; will always start with MapComplete, followed by the version number",
|
||||
},
|
||||
{
|
||||
key: "locale",
|
||||
value: "en|nl|de|...",
|
||||
docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks."
|
||||
docs: "The code of the language that the contributor used MapComplete in. Hints what language the user speaks.",
|
||||
},
|
||||
{
|
||||
key: "host",
|
||||
value: "https://mapcomplete.org/<theme>",
|
||||
docs: "The URL that the contributor used to make changes. One can see the used instance with this"
|
||||
docs: "The URL that the contributor used to make changes. One can see the used instance with this",
|
||||
},
|
||||
{
|
||||
key: "imagery",
|
||||
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)"
|
||||
}
|
||||
docs: "The identifier of the used background layer, this will probably be an identifier from the [editor layer index](https://github.com/osmlab/editor-layer-index)",
|
||||
},
|
||||
],
|
||||
"default"
|
||||
),
|
||||
...addSource(ChangeTagAction.metatags, "ChangeTag"),
|
||||
...addSource(ChangeLocationAction.metatags, "ChangeLocation"),
|
||||
...addSource(DeleteAction.metatags, "DeleteAction")
|
||||
...addSource(DeleteAction.metatags, "DeleteAction"),
|
||||
// TODO
|
||||
/*
|
||||
...DeleteAction.metatags,
|
||||
|
|
@ -244,11 +252,11 @@ export class Changes {
|
|||
docs,
|
||||
specialMotivation
|
||||
? "This might give a reason per modified node or way"
|
||||
: ""
|
||||
: "",
|
||||
].join("\n"),
|
||||
source
|
||||
source,
|
||||
])
|
||||
)
|
||||
),
|
||||
].join("\n\n")
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +275,7 @@ export class Changes {
|
|||
this._changesetHandler._remappings.has("node/" + this._nextId) ||
|
||||
this._changesetHandler._remappings.has("way/" + this._nextId) ||
|
||||
this._changesetHandler._remappings.has("relation/" + this._nextId)
|
||||
)
|
||||
)
|
||||
return this._nextId
|
||||
}
|
||||
|
||||
|
|
@ -504,7 +512,7 @@ export class Changes {
|
|||
const result = {
|
||||
newObjects: [],
|
||||
modifiedObjects: [],
|
||||
deletedObjects: []
|
||||
deletedObjects: [],
|
||||
}
|
||||
|
||||
objects.forEach((v, id) => {
|
||||
|
|
@ -665,7 +673,7 @@ export class Changes {
|
|||
} else {
|
||||
this._reportError(
|
||||
`Got an orphaned change. The 'creation'-change description for ${c.type}/${c.id} got lost. Permanently dropping this change:` +
|
||||
JSON.stringify(c)
|
||||
JSON.stringify(c)
|
||||
)
|
||||
}
|
||||
return
|
||||
|
|
@ -676,10 +684,10 @@ export class Changes {
|
|||
} else {
|
||||
console.log(
|
||||
"Refusing change about " +
|
||||
c.type +
|
||||
"/" +
|
||||
c.id +
|
||||
" as not in the objects. No internet?"
|
||||
c.type +
|
||||
"/" +
|
||||
c.id +
|
||||
" as not in the objects. No internet?"
|
||||
)
|
||||
refused.push(c)
|
||||
}
|
||||
|
|
@ -694,7 +702,7 @@ export class Changes {
|
|||
*/
|
||||
private async flushSelectChanges(
|
||||
pending: ChangeDescription[],
|
||||
openChangeset: UIEventSource<{ id: number, opened: number }>
|
||||
openChangeset: UIEventSource<{ id: number; opened: number }>
|
||||
): Promise<ChangeDescription[]> {
|
||||
const neededIds = Changes.GetNeededIds(pending)
|
||||
/* Download the latest version of the OSM-objects
|
||||
|
|
@ -775,14 +783,14 @@ export class Changes {
|
|||
([key, count]) => ({
|
||||
key: key,
|
||||
value: count,
|
||||
aggregate: true
|
||||
aggregate: true,
|
||||
})
|
||||
)
|
||||
const motivations = pending
|
||||
.filter((descr) => descr.meta.specialMotivation !== undefined)
|
||||
.map((descr) => ({
|
||||
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
|
||||
value: descr.meta.specialMotivation
|
||||
value: descr.meta.specialMotivation,
|
||||
}))
|
||||
|
||||
const distances = Utils.NoNull(pending.map((descr) => descr.meta.distanceToObject))
|
||||
|
|
@ -813,7 +821,7 @@ export class Changes {
|
|||
return {
|
||||
key,
|
||||
value: count,
|
||||
aggregate: true
|
||||
aggregate: true,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
|
@ -828,20 +836,19 @@ export class Changes {
|
|||
const metatags: ChangesetTag[] = [
|
||||
{
|
||||
key: "comment",
|
||||
value: comment
|
||||
value: comment,
|
||||
},
|
||||
{
|
||||
key: "theme",
|
||||
value: theme
|
||||
value: theme,
|
||||
},
|
||||
...perType,
|
||||
...motivations,
|
||||
...perBinMessage
|
||||
...perBinMessage,
|
||||
]
|
||||
return metatags
|
||||
}
|
||||
|
||||
|
||||
private async flushChangesAsync(): Promise<void> {
|
||||
try {
|
||||
// At last, we build the changeset and upload
|
||||
|
|
@ -862,9 +869,9 @@ export class Changes {
|
|||
const openChangeset = this.state.osmConnection.getCurrentChangesetFor(theme)
|
||||
console.log(
|
||||
"Using current-open-changeset-" +
|
||||
theme +
|
||||
" from the preferences, got " +
|
||||
openChangeset.data
|
||||
theme +
|
||||
" from the preferences, got " +
|
||||
openChangeset.data
|
||||
)
|
||||
|
||||
const refused = await this.flushSelectChanges(pendingChanges, openChangeset)
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ export class ChangesetHandler {
|
|||
|
||||
private async UploadWithNew(
|
||||
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
||||
openChangeset: UIEventSource<{ id: number, opened: number }>,
|
||||
openChangeset: UIEventSource<{ id: number; opened: number }>,
|
||||
extraMetaTags: ChangesetTag[]
|
||||
) {
|
||||
const csId = await this.OpenChangeset(extraMetaTags)
|
||||
|
|
@ -146,7 +146,7 @@ export class ChangesetHandler {
|
|||
public async UploadChangeset(
|
||||
generateChangeXML: (csid: number, remappings: Map<string, string>) => string,
|
||||
extraMetaTags: ChangesetTag[],
|
||||
openChangeset: UIEventSource<{ id: number, opened: number }>
|
||||
openChangeset: UIEventSource<{ id: number; opened: number }>
|
||||
): Promise<void> {
|
||||
if (
|
||||
!extraMetaTags.some((tag) => tag.key === "comment") ||
|
||||
|
|
@ -171,8 +171,9 @@ export class ChangesetHandler {
|
|||
|
||||
console.log("Trying to reuse changeset", openChangeset.data)
|
||||
const now = new Date()
|
||||
const changesetIsUsable = openChangeset.data !== undefined &&
|
||||
(now.getTime() - openChangeset.data.opened < 24 * 60 * 60 * 1000)
|
||||
const changesetIsUsable =
|
||||
openChangeset.data !== undefined &&
|
||||
now.getTime() - openChangeset.data.opened < 24 * 60 * 60 * 1000
|
||||
if (changesetIsUsable) {
|
||||
try {
|
||||
const csId = openChangeset.data
|
||||
|
|
|
|||
|
|
@ -246,13 +246,20 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public getPreference<T extends string = string>(
|
||||
key: string, options?: {
|
||||
defaultValue?: string,
|
||||
prefix?: "mapcomplete-" | string,
|
||||
key: string,
|
||||
options?: {
|
||||
defaultValue?: string
|
||||
prefix?: "mapcomplete-" | string
|
||||
saveToLocalStorage?: true | boolean
|
||||
}
|
||||
): UIEventSource<T | undefined> {
|
||||
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, options?.defaultValue, options?.prefix ?? "mapcomplete-")
|
||||
return <UIEventSource<T>>(
|
||||
this.preferencesHandler.getPreference(
|
||||
key,
|
||||
options?.defaultValue,
|
||||
options?.prefix ?? "mapcomplete-"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
public LogOut() {
|
||||
|
|
@ -735,10 +742,8 @@ export class OsmConnection {
|
|||
}
|
||||
|
||||
public getCurrentChangesetFor(theme: string) {
|
||||
return UIEventSource.asObject<{ id: number, opened: number }>(
|
||||
this.GetPreference(
|
||||
"current-changeset-" + theme
|
||||
),
|
||||
return UIEventSource.asObject<{ id: number; opened: number }>(
|
||||
this.GetPreference("current-changeset-" + theme),
|
||||
undefined
|
||||
)
|
||||
}
|
||||
|
|
@ -748,9 +753,10 @@ export class OsmConnection {
|
|||
*/
|
||||
public getAllOpenChangesetsPreferences(): Store<string[]> {
|
||||
const prefix = "current-changeset-"
|
||||
return this.preferencesHandler.allPreferences.map(dict =>
|
||||
return this.preferencesHandler.allPreferences.map((dict) =>
|
||||
Object.keys(dict)
|
||||
.filter(k => k.startsWith(prefix))
|
||||
.map(k => k.substring(prefix.length)))
|
||||
.filter((k) => k.startsWith(prefix))
|
||||
.map((k) => k.substring(prefix.length))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,8 +91,16 @@ export class OsmPreferences {
|
|||
}
|
||||
}
|
||||
|
||||
public getPreference(key: string, defaultValue: string = undefined, prefix?: string, saveLocally = true) {
|
||||
return this.getPreferenceSeedFromlocal(key, defaultValue, { prefix, saveToLocalStorage: saveLocally })
|
||||
public getPreference(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
prefix?: string,
|
||||
saveLocally = true
|
||||
) {
|
||||
return this.getPreferenceSeedFromlocal(key, defaultValue, {
|
||||
prefix,
|
||||
saveToLocalStorage: saveLocally,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,7 +151,6 @@ export class OsmPreferences {
|
|||
* OsmPreferences.mergeDict({abc: "123", def: "123", "def:0": "456", "def:1":"789"}) // => {abc: "123", def: "123456789"}
|
||||
*/
|
||||
private static mergeDict(dict: Record<string, string>): Record<string, string> {
|
||||
|
||||
const keyParts: Record<string, Record<number, string>> = {}
|
||||
const endsWithNumber = /:[0-9]+$/
|
||||
for (const key of Object.keys(dict)) {
|
||||
|
|
@ -167,7 +174,6 @@ export class OsmPreferences {
|
|||
}
|
||||
subparts[""] = dict[key]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const newDict = {}
|
||||
|
|
@ -199,7 +205,7 @@ export class OsmPreferences {
|
|||
this.auth.xhr(
|
||||
{
|
||||
method: "GET",
|
||||
path: "/api/0.6/user/preferences"
|
||||
path: "/api/0.6/user/preferences",
|
||||
},
|
||||
(error, value: XMLDocument) => {
|
||||
if (error) {
|
||||
|
|
@ -220,7 +226,6 @@ export class OsmPreferences {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
private static readonly endsWithNumber = /:[0-9]+$/
|
||||
|
||||
/**
|
||||
|
|
@ -234,7 +239,6 @@ export class OsmPreferences {
|
|||
*
|
||||
*/
|
||||
private static keysStartingWith(allKeys: string[], key: string): string[] {
|
||||
|
||||
const keys = allKeys.filter((k) => {
|
||||
if (k === key) {
|
||||
return true
|
||||
|
|
@ -300,7 +304,7 @@ export class OsmPreferences {
|
|||
{
|
||||
method: "DELETE",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
headers: { "Content-Type": "text/plain" }
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
|
|
@ -342,9 +346,12 @@ export class OsmPreferences {
|
|||
}
|
||||
|
||||
try {
|
||||
|
||||
return this.osmConnection.interact("user/preferences/" + encodeURIComponent(k),
|
||||
"PUT", { "Content-Type": "text/plain" }, v)
|
||||
return this.osmConnection.interact(
|
||||
"user/preferences/" + encodeURIComponent(k),
|
||||
"PUT",
|
||||
{ "Content-Type": "text/plain" },
|
||||
v
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("Could not upload preference due to", e)
|
||||
}
|
||||
|
|
@ -365,7 +372,13 @@ export class OsmPreferences {
|
|||
}
|
||||
|
||||
private async cleanup() {
|
||||
const prefixesToClean = ["mapcomplete-mapcomplete-", "mapcomplete-places-history", "unofficial-theme-", "mapcompleteplaces", "mapcompletethemes"] // TODO enable this one once the new system is in prod "mapcomplete-current-open-changeset-"]
|
||||
const prefixesToClean = [
|
||||
"mapcomplete-mapcomplete-",
|
||||
"mapcomplete-places-history",
|
||||
"unofficial-theme-",
|
||||
"mapcompleteplaces",
|
||||
"mapcompletethemes",
|
||||
] // TODO enable this one once the new system is in prod "mapcomplete-current-open-changeset-"]
|
||||
let somethingChanged = false
|
||||
for (const prefix of prefixesToClean) {
|
||||
const hasChange = await this.removeAllWithPrefix(prefix) // Don't inline - short-circuiting
|
||||
|
|
@ -381,10 +394,13 @@ export class OsmPreferences {
|
|||
for (const theme of themes.data) {
|
||||
const cs = this.osmConnection.getCurrentChangesetFor(theme)
|
||||
if (now.getTime() - cs.data.opened > 24 * 60 * 60 * 1000) {
|
||||
console.log("Clearing 'open changeset' for theme", theme, "; definitively expired by now")
|
||||
console.log(
|
||||
"Clearing 'open changeset' for theme",
|
||||
theme,
|
||||
"; definitively expired by now"
|
||||
)
|
||||
cs.set(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import osmtogeojson from "osmtogeojson"
|
|||
import { FeatureCollection, Geometry } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
|
||||
("use strict")
|
||||
;("use strict")
|
||||
/**
|
||||
* Interfaces overpass to get all the latest data
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "d
|
|||
export interface GeoLocationPointProperties extends GeolocationCoordinates {
|
||||
id: "gps" | string
|
||||
"user:location": "yes"
|
||||
date: string,
|
||||
date: string
|
||||
alpha?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export default class SearchState {
|
|||
const results = themeSearch.data.search(query, 3)
|
||||
const deduped: MinimalThemeInformation[] = []
|
||||
for (const result of results) {
|
||||
if (deduped.some(th => th.id === result.id)) {
|
||||
if (deduped.some((th) => th.id === result.id)) {
|
||||
continue
|
||||
}
|
||||
deduped.push(result)
|
||||
|
|
|
|||
|
|
@ -57,10 +57,7 @@ class RoundRobinStore<T> {
|
|||
this._index.set((i + 1) % this._maxCount)
|
||||
this._store.data[i] = t
|
||||
this._store.ping()
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export class OptionallySyncedHistory<T extends object | string> {
|
||||
|
|
@ -84,18 +81,21 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
this._maxHistory = maxHistory
|
||||
this._isSame = isSame
|
||||
this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", {
|
||||
defaultValue: "sync"
|
||||
defaultValue: "sync",
|
||||
})
|
||||
|
||||
this.syncedBackingStore = Stores.fromArray(
|
||||
Utils.TimesT(maxHistory, (i) => {
|
||||
const pref = osmconnection.getPreference(key + "-hist-" + i + "-")
|
||||
return UIEventSource.asObject<T>(pref, undefined)
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
const ringIndex = UIEventSource.asInt(osmconnection.getPreference(key + "-hist-round-robin", {
|
||||
defaultValue: "0"
|
||||
}))
|
||||
const ringIndex = UIEventSource.asInt(
|
||||
osmconnection.getPreference(key + "-hist-round-robin", {
|
||||
defaultValue: "0",
|
||||
})
|
||||
)
|
||||
this.syncedOrdered = new RoundRobinStore<T>(this.syncedBackingStore, ringIndex, 10)
|
||||
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
|
||||
const thisSession = (this.thisSession = new UIEventSource<T[]>(
|
||||
|
|
@ -104,7 +104,10 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
))
|
||||
this.syncPreference.addCallback((syncmode) => {
|
||||
if (syncmode === "sync") {
|
||||
const list = [...thisSession.data, ...this.syncedOrdered.value.data].slice(0, maxHistory)
|
||||
const list = [...thisSession.data, ...this.syncedOrdered.value.data].slice(
|
||||
0,
|
||||
maxHistory
|
||||
)
|
||||
if (this._isSame) {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
for (let j = i + 1; j < list.length; j++) {
|
||||
|
|
@ -140,7 +143,9 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
|
||||
public add(t: T) {
|
||||
if (this._isSame) {
|
||||
const alreadyNoted = this.getAppropriateStore().data.some(item => this._isSame(item, t))
|
||||
const alreadyNoted = this.getAppropriateStore().data.some((item) =>
|
||||
this._isSame(item, t)
|
||||
)
|
||||
if (alreadyNoted) {
|
||||
return
|
||||
}
|
||||
|
|
@ -154,7 +159,7 @@ export class OptionallySyncedHistory<T extends object | string> {
|
|||
}
|
||||
this.local.ping()
|
||||
} else if (this.syncPreference.data === "sync") {
|
||||
this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => {
|
||||
this.osmconnection.isLoggedIn.addCallbackAndRun((loggedIn) => {
|
||||
// Wait until we are logged in and the settings are downloaded before adding the preference
|
||||
if (loggedIn) {
|
||||
this.syncedOrdered.add(t)
|
||||
|
|
@ -271,22 +276,29 @@ export default class UserRelatedState {
|
|||
this.a11y = this.osmConnection.getPreference("a11y")
|
||||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.getPreference("identity", { defaultValue: undefined, prefix: "mangrove" }),
|
||||
this.osmConnection.getPreference("identity", {
|
||||
defaultValue: undefined,
|
||||
prefix: "mangrove",
|
||||
}),
|
||||
this.osmConnection.getPreference("identity-creation-date", {
|
||||
defaultValue: undefined,
|
||||
prefix: "mangrove"
|
||||
prefix: "mangrove",
|
||||
})
|
||||
)
|
||||
this.preferredBackgroundLayer = this.osmConnection.getPreference("preferred-background-layer")
|
||||
|
||||
this.addNewFeatureMode = this.osmConnection.getPreference("preferences-add-new-mode",
|
||||
{ defaultValue: "button_click_right" }
|
||||
this.preferredBackgroundLayer = this.osmConnection.getPreference(
|
||||
"preferred-background-layer"
|
||||
)
|
||||
|
||||
this.addNewFeatureMode = this.osmConnection.getPreference("preferences-add-new-mode", {
|
||||
defaultValue: "button_click_right",
|
||||
})
|
||||
this.showScale = UIEventSource.asBoolean(
|
||||
this.osmConnection.getPreference("preference-show-scale", { defaultValue: "false" })
|
||||
)
|
||||
|
||||
this.imageLicense = this.osmConnection.getPreference("pictures-license", { defaultValue: "CC0" })
|
||||
this.imageLicense = this.osmConnection.getPreference("pictures-license", {
|
||||
defaultValue: "CC0",
|
||||
})
|
||||
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
|
||||
this.translationMode = this.initTranslationMode()
|
||||
this.homeLocation = this.initHomeLocation()
|
||||
|
|
@ -370,8 +382,8 @@ export default class UserRelatedState {
|
|||
} catch (e) {
|
||||
console.warn(
|
||||
"Removing theme " +
|
||||
id +
|
||||
" as it could not be parsed from the preferences; the content is:",
|
||||
id +
|
||||
" as it could not be parsed from the preferences; the content is:",
|
||||
str
|
||||
)
|
||||
pref.setData(null)
|
||||
|
|
@ -401,7 +413,7 @@ export default class UserRelatedState {
|
|||
icon: layout.icon,
|
||||
title: layout.title.translations,
|
||||
shortDescription: layout.shortDescription.translations,
|
||||
definition: layout["definition"]
|
||||
definition: layout["definition"],
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
@ -456,13 +468,13 @@ export default class UserRelatedState {
|
|||
id: "home",
|
||||
"user:home": "yes",
|
||||
_lon: homeLonLat[0],
|
||||
_lat: homeLonLat[1]
|
||||
_lat: homeLonLat[1],
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: homeLonLat
|
||||
}
|
||||
}
|
||||
coordinates: homeLonLat,
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
return new StaticFeatureSource(feature)
|
||||
|
|
@ -484,7 +496,7 @@ export default class UserRelatedState {
|
|||
_applicationOpened: new Date().toISOString(),
|
||||
_supports_sharing:
|
||||
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
|
||||
_iframe: Utils.isIframe ? "yes" : "no"
|
||||
_iframe: Utils.isIframe ? "yes" : "no",
|
||||
})
|
||||
if (!Utils.runningFromConsole) {
|
||||
amendedPrefs.data["_host"] = window.location.host
|
||||
|
|
@ -532,18 +544,18 @@ export default class UserRelatedState {
|
|||
const zenLinks: { link: string; id: string }[] = Utils.NoNull([
|
||||
hasMissingTheme
|
||||
? {
|
||||
id: "theme:" + layout.id,
|
||||
link: LinkToWeblate.hrefToWeblateZen(
|
||||
language,
|
||||
"themes",
|
||||
layout.id
|
||||
)
|
||||
}
|
||||
id: "theme:" + layout.id,
|
||||
link: LinkToWeblate.hrefToWeblateZen(
|
||||
language,
|
||||
"themes",
|
||||
layout.id
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
...missingLayers.map((id) => ({
|
||||
id: "layer:" + id,
|
||||
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id)
|
||||
}))
|
||||
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
|
||||
})),
|
||||
])
|
||||
const untranslated_count = untranslated.length
|
||||
amendedPrefs.data["_translation_total"] = "" + total
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,12 +92,12 @@ export class TagUtils {
|
|||
"!~i~": {
|
||||
name: "Value does *not* match case-invariant regex",
|
||||
overpassSupport: true,
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value (thus: a `^` and `$` are automatically added to start and end). This filter returns true if the value does *not* match"
|
||||
docs: "A tag can also be tested against a regex with `key~i~regex`, where the case of the value will be ignored. The regex is still matched against the _entire_ value (thus: a `^` and `$` are automatically added to start and end). This filter returns true if the value does *not* match",
|
||||
},
|
||||
"~~": {
|
||||
name: "Key and value should match given regex",
|
||||
overpassSupport: true,
|
||||
docs: "Both the `key` and `value` part of this specification are interpreted as regexes, both the key and value must completely match their respective regexes (thus: a `^` and `$` are automatically added to start and end)"
|
||||
docs: "Both the `key` and `value` part of this specification are interpreted as regexes, both the key and value must completely match their respective regexes (thus: a `^` and `$` are automatically added to start and end)",
|
||||
},
|
||||
"~i~~": {
|
||||
name: "Key and value should match a given regex; value is case-invariant",
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@ export class Stores {
|
|||
*/
|
||||
public static FromPromise<T>(promise: Promise<T>): Store<T | undefined> {
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise?.catch((err): undefined => {
|
||||
console.warn("Promise failed:", err)
|
||||
return undefined
|
||||
})?.then((d) => src.setData(d))
|
||||
promise
|
||||
?.catch((err): undefined => {
|
||||
console.warn("Promise failed:", err)
|
||||
return undefined
|
||||
})
|
||||
?.then((d) => src.setData(d))
|
||||
return src
|
||||
}
|
||||
|
||||
|
|
@ -109,14 +111,14 @@ export class Stores {
|
|||
}
|
||||
|
||||
public static fromArray<T>(sources: ReadonlyArray<UIEventSource<T>>): UIEventSource<T[]> {
|
||||
const src = new UIEventSource<T[]>(sources.map(s => s.data))
|
||||
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i].addCallback(content => {
|
||||
sources[i].addCallback((content) => {
|
||||
src.data[i] = content
|
||||
src.ping()
|
||||
})
|
||||
}
|
||||
src.addCallbackD(contents => {
|
||||
src.addCallbackD((contents) => {
|
||||
for (let i = 0; i < contents.length; i++) {
|
||||
sources[i].setData(contents[i])
|
||||
}
|
||||
|
|
@ -125,9 +127,9 @@ export class Stores {
|
|||
}
|
||||
|
||||
public static fromStoresArray<T>(sources: ReadonlyArray<Store<T>>): Store<T[]> {
|
||||
const src = new UIEventSource<T[]>(sources.map(s => s.data))
|
||||
const src = new UIEventSource<T[]>(sources.map((s) => s.data))
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
sources[i].addCallback(content => {
|
||||
sources[i].addCallback((content) => {
|
||||
src.data[i] = content
|
||||
src.ping()
|
||||
})
|
||||
|
|
@ -399,8 +401,7 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
this.data = data
|
||||
}
|
||||
|
||||
private static readonly pass: () => void = () => {
|
||||
}
|
||||
private static readonly pass: () => void = () => {}
|
||||
|
||||
addCallback(_: (data: T) => void): () => void {
|
||||
// pass: data will never change
|
||||
|
|
@ -678,8 +679,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
}
|
||||
|
||||
export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
||||
private static readonly pass: () => void = () => {
|
||||
}
|
||||
private static readonly pass: () => void = () => {}
|
||||
public data: T
|
||||
_callbacks: ListenerTracker<T> = new ListenerTracker<T>()
|
||||
|
||||
|
|
@ -832,7 +832,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
try {
|
||||
return <T>JSON.parse(str)
|
||||
} catch (e) {
|
||||
console.error("Could not parse value", str, "due to", e, "; the underlying data store has tag", stringUIEventSource.tag)
|
||||
console.error(
|
||||
"Could not parse value",
|
||||
str,
|
||||
"due to",
|
||||
e,
|
||||
"; the underlying data store has tag",
|
||||
stringUIEventSource.tag
|
||||
)
|
||||
return defaultV
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -166,7 +166,11 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
|
|||
private readonly _radius: number
|
||||
private readonly _panoramax: Panoramax
|
||||
name: string = "panoramax"
|
||||
public static readonly apiUrls: ReadonlyArray<string> = ["https://panoramax.openstreetmap.fr", "https://api.panoramax.xyz", "https://panoramax.mapcomplete.org"]
|
||||
public static readonly apiUrls: ReadonlyArray<string> = [
|
||||
"https://panoramax.openstreetmap.fr",
|
||||
"https://api.panoramax.xyz",
|
||||
"https://panoramax.mapcomplete.org",
|
||||
]
|
||||
|
||||
constructor(url?: string, radius: number = 100) {
|
||||
this._radius = radius
|
||||
|
|
@ -286,7 +290,7 @@ class MapillaryFetcher implements ImageFetcher {
|
|||
mapillary: img.id,
|
||||
},
|
||||
details: {
|
||||
isSpherical: this._panoramas === "only"
|
||||
isSpherical: this._panoramas === "only",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -298,10 +302,12 @@ type P4CService = (typeof P4CImageFetcher.services)[number]
|
|||
|
||||
export class CombinedFetcher {
|
||||
private readonly sources: ReadonlyArray<CachedFetcher>
|
||||
public static apiUrls = [...P4CImageFetcher.apiUrls,
|
||||
Imgur.apiUrl, ...Imgur.supportingUrls,
|
||||
public static apiUrls = [
|
||||
...P4CImageFetcher.apiUrls,
|
||||
Imgur.apiUrl,
|
||||
...Imgur.supportingUrls,
|
||||
...MapillaryFetcher.apiUrls,
|
||||
...ImagesFromPanoramaxFetcher.apiUrls
|
||||
...ImagesFromPanoramaxFetcher.apiUrls,
|
||||
]
|
||||
|
||||
constructor(radius: number, maxage: Date, indexedFeatures: IndexedFeatureSource) {
|
||||
|
|
@ -313,14 +319,15 @@ export class CombinedFetcher {
|
|||
new MapillaryFetcher({
|
||||
max_images: 25,
|
||||
start_captured_at: maxage,
|
||||
panoramas: "only"
|
||||
panoramas: "only",
|
||||
}),
|
||||
new MapillaryFetcher({
|
||||
max_images: 25,
|
||||
start_captured_at: maxage,
|
||||
panoramas: "no"
|
||||
}), new P4CImageFetcher("mapillary"),
|
||||
new P4CImageFetcher("wikicommons")
|
||||
panoramas: "no",
|
||||
}),
|
||||
new P4CImageFetcher("mapillary"),
|
||||
new P4CImageFetcher("wikicommons"),
|
||||
].map((f) => new CachedFetcher(f))
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue