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

View file

@ -39,20 +39,24 @@ export class AvailableRasterLayers {
url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution: {
text: "OpenStreetMap",
url: "https://openStreetMap.org/copyright"
url: "https://openStreetMap.org/copyright",
},
best: true,
max_zoom: 19,
min_zoom: 0,
category: "osmbasedmap"
category: "osmbasedmap",
}
public static readonly osmCarto: RasterLayerPolygon = {
type: "Feature",
properties: AvailableRasterLayers.osmCartoProperties,
geometry: BBox.global.asGeometry()
geometry: BBox.global.asGeometry(),
}
public static allAvailableGlobalLayers = new Set([...AvailableRasterLayers.globalLayers, AvailableRasterLayers.osmCarto, AvailableRasterLayers.bing])
public static allAvailableGlobalLayers = new Set([
...AvailableRasterLayers.globalLayers,
AvailableRasterLayers.osmCarto,
AvailableRasterLayers.bing,
])
private static initGlobalLayers(): RasterLayerPolygon[] {
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers).layers.filter(
@ -75,8 +79,6 @@ export class AvailableRasterLayers {
)
}
/**
* The default background layer that any theme uses which does not explicitly define a background
*/

View file

@ -1,7 +1,10 @@
import { DesugaringStep } from "./Conversion"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "../Json/QuestionableTagRenderingConfigJson"
import { ConversionContext } from "./ConversionContext"
import { Translation } from "../../../UI/i18n/Translation"
import { TagUtils } from "../../../Logic/Tags/TagUtils"

View file

@ -1,6 +1,18 @@
import { Concat, DesugaringContext, DesugaringStep, Each, FirstOf, Fuse, On, SetDefault } from "./Conversion"
import {
Concat,
DesugaringContext,
DesugaringStep,
Each,
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import {
MinimalTagRenderingConfigJson,
TagRenderingConfigJson,
} from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"

View file

@ -271,7 +271,6 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs
*/
default?: string
/**
* question: If this key shared and distinguished by a postfix, what is the postfix?
* This option is used specifically for `charge`, where the cost is indicated with `/item`.

View file

@ -381,14 +381,11 @@ export default class LayerConfig extends WithContextLoader {
* @private
*/
private generateDocumentationQuickTable(): string {
return MarkdownUtils.table(
["id", "question", "labels", "freeform key"],
this.tagRenderings
.filter(tr => tr.labels.indexOf("ignore_docs") < 0)
.map(tr => {
.filter((tr) => tr.labels.indexOf("ignore_docs") < 0)
.map((tr) => {
let key = "_Multiple choice only_"
if (tr.freeform) {
const type = `[${tr.freeform.type}](../SpecialInputElements.md#${tr.freeform.type})`
@ -419,14 +416,10 @@ export default class LayerConfig extends WithContextLoader {
`[${tr.id}](#${tr.id}) ${origDef}`,
Utils.NoNull([q, r, options]).join("<br/>"),
tr.labels.join(", "),
key
key,
]
})
)
}
public generateDocumentation(

View file

@ -5,7 +5,10 @@ import { TagUtils } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import { MappingConfigJson, QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import { RegexTag } from "../../Logic/Tags/RegexTag"
@ -697,7 +700,10 @@ export default class TagRenderingConfig {
* TagRenderingConfig.splitPostfixDistinguished(" €10 / day ") // => {value: "€10", denomination: "day"}
*
*/
private static splitPostfixDistinguished(part: string): { value: string, denomination: string } {
private static splitPostfixDistinguished(part: string): {
value: string
denomination: string
} {
const i = part.indexOf("/")
if (i < 0) {
return { value: part, denomination: "" }

View file

@ -43,7 +43,9 @@ export class UserMapFeatureswitchState extends WithUserRelatedState {
readonly geolocationState: GeoLocationState
readonly geolocation: GeoLocationHandler
readonly geolocationControl: GeolocationControlState
readonly historicalUserLocations: WritableFeatureSource<Feature<Point, GeoLocationPointProperties>>
readonly historicalUserLocations: WritableFeatureSource<
Feature<Point, GeoLocationPointProperties>
>
readonly availableLayers: { store: Store<RasterLayerPolygon[]> }
readonly currentView: FeatureSource<Feature<Polygon>>

View file

@ -55,7 +55,7 @@ export class WithImageState extends WithGuiState implements SpecialVisualization
featureSwitches: this.featureSwitches,
selectedElement: this.selectedElement,
indexedFeatures: this.indexedFeatures,
guistate: this.guistate
guistate: this.guistate,
})
new PendingChangesUploader(this.changes, this.selectedElement, this.imageUploadManager)
}

View file

@ -86,7 +86,7 @@ export class Tiles {
static asGeojson(zIndex: number, x?: number, y?: number): Feature<Polygon> {
let z = zIndex
if (x === undefined) {
[z, x, y] = Tiles.tile_from_index(zIndex)
;[z, x, y] = Tiles.tile_from_index(zIndex)
}
const bounds = Tiles.tile_bounds_lon_lat(z, x, y)
return new BBox(bounds).asGeoJson()
@ -161,10 +161,10 @@ export class Tiles {
private static lat2tile(lat: number, zoom: number): number {
return Math.floor(
((1 -
Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) /
Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) /
Math.PI) /
2) *
Math.pow(2, zoom)
Math.pow(2, zoom)
)
}
}

View file

@ -165,7 +165,7 @@
<Page {onlyLink} shown={pg.failedImages} bodyPadding="p-0 pb-4">
<svelte:fragment slot="header">
<PhotoIcon />
<Tr t={Translations.t.imageQueue.menu.Subs({count: $nrOfFailedImages.length})} />
<Tr t={Translations.t.imageQueue.menu.Subs({ count: $nrOfFailedImages.length })} />
</svelte:fragment>
<QueuedImagesView {state} />
</Page>

View file

@ -35,7 +35,9 @@
export let attributionFormat: "minimal" | "medium" | "large" = "medium"
let previewedImage: UIEventSource<Partial<ProvidedImage>> = MenuState.previewedImage
export let canZoom = previewedImage !== undefined
export let nearbyFeatures: Feature<Geometry, HotspotProperties>[] | Store<Feature<Geometry, HotspotProperties>[]> = []
export let nearbyFeatures:
| Feature<Geometry, HotspotProperties>[]
| Store<Feature<Geometry, HotspotProperties>[]> = []
let loaded = false
let showBigPreview = new UIEventSource(false)
@ -49,7 +51,7 @@
previewedImage.addCallbackAndRun((previewedImage) => {
showBigPreview.set(
previewedImage !== undefined &&
(previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url)
(previewedImage?.id ?? previewedImage?.url) === (image.id ?? image.url)
)
})
)
@ -67,16 +69,15 @@
type: "Feature",
properties: {
id: image.id,
rotation: image.rotation
rotation: image.rotation,
},
geometry: {
type: "Point",
coordinates: [image.lon, image.lat]
}
coordinates: [image.lon, image.lat],
},
}
state?.geocodedImages.set([f])
}
</script>
<Popup shown={showBigPreview} bodyPadding="p-0" dismissable={true}>
@ -135,10 +136,12 @@
/>
{#if image.isSpherical}
<div class="absolute top-0 left-0 w-full h-full flex justify-center items-center pointer-events-none">
<div class="bg-black opacity-50 rounded-full p-[3.25rem]">
<div class="w-0 h-0 relative flex items-center justify-center">
<Panorama360 class="absolute w-16 h-16" color="#ffffff" />
<div
class="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center justify-center"
>
<div class="rounded-full bg-black p-[3.25rem] opacity-50">
<div class="relative flex h-0 w-0 items-center justify-center">
<Panorama360 class="absolute h-16 w-16" color="#ffffff" />
</div>
</div>
</div>
@ -157,7 +160,6 @@
<ImageAttribution {image} {attributionFormat} />
</div>
</div>
{:else if image.status === "hidden"}
<div class="subtle">This image has been reported</div>
{/if}

View file

@ -18,12 +18,14 @@
type: "Feature",
geometry: {
type: "Point",
coordinates: GeoOperations.centerpointCoordinates(feature)
coordinates: GeoOperations.centerpointCoordinates(feature),
},
properties: {
name: layer?.title?.GetRenderValue(feature.properties)?.Subs(feature.properties)?.txt ?? feature?.properties?.name,
focus: true
}
name:
layer?.title?.GetRenderValue(feature.properties)?.Subs(feature.properties)?.txt ??
feature?.properties?.name,
focus: true,
},
}
</script>

View file

@ -20,7 +20,9 @@
export let image: Partial<ProvidedImage> & { id: string; url: string }
export let clss: string = undefined
export let nearbyFeatures: Feature<Geometry, HotspotProperties>[] | Store<Feature<Geometry, HotspotProperties>[]> = []
export let nearbyFeatures:
| Feature<Geometry, HotspotProperties>[]
| Store<Feature<Geometry, HotspotProperties>[]> = []
let isLoaded = new UIEventSource(false)
console.log(">>> slots are", $$slots)
@ -37,17 +39,17 @@
</div>
{#if $$slots["dot-menu-actions"]}
<DotMenu dotsPosition="top-0 left-0" dotsSize="w-8 h-8" hideBackground>
<slot name="dot-menu-actions">
<button
class="no-image-background pointer-events-auto flex items-center"
on:click={() => ImageProvider.offerImageAsDownload(image)}
>
<DownloadIcon class="h-6 w-6 px-2 opacity-100" />
<Tr t={Translations.t.general.download.downloadImage} />
</button>
</slot>
</DotMenu>
<DotMenu dotsPosition="top-0 left-0" dotsSize="w-8 h-8" hideBackground>
<slot name="dot-menu-actions">
<button
class="no-image-background pointer-events-auto flex items-center"
on:click={() => ImageProvider.offerImageAsDownload(image)}
>
<DownloadIcon class="h-6 w-6 px-2 opacity-100" />
<Tr t={Translations.t.general.download.downloadImage} />
</button>
</slot>
</DotMenu>
{/if}
<div
class="pointer-events-none absolute bottom-0 left-0 flex w-full flex-wrap items-end justify-between"

View file

@ -13,8 +13,9 @@
import type { Feature, Geometry, Point } from "geojson"
import { Store } from "../../Logic/UIEventSource"
export let nearbyFeatures: Feature<Geometry, HotspotProperties>[] | Store<Feature<Geometry, HotspotProperties>[]> = []
export let nearbyFeatures:
| Feature<Geometry, HotspotProperties>[]
| Store<Feature<Geometry, HotspotProperties>[]> = []
export let image: Partial<ProvidedImage>
let panzoomInstance = undefined
let panzoomEl: HTMLElement
@ -34,17 +35,15 @@
if (Array.isArray(nearbyFeatures)) {
viewer.setNearbyFeatures(nearbyFeatures)
} else {
nearbyFeatures.addCallbackAndRunD(feats => {
nearbyFeatures.addCallbackAndRunD((feats) => {
viewer.setNearbyFeatures(feats)
})
}
isLoaded.set(true)
}
$: {
if (image.isSpherical) {
initPhotosphere()
} else if (panzoomEl) {
panzoomInstance = panzoom(panzoomEl, {
@ -52,7 +51,7 @@
boundsPadding: 0.49,
minZoom: 0.1,
maxZoom: 25,
initialZoom: 1.0
initialZoom: 1.0,
})
} else {
panzoomInstance?.dispose()
@ -61,17 +60,17 @@
</script>
<head>
<link rel="stylesheet" href="./css/pannellum.css">
<link rel="stylesheet" href="./css/pannellum.css" />
</head>
{#if image.isSpherical}
<div bind:this={viewerEl} class="w-full h-full" />
<div bind:this={viewerEl} class="h-full w-full" />
{:else}
<img
bind:this={panzoomEl}
class="panzoom-image h-fit max-w-fit"
on:load={() => {
isLoaded?.setData(true)
}}
isLoaded?.setData(true)
}}
src={image.url_hd ?? image.url}
/>
{/if}

View file

@ -30,7 +30,7 @@
export let linkable = true
let targetValue = Object.values(image.osmTags)[0]
let isLinked = new UIEventSource(Object.values(tags.data).some((v) => targetValue === v))
isLinked.addCallbackAndRun(linked => {
isLinked.addCallbackAndRun((linked) => {
if (linked) {
MenuState.previewedImage.set(undefined)
}
@ -43,7 +43,7 @@
provider: AllImageProviders.byName(image.provider),
date: new Date(image.date),
id: Object.values(image.osmTags)[0],
isSpherical: image.details.isSpherical
isSpherical: image.details.isSpherical,
}
async function applyLink(isLinked: boolean) {
@ -54,7 +54,7 @@
if (isLinked) {
const action = new LinkImageAction(currentTags.id, key, url, tags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "link-image"
changeType: "link-image",
})
await state.changes.applyAction(action)
} else {
@ -63,7 +63,7 @@
if (v === url) {
const action = new ChangeTagAction(currentTags.id, new Tag(k, ""), currentTags, {
theme: tags.data._orig_theme ?? state.theme.id,
changeType: "remove-image"
changeType: "remove-image",
})
state.changes.applyAction(action)
}
@ -99,9 +99,7 @@
attributionFormat="minimal"
>
<svelte:fragment slot="dot-menu-actions">
<LoginToggle {state} silentFail={true} hiddenFail={true}>
{#if linkable}
<label>
<input bind:checked={$isLinked} type="checkbox" />
@ -110,7 +108,6 @@
{/if}
</LoginToggle>
</svelte:fragment>
</AttributedImage>
<LoginToggle {state} silentFail={true}>
{#if linkable}

View file

@ -46,8 +46,7 @@
(pics: P4CPicture[]) =>
pics
.filter(
(p: P4CPicture) =>
!loadedImages.data.has(p.pictureUrl) // We don't show any image which is already linked
(p: P4CPicture) => !loadedImages.data.has(p.pictureUrl) // We don't show any image which is already linked
)
.slice(0, 25),
[loadedImages]
@ -60,15 +59,15 @@
type: "Feature",
geometry: {
type: "Point",
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat]
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
},
properties: <PanoramaView>{
id: p4c.pictureUrl,
url: p4c.pictureUrl,
northOffset: p4c.direction,
rotation: p4c.direction,
spherical: p4c.details.isSpherical ? "yes" : "no"
}
spherical: p4c.details.isSpherical ? "yes" : "no",
},
}
)
)
@ -80,14 +79,14 @@
type: "Feature",
geometry: {
type: "Point",
coordinates: [s.coordinates.lng, s.coordinates.lat]
coordinates: [s.coordinates.lng, s.coordinates.lat],
},
properties: {
id: s.pictureUrl,
selected: "yes",
rotation: s.direction
}
}
rotation: s.direction,
},
},
]
})
@ -112,7 +111,7 @@
rotation: state.mapProperties.rotation,
pitch: state.mapProperties.pitch,
zoom: new UIEventSource<number>(16),
location: new UIEventSource({ lon, lat })
location: new UIEventSource({ lon, lat }),
})
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
@ -123,7 +122,7 @@
onClick: (feature) => {
console.log("CLicked:", feature.properties)
highlighted.set(feature.properties.id)
}
},
})
ShowDataLayer.showMultipleLayers(map, new StaticFeatureSource([feature]), state.theme.layers)
@ -146,24 +145,29 @@
layer: geocodedImageLayer,
onClick: (feature) => {
highlighted.set(feature.properties.id)
}
},
})
let nearbyFeatures: Store<Feature[]> = asFeatures.map(nearbyPoints => {
return [{
type: "Feature",
geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) },
properties: {
name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt,
focus: true
}
}, ...nearbyPoints.filter(p => p.properties.spherical === "yes").map(f => ({
...f, properties: {
name: "Nearby panorama",
pitch: "auto",
type: "scene",
gotoPanorama: f
}
}))
let nearbyFeatures: Store<Feature[]> = asFeatures.map((nearbyPoints) => {
return [
{
type: "Feature",
geometry: { type: "Point", coordinates: GeoOperations.centerpointCoordinates(feature) },
properties: {
name: layer.title?.GetRenderValue(feature.properties).Subs(feature.properties).txt,
focus: true,
},
},
...nearbyPoints
.filter((p) => p.properties.spherical === "yes")
.map((f) => ({
...f,
properties: {
name: "Nearby panorama",
pitch: "auto",
type: "scene",
gotoPanorama: f,
},
})),
]
})
@ -204,7 +208,16 @@
selected.set(undefined)
}}
>
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} {nearbyFeatures} />
<LinkableImage
{tags}
{image}
{state}
{feature}
{layer}
{linkable}
{highlighted}
{nearbyFeatures}
/>
</span>
{/each}
</div>

View file

@ -13,40 +13,45 @@
export let imageArguments: ImageUploadArguments
let confirmDelete = new UIEventSource(false)
function del() {
queue.delete(imageArguments)
}
const t = Translations.t
let src = undefined
try{
try {
src = URL.createObjectURL(imageArguments.blob)
}catch (e) {
} catch (e) {
console.error("Could not create an ObjectURL for blob", imageArguments.blob)
}
</script>
<div class="low-interaction rounded border-interactive w-fit p-2 m-1 flex flex-col">
<img class="max-w-64 w-auto max-h-64 w-auto" {src} />
{imageArguments.featureId} {imageArguments.layoutId}
<button class="as-link self-end" on:click={() => {confirmDelete.set(true)}}>
<div class="low-interaction border-interactive m-1 flex w-fit flex-col rounded p-2">
<img class="max-h-64 w-auto w-auto max-w-64" {src} />
{imageArguments.featureId}
{imageArguments.layoutId}
<button
class="as-link self-end"
on:click={() => {
confirmDelete.set(true)
}}
>
<TrashIcon class="w-4" />
<Tr t={t.imageQueue.delete} />
</button>
<Popup shown={confirmDelete} dismissable={true}>
<Page shown={confirmDelete}>
<svelte:fragment slot="header">
<TrashIcon class="w-8 m-1" />
<TrashIcon class="m-1 w-8" />
<Tr t={t.imageQueue.confirmDeleteTitle} />
</svelte:fragment>
<div class="flex flex-col ">
<div class="flex flex-col">
<div class="flex justify-center">
<img class="max-w-128 w-auto max-h-128 w-auto" src={URL.createObjectURL(imageArguments.blob)} />
<img
class="max-w-128 max-h-128 w-auto w-auto"
src={URL.createObjectURL(imageArguments.blob)}
/>
</div>
<div class="flex w-full">
@ -54,8 +59,7 @@
<Tr t={t.general.back} />
</BackButton>
<button on:click={() => del()} class="primary w-full">
<TrashIcon class="w-8 m-1" />
<TrashIcon class="m-1 w-8" />
<Tr t={t.imageQueue.confirmDelete} />
</button>
</div>

View file

@ -23,13 +23,13 @@
<Tr t={q.intro} />
</div>
<UploadingImageCounter {state}/>
<UploadingImageCounter {state} />
{#if $isUploading}
<Loading />
{:else}
<button class="primary" on:click={() => state.imageUploadManager.uploadQueue()}>
<ArrowPathIcon class="w-8 h-8 m-1" />
<ArrowPathIcon class="m-1 h-8 w-8" />
<Tr t={q.retryAll} />
</button>
{/if}

View file

@ -47,7 +47,14 @@
errs.push(canBeUploaded.error)
continue
}
await state?.imageUploadManager?.uploadImageAndApply(file, tags, targetKey, noBlur, feature, { ignoreGPS })
await state?.imageUploadManager?.uploadImageAndApply(
file,
tags,
targetKey,
noBlur,
feature,
{ ignoreGPS }
)
} catch (e) {
console.error(e)
state.reportError(e, "Could not upload image")

View file

@ -26,9 +26,9 @@
*/
function getCount(input: Store<string[]>): Store<number> {
if (featureId == "*") {
return input.map(inp => inp.length)
return input.map((inp) => inp.length)
}
return input.map(success => success.filter(item => item === featureId).length)
return input.map((success) => success.filter((item) => item === featureId).length)
}
let successfull = getCount(state.imageUploadManager.successfull)
@ -39,7 +39,7 @@
const t = Translations.t.image
const debugging = state.featureSwitches.featureSwitchIsDebugging
let dismissed = 0
failed.addCallbackAndRun(failed => {
failed.addCallbackAndRun((failed) => {
dismissed = Math.min(failed, dismissed)
})
</script>
@ -56,7 +56,7 @@
{#if $pending - $failed === 1}
<Tr t={t.upload.one.uploading} />
{:else if $pending - $failed > 1}
<Tr t={t.upload.multiple.uploading.Subs({count: $pending})} />
<Tr t={t.upload.multiple.uploading.Subs({ count: $pending })} />
{/if}
</Loading>
</div>
@ -70,6 +70,6 @@
{#if $successfull === 1}
<Tr cls="thanks" t={t.upload.one.done} />
{:else if $successfull > 1}
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $successfull})} />
<Tr cls="thanks" t={t.upload.multiple.done.Subs({ count: $successfull })} />
{/if}
{/if}

View file

@ -4,38 +4,37 @@ import { Feature, Geometry, Point } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import { HotspotProperties, PanoramaView } from "../../Logic/ImageProviders/ImageProvider"
export class PhotoSphereViewerWrapper {
private imageInfo: Feature<Point, PanoramaView>
private readonly viewer: Pannellum.Viewer
private nearbyFeatures: Feature<Geometry, HotspotProperties>[] = []
constructor(container: HTMLElement, imageInfo: Feature<Point, PanoramaView>, nearbyFeatures?: Feature<Geometry, HotspotProperties>[]) {
constructor(
container: HTMLElement,
imageInfo: Feature<Point, PanoramaView>,
nearbyFeatures?: Feature<Geometry, HotspotProperties>[]
) {
this.imageInfo = imageInfo
this.viewer = pannellum.viewer(container,
<any>{
default: {
firstScene: imageInfo.properties.url,
sceneFadeDuration: 250
this.viewer = pannellum.viewer(container, <any>{
default: {
firstScene: imageInfo.properties.url,
sceneFadeDuration: 250,
},
scenes: {
[imageInfo.properties.url]: {
type: "equirectangular",
hfov: 110,
panorama: imageInfo.properties.url,
autoLoad: true,
hotSpots: [],
sceneFadeDuration: 250,
compass: true,
showControls: false,
northOffset: imageInfo.properties.northOffset,
horizonPitch: imageInfo.properties.pitchOffset,
},
scenes: {
[imageInfo.properties.url]:
{
type: "equirectangular",
hfov: 110,
panorama: imageInfo.properties.url,
autoLoad: true,
hotSpots: [],
sceneFadeDuration: 250,
compass: true,
showControls: false,
northOffset: imageInfo.properties.northOffset,
horizonPitch: imageInfo.properties.pitchOffset
}
}
}
)
},
})
this.setNearbyFeatures(nearbyFeatures)
}
@ -43,12 +42,13 @@ export class PhotoSphereViewerWrapper {
public calculatePitch(feature: Feature): number {
const coors = this.imageInfo.geometry.coordinates
const distance = GeoOperations.distanceBetween(
<[number, number]>coors, GeoOperations.centerpointCoordinates(feature)
<[number, number]>coors,
GeoOperations.centerpointCoordinates(feature)
)
// In: -pi/2 up to pi/2
const alpha = Math.atan(distance / 4) // in radians
const degrees = alpha * 360 / (2 * Math.PI)
const degrees = (alpha * 360) / (2 * Math.PI)
return -degrees
}
@ -62,7 +62,7 @@ export class PhotoSphereViewerWrapper {
this.viewer.addScene(imageInfo.properties.url, <any>{
panorama: imageInfo.properties.url,
northOffset: imageInfo.properties.northOffset,
type: "equirectangular"
type: "equirectangular",
})
this.viewer.loadScene(imageInfo.properties.url, 0, imageInfo.properties.northOffset)
@ -70,7 +70,8 @@ export class PhotoSphereViewerWrapper {
}
private clearHotspots() {
const hotspots = this.viewer.getConfig()["scenes"][this.imageInfo.properties.url].hotSpots ?? []
const hotspots =
this.viewer.getConfig()["scenes"][this.imageInfo.properties.url].hotSpots ?? []
for (const hotspot of hotspots) {
this.viewer.removeHotSpot(hotspot?.id, this.imageInfo.properties.url)
}
@ -95,20 +96,21 @@ export class PhotoSphereViewerWrapper {
} else if (!isNaN(f.properties.pitch)) {
pitch = f.properties.pitch
}
this.viewer.addHotSpot({
type: f.properties.gotoPanorama !== undefined ? "scene" : "info",
yaw: (yaw - northOffs) % 360,
pitch,
text: f.properties.name,
clickHandlerFunc: () => {
this.setPanorama(f.properties.gotoPanorama)
}
}, this.imageInfo.properties.url)
this.viewer.addHotSpot(
{
type: f.properties.gotoPanorama !== undefined ? "scene" : "info",
yaw: (yaw - northOffs) % 360,
pitch,
text: f.properties.name,
clickHandlerFunc: () => {
this.setPanorama(f.properties.gotoPanorama)
},
},
this.imageInfo.properties.url
)
if (f.properties.focus) {
this.viewer.setYaw(yaw - northOffs)
}
}
}
}

View file

@ -1,5 +1,4 @@
<script lang="ts">
/**
* Used to quickly calculate a distance by dragging a map (and selecting start- and endpoints)
*/
@ -24,71 +23,77 @@
export let value: UIEventSource<number>
export let feature: Feature
export let args: { background?: string, zoom?: number }
export let args: { background?: string; zoom?: number }
export let state: ThemeViewState = undefined
export let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let center = GeoOperations.centerpointCoordinates(feature)
export let initialCoordinate: { lon: number, lat: number } = { lon: center[0], lat: center[1] }
let mapLocation: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(initialCoordinate)
export let initialCoordinate: { lon: number; lat: number } = { lon: center[0], lat: center[1] }
let mapLocation: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(
initialCoordinate
)
let bg = args?.background
let rasterLayer = state?.mapProperties.rasterLayer
if (bg !== undefined) {
if (eliCategory.indexOf(bg) >= 0) {
const availableLayers = state.availableLayers.store.data
const startLayer: RasterLayerPolygon = RasterLayerUtils.SelectBestLayerAccordingTo(availableLayers, bg)
const startLayer: RasterLayerPolygon = RasterLayerUtils.SelectBestLayerAccordingTo(
availableLayers,
bg
)
rasterLayer = new UIEventSource(startLayer)
state?.mapProperties.rasterLayer.addCallbackD(layer => rasterLayer.set(layer))
state?.mapProperties.rasterLayer.addCallbackD((layer) => rasterLayer.set(layer))
}
}
let mapProperties: Partial<MapProperties> = {
rasterLayer: rasterLayer,
location: mapLocation,
zoom: new UIEventSource(args?.zoom ?? 18)
zoom: new UIEventSource(args?.zoom ?? 18),
}
let start: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined)
let start: UIEventSource<{ lon: number; lat: number }> = new UIEventSource(undefined)
function selectStart() {
start.set(mapLocation.data)
}
let lengthFeature: Store<Feature[]> = start.map(start => {
if (!start) {
return []
}
// A bit of a double task: calculate the actual value _and_ the map rendering
const end = mapLocation.data
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
value.set(distance.toFixed(2))
return <Feature[]>[
{
type: "Feature",
properties: {
id: "distance_line_" + distance,
distance: "" + distance
},
geometry: {
type: "LineString",
coordinates: [[start.lon, start.lat], [end.lon, end.lat]]
}
let lengthFeature: Store<Feature[]> = start.map(
(start) => {
if (!start) {
return []
}
]
// A bit of a double task: calculate the actual value _and_ the map rendering
const end = mapLocation.data
const distance = GeoOperations.distanceBetween([start.lon, start.lat], [end.lon, end.lat])
value.set(distance.toFixed(2))
}, [mapLocation])
return <Feature[]>[
{
type: "Feature",
properties: {
id: "distance_line_" + distance,
distance: "" + distance,
},
geometry: {
type: "LineString",
coordinates: [
[start.lon, start.lat],
[end.lon, end.lat],
],
},
},
]
},
[mapLocation]
)
new ShowDataLayer(map, {
layer: new LayerConfig(conflation),
features: new StaticFeatureSource(lengthFeature)
features: new StaticFeatureSource(lengthFeature),
})
</script>
<div class="relative w-full h-64">
<div class="relative h-64 w-full">
<LocationInput value={mapLocation} {mapProperties} {map} />
<div class="absolute bottom-0 left-0 p-4">
<OpenBackgroundSelectorButton {state} {map} />

View file

@ -61,7 +61,7 @@ export default class Validators {
"translation",
"url",
"velopark",
"wikidata"
"wikidata",
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -93,7 +93,7 @@ export default class Validators {
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new CurrencyValidator(),
new RegexValidator()
new RegexValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -6,17 +6,19 @@ export default class DistanceValidator extends Validator {
private readonly docs: string = [
"#### Helper-arguments",
"Options are:",
["````json",
" \"background\": \"some_background_id or category, e.g. 'map'\"",
" \"zoom\": 20 # initial zoom level of the map",
[
"````json",
' "background": "some_background_id or category, e.g. \'map\'"',
' "zoom": 20 # initial zoom level of the map',
"}",
"```"].join("\n")
"```",
].join("\n"),
].join("\n\n")
constructor() {
super(
"distance",
"A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `[\"21\", \"map,photo\"]",
'A geographical distance in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma separated) ], e.g. `["21", "map,photo"]',
"decimal"
)
}
@ -35,14 +37,18 @@ export default class DistanceValidator extends Validator {
}
const optionalKeys = ["background", "zoom"]
const keys = Object.keys(args).filter(k => optionalKeys.indexOf(k) < 0)
const keys = Object.keys(args).filter((k) => optionalKeys.indexOf(k) < 0)
if (keys.length > 0) {
return "Unknown key " + keys.join("; ") + "; use " + optionalKeys.join("; ") + " instead"
return (
"Unknown key " + keys.join("; ") + "; use " + optionalKeys.join("; ") + " instead"
)
}
const bg = args["background"]
if (bg && eliCategory.indexOf(bg) < 0) {
return "The given background layer is not a recognized ELI-type. Perhaps you meant one of " +
Utils.sortedByLevenshteinDistance(bg, eliCategory, x => x).slice(0, 5)
return (
"The given background layer is not a recognized ELI-type. Perhaps you meant one of " +
Utils.sortedByLevenshteinDistance(bg, eliCategory, (x) => x).slice(0, 5)
)
}
if (typeof args["zoom"] !== "number") {
return "zoom must be a number, got a " + typeof args["zoom"]

View file

@ -302,9 +302,7 @@
</div>
</NextButton>
</div>
<TagHint
tags={selectedPreset.preset.tags}
/>
<TagHint tags={selectedPreset.preset.tags} />
</TitledPanel>
{:else if _globalFilter?.length > 0 && _globalFilter?.length > checkedOfGlobalFilters}
<Tr t={_globalFilter[checkedOfGlobalFilters].onNewPoint?.safetyCheck} cls="mx-12" />

View file

@ -48,7 +48,9 @@
let selectedTags: UploadableTag[]
let changedProperties = undefined
$: changedProperties = TagUtils.changeAsProperties(And.construct(selectedTags)?.asChange(tags?.data ?? {}) ?? [])
$: changedProperties = TagUtils.changeAsProperties(
And.construct(selectedTags)?.asChange(tags?.data ?? {}) ?? []
)
let isHardDelete = undefined
$: isHardDelete = changedProperties[DeleteConfig.deleteReasonKey] !== undefined
@ -58,7 +60,9 @@
}
currentState = "applying"
let actionToTake: OsmChangeAction
const changedProperties = TagUtils.changeAsProperties(And.construct(selectedTags)?.asChange(tags?.data ?? {}))
const changedProperties = TagUtils.changeAsProperties(
And.construct(selectedTags)?.asChange(tags?.data ?? {})
)
const deleteReason = changedProperties[DeleteConfig.deleteReasonKey]
if (deleteReason) {
let softDeletionTags: UploadableTag

View file

@ -37,21 +37,19 @@
{#if tagRenderings.length > 0}
<div class="mb-8">
<AccordionSingle>
<div slot="header">
{#if headerTr}
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
{:else}
{header}
{/if}
</div>
<div slot="header">
{#if headerTr}
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
{:else}
{header}
{/if}
</div>
{#each tagRenderings as config (config.id)}
{#if config.IsKnown($tags) && (config.condition === undefined || config.condition.matchesProperties($tags))}
<TagRenderingEditableDynamic {tags} {config} {state} {selectedElement} {layer} />
{/if}
{/each}
</AccordionSingle>
{#if config.IsKnown($tags) && (config.condition === undefined || config.condition.matchesProperties($tags))}
<TagRenderingEditableDynamic {tags} {config} {state} {selectedElement} {layer} />
{/if}
{/each}
</AccordionSingle>
</div>
{/if}

View file

@ -119,6 +119,7 @@
}, 50)
}
</script>
{#if $loginEnabled}
<div
bind:this={questionboxElem}

View file

@ -23,7 +23,7 @@
if (config === undefined) {
console.error("TagRenderingAnswer: Config is undefined")
throw ("Config is undefined in tagRenderingAnswer")
throw "Config is undefined in tagRenderingAnswer"
}
let trs: Store<{ then: Translation; icon?: string; iconClass?: string }[]> = tags.mapD((tags) =>
Utils.NoNull(config?.GetRenderValues(tags))

View file

@ -23,11 +23,21 @@
export let getCountry = () => "?"
onMount(() => {
console.log("Setting selected unit based on country", getCountry(), "and upstream value:", upstreamValue.data)
console.log(
"Setting selected unit based on country",
getCountry(),
"and upstream value:",
upstreamValue.data
)
if (upstreamValue.data === undefined || upstreamValue.data === "") {
// Init the selected unit
let denomination: Denomination = unit.getDefaultDenomination(getCountry)
console.log("Found denom", denomination.canonical, "available denominations are:", unit.denominations.map(denom => denom.canonical))
console.log(
"Found denom",
denomination.canonical,
"available denominations are:",
unit.denominations.map((denom) => denom.canonical)
)
selectedUnit.setData(denomination.canonical)
}
})

View file

@ -16,17 +16,16 @@
export let state: SpecialVisualizationState
let searchTerm = state.searchState.searchTerm
let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map((themes) => {
const recent = themes.filter((th) => th !== state.theme.id).slice(0, 6)
const deduped: MinimalThemeInformation[] = []
for (const theme of recent) {
if (deduped.some(th => th.id === theme.id)) {
continue
}
deduped.push(theme)
const recent = themes.filter((th) => th !== state.theme.id).slice(0, 6)
const deduped: MinimalThemeInformation[] = []
for (const theme of recent) {
if (deduped.some((th) => th.id === theme.id)) {
continue
}
return deduped
deduped.push(theme)
}
)
return deduped
})
let themeResults = state.searchState.themeSuggestions
const t = Translations.t.general.search

View file

@ -79,7 +79,13 @@ export class ImageVisualisations {
const estimated = tags.mapD((tags) =>
AllImageProviders.estimateNumberOfImages(tags, imagePrefixes)
)
return new SvelteUIElement(ImageCarousel, { state, tags, images, estimated, feature })
return new SvelteUIElement(ImageCarousel, {
state,
tags,
images,
estimated,
feature,
})
},
},
{

View file

@ -1,4 +1,8 @@
import { SpecialVisualization, SpecialVisualizationState, SpecialVisualizationSvelte } from "../SpecialVisualization"
import {
SpecialVisualization,
SpecialVisualizationState,
SpecialVisualizationSvelte,
} from "../SpecialVisualization"
import Constants from "../../Models/Constants"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Feature } from "geojson"

View file

@ -80,23 +80,22 @@ export class SettingsVisualisations {
group: "settings",
docs: "Shows the current state of storage",
args: [],
constr: function(state: SpecialVisualizationState): SvelteUIElement {
constr: function (state: SpecialVisualizationState): SvelteUIElement {
const data = {}
for (const key in localStorage) {
data[key] = localStorage[key]
}
const tags = new UIEventSource(data)
navigator.storage.estimate().then(estimate => {
navigator.storage.estimate().then((estimate) => {
data["__usage:current:bytes"] = estimate.usage
data["__usage:current:human"] = Utils.toHumanByteSize(estimate.usage)
data["__usage:quota:bytes"] = estimate.quota
data["__usage:quota:human"] = Utils.toHumanByteSize(estimate.quota)
tags.ping()
})
return new SvelteUIElement(AllTagsPanel, { state, tags })
}
},
},
{
funcName: "clear_caches",

View file

@ -29,13 +29,13 @@ class QuestionViz implements SpecialVisualizationSvelte {
},
{
name: "blacklisted-labels",
doc: "One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'"
doc: "One or more ';'-separated labels of questions which should _not_ be included. Note that the questionbox which is added by default will blacklist 'hidden'",
},
{
name: "show_all",
default: "user-preference",
doc: "Either `no`, `yes` or `user-preference`. Indicates if all questions should be shown at once"
}
doc: "Either `no`, `yes` or `user-preference`. Indicates if all questions should be shown at once",
},
]
svelteBased = true
group: "default"
@ -69,7 +69,7 @@ class QuestionViz implements SpecialVisualizationSvelte {
state,
onlyForLabels: labels,
notForLabels: blacklist,
showAllQuestionsAtOnce
showAllQuestionsAtOnce,
})
}
}

View file

@ -24,8 +24,8 @@ export class WebAndCommunicationSpecialVisualisations {
{
name: "key",
doc: "The attribute-name containing the link",
required: true
}
required: true,
},
],
constr(
@ -35,7 +35,7 @@ export class WebAndCommunicationSpecialVisualisations {
): BaseUIElement {
const key = argument[0]
return new SvelteUIElement(FediverseLink, { key, tags, state })
}
},
},
{
funcName: "wikipedia",
@ -45,8 +45,8 @@ export class WebAndCommunicationSpecialVisualisations {
{
name: "keyToShowWikipediaFor",
doc: "Use the wikidata entry from this key to show the wikipedia article for. Multiple keys can be given (separated by ';'), in which case the first matching value is used",
defaultValue: "wikidata;wikipedia"
}
defaultValue: "wikidata;wikipedia",
},
],
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
@ -59,9 +59,9 @@ export class WebAndCommunicationSpecialVisualisations {
return tags[key]?.split(";")?.map((id) => id.trim()) ?? []
})
return new SvelteUIElement(WikipediaPanel, {
wikiIds
wikiIds,
})
}
},
},
{
funcName: "wikidata_label",
@ -72,8 +72,8 @@ export class WebAndCommunicationSpecialVisualisations {
{
name: "keyToShowWikidataFor",
doc: "Use the wikidata entry from this key to show the label",
defaultValue: "wikidata"
}
defaultValue: "wikidata",
},
],
needsUrls: Wikidata.neededUrls,
example:
@ -87,7 +87,7 @@ export class WebAndCommunicationSpecialVisualisations {
)
return wikidataIds?.[0]
})
const entry = id.bind(id => Wikidata.LoadWikidataEntry(id))
const entry = id.bind((id) => Wikidata.LoadWikidataEntry(id))
return new VariableUiElement(
entry.map((e) => {
@ -96,8 +96,9 @@ export class WebAndCommunicationSpecialVisualisations {
}
const response = <WikidataResponse>e["success"]
return Translation.fromMap(response.labels)
}))
}
})
)
},
},
new MapillaryLinkVis(),
{
@ -108,29 +109,29 @@ export class WebAndCommunicationSpecialVisualisations {
{
name: "to",
doc: "Who to send the email to?",
required: true
required: true,
},
{
name: "subject",
doc: "The subject of the email",
required: true
required: true,
},
{
name: "body",
doc: "The text in the email",
required: true
required: true,
},
{
name: "button_text",
doc: "The text shown on the button in the UI",
required: true
}
required: true,
},
],
constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags })
}
},
},
{
funcName: "link",
@ -140,29 +141,29 @@ export class WebAndCommunicationSpecialVisualisations {
{
name: "text",
doc: "Text to be shown",
required: true
required: true,
},
{
name: "href",
doc: "The URL to link to. Note that this will be URI-encoded before ",
required: true
required: true,
},
{
name: "class",
doc: "CSS-classes to add to the element"
doc: "CSS-classes to add to the element",
},
{
name: "download",
doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button."
doc: "Expects a string which denotes the filename to download the contents of `href` into. If set, this link will act as a download-button.",
},
{
name: "arialabel",
doc: "If set, this text will be used as aria-label"
doc: "If set, this text will be used as aria-label",
},
{
name: "icon",
doc: "If set, show this icon next to the link. You might want to combine this with `class: button`"
}
doc: "If set, show this icon next to the link. You might want to combine this with `class: button`",
},
],
constr(
@ -184,10 +185,10 @@ export class WebAndCommunicationSpecialVisualisations {
download: tagSource.map((tags) => Utils.SubstituteKeys(download, tags)),
ariaLabel: tagSource.map((tags) => Utils.SubstituteKeys(ariaLabel, tags)),
newTab: new ImmutableStore(newTab),
icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags))
icon: tagSource.map((tags) => Utils.SubstituteKeys(icon, tags)),
}).setSpan()
}
}
},
},
]
}
}

View file

@ -1,7 +1,11 @@
import { FixedUiElement } from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import { default as FeatureTitle } from "./Popup/Title.svelte"
import { RenderingSpecification, SpecialVisualization, SpecialVisualizationState } from "./SpecialVisualization"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
@ -36,11 +40,8 @@ import { UISpecialVisualisations } from "./SpecialVisualisations/UISpecialVisual
import { SettingsVisualisations } from "./SpecialVisualisations/SettingsVisualisations"
import { ReviewSpecialVisualisations } from "./SpecialVisualisations/ReviewSpecialVisualisations"
import { DataImportSpecialVisualisations } from "./SpecialVisualisations/DataImportSpecialVisualisations"
import TagrenderingManipulationSpecialVisualisations
from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations"
import {
WebAndCommunicationSpecialVisualisations
} from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
import TagrenderingManipulationSpecialVisualisations from "./SpecialVisualisations/TagrenderingManipulationSpecialVisualisations"
import { WebAndCommunicationSpecialVisualisations } from "./SpecialVisualisations/WebAndCommunicationSpecialVisualisations"
import ClearGPSHistory from "./BigComponents/ClearGPSHistory.svelte"
import AllFeaturesStatistics from "./Statistics/AllFeaturesStatistics.svelte"

View file

@ -16,7 +16,9 @@
export let layer: LayerConfig
export let state: ThemeViewState
let bbox = state.mapProperties.bounds
let elements: Store<Feature[]> = bbox.mapD(bbox => state.perLayer.get(layer.id).GetFeaturesWithin(bbox))
let elements: Store<Feature[]> = bbox.mapD((bbox) =>
state.perLayer.get(layer.id).GetFeaturesWithin(bbox)
)
let trs = layer.tagRenderings.filter((tr) => tr.question)
</script>

View file

@ -63,7 +63,7 @@
return "offline"
}
}),
message: osmApi
message: osmApi,
})
}
@ -91,7 +91,7 @@
}
const files: string[] = s["success"]["allFiles"]
return "Contains " + (files.length ?? "no") + " files"
})
}),
})
}
{
@ -107,7 +107,7 @@
return "degraded"
}
}),
message: simpleMessage(testDownload(Constants.panoramax.url + "/api"))
message: simpleMessage(testDownload(Constants.panoramax.url + "/api")),
})
}
{
@ -123,7 +123,7 @@
return "degraded"
}
}),
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip"))
message: simpleMessage(testDownload(Constants.GeoIpServer + "/ip")),
})
}
@ -142,7 +142,7 @@
}
return "degraded"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -161,7 +161,7 @@
}
return "online"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -200,7 +200,7 @@
const json = JSON.stringify(s["success"], null, " ")
return "Database is " + Math.floor(timediffDays) + " days out of sync\n\n" + json
})
}),
})
}
@ -213,7 +213,7 @@
layer: "food",
z: 14,
x: 8848,
y: 5828
y: 5828,
})
)
services.push({
@ -224,7 +224,7 @@
}
return "online"
}),
message: new ImmutableStore("See SettingUpPSQL.md to fix")
message: new ImmutableStore("See SettingUpPSQL.md to fix"),
})
}
@ -243,7 +243,7 @@
}
return "degraded"
}),
message: status.map((s) => JSON.stringify(s))
message: status.map((s) => JSON.stringify(s)),
})
}
@ -262,7 +262,7 @@
return "online"
}
return "degraded"
})
}),
})
}
@ -281,7 +281,7 @@
}
return "degraded"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
@ -307,7 +307,7 @@
return "online"
}),
message: simpleMessage(status)
message: simpleMessage(status),
})
}
}
@ -320,7 +320,7 @@
return "online"
}
return "offline"
})
}),
})
services.push({
@ -331,35 +331,33 @@
}
// This code will break in the future. Time to blame past me!
const response = JSON.parse(r["error"].substring("other error: , ".length))
if (response.message === "\"images\" is required") {
if (response.message === '"images" is required') {
// Actual expected behaviour
return "online"
}
console.log("R", response)
return "offline"
})
}),
})
}
{
services.push({
name: "Version Control Server (Forgéjo)",
status: testDownload("https://source.mapcomplete.org", true).mapD(r => {
status: testDownload("https://source.mapcomplete.org", true).mapD((r) => {
if (r["success"]) {
return "online"
}
return "offline"
})
}),
})
services.push({
name: "Translation service (Weblate)",
status: testDownload("https://translate.mapcomplete.org", true).mapD(r => {
status: testDownload("https://translate.mapcomplete.org", true).mapD((r) => {
if (r["success"]) {
return "online"
}
return "offline"
})
}),
})
}
@ -433,7 +431,6 @@
let now = Math.round(new Date().getTime() / 1000)
let twoDaysAgo = now - 2 * 24 * 60 * 60
let lastHour = now - 60 * 60
</script>
<h1>MapComplete status indicators</h1>
@ -457,16 +454,21 @@
<h3>Panoramax & OSM.fr Blurring service</h3>
<a href="https://status.thibaultmol.link/status/panoramax">Panoramax.MapComplete.org status page</a>
<a href="https://munin.openstreetmap.fr/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/index.html#system"
target="_blank" rel="noopener">
<a
href="https://munin.openstreetmap.fr/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/index.html#system"
target="_blank"
rel="noopener"
>
See more statistics for the blurring service
</a>
<img
style="width: 80rem"
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${twoDaysAgo},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`} />
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${twoDaysAgo},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`}
/>
<img
style="width: 80rem"
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${lastHour},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`} />
src={`https://munin.openstreetmap.fr/munin-cgi/munin-cgi-graph/osm37.openstreetmap.fr/blur.vm.openstreetmap.fr/nvidia_gpu_power-pinpoint=${lastHour},${now}.png?&lower_limit=&upper_limit=&size_x=800&size_y=400`}
/>
<button
on:click={() => {
fetch(Constants.ErrorReportServer, {

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from "svelte"
export let imageInfo
@ -8,32 +7,25 @@
let container: HTMLElement
onMount(() => {
console.log("Creating viewer...")
const features = [
{
"type": "Feature",
"properties": { "name": "trap" },
"geometry": {
"coordinates": [
3.742395038713312,
51.05237592785801
],
"type": "Point"
}
}
type: "Feature",
properties: { name: "trap" },
geometry: {
coordinates: [3.742395038713312, 51.05237592785801],
type: "Point",
},
},
]
const viewer = new PhotoSphereViewerWrapper(container, imageInfo, features)
// console.log(panorama, container)
})
</script>
<head>
<link rel="stylesheet" href="./node_modules/pannellum/build/pannellum.css">
<link rel="stylesheet" href="./node_modules/pannellum/build/pannellum.css" />
</head>
<div bind:this={container} class="h-screen w-screen border" style="height: 500px"></div>
<div bind:this={container} class="h-screen w-screen border" style="height: 500px" />

View file

@ -165,7 +165,6 @@ export class Translation extends BaseUIElement {
* Which language will be effectively used for the given language of choice?
*/
public actualLanguage(language: string): "*" | string | undefined {
const txt = this.translations[language]
if (txt !== undefined) {
return language

View file

@ -1,11 +1,11 @@
{
"contributors": [
{
"commits": 9369,
"commits": 9515,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 520,
"commits": 546,
"contributor": "Robin van der Linde"
},
{
@ -49,7 +49,7 @@
"contributor": "riQQ"
},
{
"commits": 26,
"commits": 27,
"contributor": "Osmwithspace"
},
{
@ -100,6 +100,10 @@
"commits": 15,
"contributor": "ToastHawaii"
},
{
"commits": 14,
"contributor": "Bastian Greshake Tzovaras"
},
{
"commits": 14,
"contributor": "danieldegroot2"
@ -148,10 +152,6 @@
"commits": 8,
"contributor": "Mateusz Konieczny"
},
{
"commits": 7,
"contributor": "Bastian Greshake Tzovaras"
},
{
"commits": 7,
"contributor": "OliNau"

View file

@ -181,9 +181,9 @@
"ar"
],
"ER": [
"ti",
"en",
"ar"
"ar",
"ti"
],
"ES": [
"es",
@ -445,7 +445,7 @@
"en"
],
"NE": [
"fr"
"ha"
],
"NG": [
"en"

View file

@ -3931,7 +3931,6 @@
"MC",
"MG",
"MU",
"NE",
"RW",
"SC",
"SN",
@ -4700,6 +4699,9 @@
"uk": "хауса",
"zh_Hant": "豪薩語",
"_meta": {
"countries": [
"NE"
],
"dir": [
"right-to-left",
"left-to-right"

View file

@ -1981,6 +1981,11 @@
"then": "scissors",
"icon": "scissors"
},
{
"if": "value=snowflake",
"then": "snowflake",
"icon": "snowflake"
},
{
"if": "value=square",
"then": "square",
@ -10724,6 +10729,10 @@
"if": "value=aerialway",
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
},
{
"if": "value=all_streets",
"then": "all_streets - Layer with (almost) all streets"
},
{
"if": "value=ambulancestation",
"then": "ambulancestation - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
@ -11006,7 +11015,7 @@
},
{
"if": "value=geocoded_image",
"then": "geocoded_image - undefined"
"then": "geocoded_image - Layer showing green dots where a geocoded image was found. See NearbyImages.svelte. Propreties: 'rotation':number,'spherical':'yes'|'no'"
},
{
"if": "value=ghost_bike",
@ -11114,7 +11123,7 @@
},
{
"if": "value=maproulette_challenge",
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Integrating_Maproulette.md) on how to do this."
},
{
"if": "value=maxspeed",
@ -11400,6 +11409,10 @@
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
},
{
"if": "value=utility_pole",
"then": "utility_pole - Layer showing various types of utility poles."
},
{
"if": "value=vending_machine",
"then": "vending_machine - Layer showing vending machines"
@ -11434,7 +11447,7 @@
},
{
"if": "value=wayside_shrine",
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by."
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by. Wayside crosses can be seen as a sub-type of a wayside shrine, typically in the form of a Christian cross at the side of the road, typically without votive offerings."
},
{
"if": "value=windturbine",
@ -12464,7 +12477,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -13801,7 +13814,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -15186,7 +15199,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -16575,7 +16588,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -17966,7 +17979,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -19355,7 +19368,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",

View file

@ -337,6 +337,11 @@
"then": "scissors",
"icon": "scissors"
},
{
"if": "value=snowflake",
"then": "snowflake",
"icon": "snowflake"
},
{
"if": "value=square",
"then": "square",
@ -603,6 +608,10 @@
"if": "value=aerialway",
"then": "<b>aerialway</b> (builtin) - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
},
{
"if": "value=all_streets",
"then": "<b>all_streets</b> (builtin) - Layer with (almost) all streets"
},
{
"if": "value=ambulancestation",
"then": "<b>ambulancestation</b> (builtin) - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
@ -885,7 +894,7 @@
},
{
"if": "value=geocoded_image",
"then": "<b>geocoded_image</b> (builtin) - undefined"
"then": "<b>geocoded_image</b> (builtin) - Layer showing green dots where a geocoded image was found. See NearbyImages.svelte. Propreties: 'rotation':number,'spherical':'yes'|'no'"
},
{
"if": "value=ghost_bike",
@ -993,7 +1002,7 @@
},
{
"if": "value=maproulette_challenge",
"then": "<b>maproulette_challenge</b> (builtin) - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
"then": "<b>maproulette_challenge</b> (builtin) - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Integrating_Maproulette.md) on how to do this."
},
{
"if": "value=maxspeed",
@ -1279,6 +1288,10 @@
"if": "value=usersettings",
"then": "<b>usersettings</b> (builtin) - A special layer which is not meant to be shown on a map, but which is used to set user settings"
},
{
"if": "value=utility_pole",
"then": "<b>utility_pole</b> (builtin) - Layer showing various types of utility poles."
},
{
"if": "value=vending_machine",
"then": "<b>vending_machine</b> (builtin) - Layer showing vending machines"
@ -1313,7 +1326,7 @@
},
{
"if": "value=wayside_shrine",
"then": "<b>wayside_shrine</b> (builtin) - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by."
"then": "<b>wayside_shrine</b> (builtin) - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by. Wayside crosses can be seen as a sub-type of a wayside shrine, typically in the form of a Christian cross at the side of the road, typically without votive offerings."
},
{
"if": "value=windturbine",
@ -4394,6 +4407,11 @@
"then": "scissors",
"icon": "scissors"
},
{
"if": "value=snowflake",
"then": "snowflake",
"icon": "snowflake"
},
{
"if": "value=square",
"then": "square",
@ -13355,6 +13373,10 @@
"if": "value=aerialway",
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
},
{
"if": "value=all_streets",
"then": "all_streets - Layer with (almost) all streets"
},
{
"if": "value=ambulancestation",
"then": "ambulancestation - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
@ -13637,7 +13659,7 @@
},
{
"if": "value=geocoded_image",
"then": "geocoded_image - undefined"
"then": "geocoded_image - Layer showing green dots where a geocoded image was found. See NearbyImages.svelte. Propreties: 'rotation':number,'spherical':'yes'|'no'"
},
{
"if": "value=ghost_bike",
@ -13745,7 +13767,7 @@
},
{
"if": "value=maproulette_challenge",
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Integrating_Maproulette.md) on how to do this."
},
{
"if": "value=maxspeed",
@ -14031,6 +14053,10 @@
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
},
{
"if": "value=utility_pole",
"then": "utility_pole - Layer showing various types of utility poles."
},
{
"if": "value=vending_machine",
"then": "vending_machine - Layer showing vending machines"
@ -14065,7 +14091,7 @@
},
{
"if": "value=wayside_shrine",
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by."
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by. Wayside crosses can be seen as a sub-type of a wayside shrine, typically in the form of a Christian cross at the side of the road, typically without votive offerings."
},
{
"if": "value=windturbine",
@ -15115,7 +15141,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -16489,7 +16515,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -17912,7 +17938,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -19337,7 +19363,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -20763,7 +20789,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -22188,7 +22214,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -25907,6 +25933,11 @@
"then": "scissors",
"icon": "scissors"
},
{
"if": "value=snowflake",
"then": "snowflake",
"icon": "snowflake"
},
{
"if": "value=square",
"then": "square",
@ -35143,6 +35174,10 @@
"if": "value=aerialway",
"then": "aerialway - Various forms of transport for passengers and goods that use wires, including cable cars, gondolas, chair lifts, drag lifts, and zip lines."
},
{
"if": "value=all_streets",
"then": "all_streets - Layer with (almost) all streets"
},
{
"if": "value=ambulancestation",
"then": "ambulancestation - An ambulance station is an area for storage of ambulance vehicles, medical equipment, personal protective equipment, and other medical supplies."
@ -35425,7 +35460,7 @@
},
{
"if": "value=geocoded_image",
"then": "geocoded_image - undefined"
"then": "geocoded_image - Layer showing green dots where a geocoded image was found. See NearbyImages.svelte. Propreties: 'rotation':number,'spherical':'yes'|'no'"
},
{
"if": "value=ghost_bike",
@ -35533,7 +35568,7 @@
},
{
"if": "value=maproulette_challenge",
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this."
"then": "maproulette_challenge - Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://source.mapcomplete.org/MapComplete/MapComplete/src/branch/develop/Docs/Integrating_Maproulette.md) on how to do this."
},
{
"if": "value=maxspeed",
@ -35819,6 +35854,10 @@
"if": "value=usersettings",
"then": "usersettings - A special layer which is not meant to be shown on a map, but which is used to set user settings"
},
{
"if": "value=utility_pole",
"then": "utility_pole - Layer showing various types of utility poles."
},
{
"if": "value=vending_machine",
"then": "vending_machine - Layer showing vending machines"
@ -35853,7 +35892,7 @@
},
{
"if": "value=wayside_shrine",
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by."
"then": "wayside_shrine - Shrines are religious places that are dedicated to specific deities, saints and other figures of religious importance. Typically, the contain religious depictions and people frequently leave offerings at those places. Wayside shrines are small shrines that can be found next to a road or pathway and are frequented by travellers passing by. Wayside crosses can be seen as a sub-type of a wayside shrine, typically in the form of a Christian cross at the side of the road, typically without votive offerings."
},
{
"if": "value=windturbine",
@ -36930,7 +36969,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -38355,7 +38394,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -39831,7 +39870,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -41307,7 +41346,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -42784,7 +42823,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",
@ -44260,7 +44299,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",

View file

@ -667,7 +667,7 @@
},
{
"if": "value=nsi",
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. "
"then": "<b>nsi</b> Gives a list of possible suggestions for a brand or operator tag. Note: this is detected automatically; there is no need to explicitly set this"
},
{
"if": "value=currency",

View file

@ -29,7 +29,7 @@
"contributor": "Allan Nordhøy"
},
{
"commits": 89,
"commits": 90,
"contributor": "Robin van der Linde"
},
{
@ -236,6 +236,10 @@
"commits": 8,
"contributor": "Vinicius"
},
{
"commits": 7,
"contributor": "Lukáš Jelínek"
},
{
"commits": 7,
"contributor": "Franco"
@ -252,10 +256,6 @@
"commits": 7,
"contributor": "Niels Elgaard Larsen"
},
{
"commits": 6,
"contributor": "Lukáš Jelínek"
},
{
"commits": 6,
"contributor": "Juele juele"

View file

@ -12,6 +12,6 @@ panoramax.imageInfo(imgId).then((imageInfo: ImageData) => {
})*/
let pkey = 1199645818028177
new Mapillary().DownloadImageInfo(pkey).then(imageInfo => {
new Mapillary().DownloadImageInfo(pkey).then((imageInfo) => {
new Test({ target, props: { imageInfo } })
})