chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2025-04-15 18:18:44 +02:00
parent 79b6927b56
commit 42ded4c1b1
328 changed files with 4062 additions and 1284 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ export class Imgur extends ImageProvider {
key: key,
provider: this,
id: value,
isSpherical: false
isSpherical: false,
},
]
}

View file

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

View file

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

View file

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

View file

@ -189,7 +189,7 @@ export class WikimediaImageProvider extends ImageProvider {
key: undefined,
provider: this,
id: image,
isSpherical: false
isSpherical: false,
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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