forked from MapComplete/MapComplete
chore: automated housekeeping...
This commit is contained in:
parent
c9ce29f206
commit
40e894df8b
294 changed files with 14209 additions and 4192 deletions
|
|
@ -5,10 +5,7 @@ import { Feature } from "geojson"
|
|||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
|
||||
export default class TitleHandler {
|
||||
constructor(
|
||||
selectedElement: Store<Feature>,
|
||||
state: SpecialVisualizationState
|
||||
) {
|
||||
constructor(selectedElement: Store<Feature>, state: SpecialVisualizationState) {
|
||||
const currentTitle: Store<string> = selectedElement.map(
|
||||
(selected) => {
|
||||
const lng = Locale.language.data
|
||||
|
|
@ -34,7 +31,6 @@ export default class TitleHandler {
|
|||
const el = document.createElement("span")
|
||||
el.innerHTML = title
|
||||
return el.textContent + " | " + defaultTitle
|
||||
|
||||
},
|
||||
[Locale.language]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Feature, Polygon } from "geojson"
|
|||
export class BBox {
|
||||
static global: BBox = new BBox([
|
||||
[-180, -90],
|
||||
[180, 90]
|
||||
[180, 90],
|
||||
])
|
||||
readonly maxLat: number
|
||||
readonly maxLon: number
|
||||
|
|
@ -53,7 +53,7 @@ export class BBox {
|
|||
static fromLeafletBounds(bounds) {
|
||||
return new BBox([
|
||||
[bounds.getWest(), bounds.getNorth()],
|
||||
[bounds.getEast(), bounds.getSouth()]
|
||||
[bounds.getEast(), bounds.getSouth()],
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ export class BBox {
|
|||
// Note: x is longitude
|
||||
f["bbox"] = new BBox([
|
||||
[minX, minY],
|
||||
[maxX, maxY]
|
||||
[maxX, maxY],
|
||||
])
|
||||
}
|
||||
return f["bbox"]
|
||||
|
|
@ -94,7 +94,7 @@ export class BBox {
|
|||
}
|
||||
return new BBox([
|
||||
[maxLon, maxLat],
|
||||
[minLon, minLat]
|
||||
[minLon, minLat],
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ export class BBox {
|
|||
public unionWith(other: BBox) {
|
||||
return new BBox([
|
||||
[Math.max(this.maxLon, other.maxLon), Math.max(this.maxLat, other.maxLat)],
|
||||
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)]
|
||||
[Math.min(this.minLon, other.minLon), Math.min(this.minLat, other.minLat)],
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +174,7 @@ export class BBox {
|
|||
|
||||
return new BBox([
|
||||
[lon - s / 2, lat - s / 2],
|
||||
[lon + s / 2, lat + s / 2]
|
||||
[lon + s / 2, lat + s / 2],
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -231,29 +231,26 @@ export class BBox {
|
|||
const lonDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLon - this.minLon) * factor)
|
||||
return new BBox([
|
||||
[this.minLon - lonDiff, this.minLat - latDiff],
|
||||
[this.maxLon + lonDiff, this.maxLat + latDiff]
|
||||
[this.maxLon + lonDiff, this.maxLat + latDiff],
|
||||
])
|
||||
}
|
||||
|
||||
padAbsolute(degrees: number): BBox {
|
||||
return new BBox([
|
||||
[this.minLon - degrees, this.minLat - degrees],
|
||||
[this.maxLon + degrees, this.maxLat + degrees]
|
||||
[this.maxLon + degrees, this.maxLat + degrees],
|
||||
])
|
||||
}
|
||||
|
||||
toLngLat(): [[number, number], [number, number]] {
|
||||
return [
|
||||
[this.minLon, this.minLat],
|
||||
[this.maxLon, this.maxLat]
|
||||
[this.maxLon, this.maxLat],
|
||||
]
|
||||
}
|
||||
|
||||
toLngLatFlat(): [number, number, number, number] {
|
||||
return [
|
||||
this.minLon, this.minLat,
|
||||
this.maxLon, this.maxLat,
|
||||
]
|
||||
return [this.minLon, this.minLat, this.maxLon, this.maxLat]
|
||||
}
|
||||
|
||||
public asGeojsonCached() {
|
||||
|
|
@ -267,7 +264,7 @@ export class BBox {
|
|||
return {
|
||||
type: "Feature",
|
||||
properties: properties,
|
||||
geometry: this.asGeometry()
|
||||
geometry: this.asGeometry(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,9 +277,9 @@ export class BBox {
|
|||
[this.maxLon, this.minLat],
|
||||
[this.maxLon, this.maxLat],
|
||||
[this.minLon, this.maxLat],
|
||||
[this.minLon, this.minLat]
|
||||
]
|
||||
]
|
||||
[this.minLon, this.minLat],
|
||||
],
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +306,7 @@ export class BBox {
|
|||
minLon,
|
||||
maxLon,
|
||||
minLat,
|
||||
maxLat
|
||||
maxLat,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export default class DetermineTheme {
|
|||
const dict = new Map<string, QuestionableTagRenderingConfigJson>()
|
||||
|
||||
for (const tagRendering of questions.tagRenderings) {
|
||||
dict.set(tagRendering.id, <QuestionableTagRenderingConfigJson> tagRendering)
|
||||
dict.set(tagRendering.id, <QuestionableTagRenderingConfigJson>tagRendering)
|
||||
}
|
||||
|
||||
return dict
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ import { OsmFeature } from "../../../Models/OsmFeature"
|
|||
* If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained
|
||||
*/
|
||||
export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource>
|
||||
implements IndexedFeatureSource {
|
||||
implements IndexedFeatureSource
|
||||
{
|
||||
public features: UIEventSource<Feature[]> = new UIEventSource([])
|
||||
public readonly featuresById: Store<Map<string, Feature>>
|
||||
protected readonly _featuresById: UIEventSource<Map<string, Feature>>
|
||||
|
|
@ -118,10 +119,11 @@ export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSour
|
|||
}
|
||||
|
||||
export class UpdatableFeatureSourceMerger<
|
||||
Src extends UpdatableFeatureSource = UpdatableFeatureSource
|
||||
>
|
||||
Src extends UpdatableFeatureSource = UpdatableFeatureSource
|
||||
>
|
||||
extends FeatureSourceMerger<Src>
|
||||
implements IndexedFeatureSource, UpdatableFeatureSource {
|
||||
implements IndexedFeatureSource, UpdatableFeatureSource
|
||||
{
|
||||
constructor(...sources: Src[]) {
|
||||
super(...sources)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { Store, UIEventSource } from "../../UIEventSource"
|
|||
import { FeatureSourceForTile, UpdatableFeatureSource } from "../FeatureSource"
|
||||
import { MvtToGeojson } from "mvt-to-geojson"
|
||||
|
||||
|
||||
export default class MvtSource implements FeatureSourceForTile, UpdatableFeatureSource {
|
||||
public readonly features: Store<GeojsonFeature<Geometry, { [name: string]: any }>[]>
|
||||
public readonly x: number
|
||||
|
|
@ -28,7 +27,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
|
|||
y: number,
|
||||
z: number,
|
||||
layerName?: string,
|
||||
isActive?: Store<boolean>,
|
||||
isActive?: Store<boolean>
|
||||
) {
|
||||
this._url = url
|
||||
this._layerName = layerName
|
||||
|
|
@ -43,7 +42,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
|
|||
}
|
||||
return fs
|
||||
},
|
||||
[isActive],
|
||||
[isActive]
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +53,6 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
|
|||
await this.currentlyRunning
|
||||
}
|
||||
|
||||
|
||||
private async download(): Promise<void> {
|
||||
try {
|
||||
const result = await fetch(this._url)
|
||||
|
|
@ -66,7 +64,7 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
|
|||
const features = MvtToGeojson.fromBuffer(buffer, this.x, this.y, this.z)
|
||||
for (const feature of features) {
|
||||
const properties = feature.properties
|
||||
if(!properties["osm_type"]){
|
||||
if (!properties["osm_type"]) {
|
||||
continue
|
||||
}
|
||||
let type: string = "node"
|
||||
|
|
@ -90,6 +88,4 @@ export default class MvtSource implements FeatureSourceForTile, UpdatableFeature
|
|||
console.error("Could not download MVT " + this._url + " tile due to", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,7 @@ export class SummaryTileSourceRewriter implements FeatureSource {
|
|||
filteredLayers: ReadonlyMap<string, FilteredLayer>
|
||||
) {
|
||||
this.filteredLayers = Array.from(filteredLayers.values()).filter(
|
||||
(l) =>
|
||||
Constants.priviliged_layers.indexOf(<any>l.layerDef.id) < 0
|
||||
(l) => Constants.priviliged_layers.indexOf(<any>l.layerDef.id) < 0
|
||||
)
|
||||
this._summarySource = summarySource
|
||||
filteredLayers.forEach((v) => {
|
||||
|
|
|
|||
|
|
@ -95,8 +95,14 @@ export class GeoOperations {
|
|||
/**
|
||||
* Starting on `from`, travels `distance` meters in the direction of the `bearing` (default: 90)
|
||||
*/
|
||||
static destination(from: Coord | [number,number],distance: number, bearing: number = 90): [number,number]{
|
||||
return <[number,number]> turf.destination(from, distance, bearing, {units: "meters"}).geometry.coordinates
|
||||
static destination(
|
||||
from: Coord | [number, number],
|
||||
distance: number,
|
||||
bearing: number = 90
|
||||
): [number, number] {
|
||||
return <[number, number]>(
|
||||
turf.destination(from, distance, bearing, { units: "meters" }).geometry.coordinates
|
||||
)
|
||||
}
|
||||
|
||||
static convexHull(featureCollection, options: { concavity?: number }) {
|
||||
|
|
@ -928,13 +934,13 @@ export class GeoOperations {
|
|||
if (meters === undefined) {
|
||||
return ""
|
||||
}
|
||||
meters = Utils.roundHuman( Math.round(meters))
|
||||
meters = Utils.roundHuman(Math.round(meters))
|
||||
if (meters < 1000) {
|
||||
return Utils.roundHuman(meters) + "m"
|
||||
}
|
||||
|
||||
if (meters >= 10000) {
|
||||
const km = Utils.roundHuman(Math.round(meters / 1000))
|
||||
const km = Utils.roundHuman(Math.round(meters / 1000))
|
||||
return km + "km"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ export default class AllImageProviders {
|
|||
AllImageProviders.genericImageProvider,
|
||||
]
|
||||
public static apiUrls: string[] = [].concat(
|
||||
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls()),
|
||||
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
|
||||
)
|
||||
public static defaultKeys = [].concat(
|
||||
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes),
|
||||
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
|
||||
)
|
||||
private static providersByName = {
|
||||
imgur: Imgur.singleton,
|
||||
|
|
@ -71,13 +71,12 @@ export default class AllImageProviders {
|
|||
*/
|
||||
public static LoadImagesFor(
|
||||
tags: Store<Record<string, string>>,
|
||||
tagKey?: string[],
|
||||
tagKey?: string[]
|
||||
): Store<ProvidedImage[]> {
|
||||
if (tags?.data?.id === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
const source = new UIEventSource([])
|
||||
const allSources: Store<ProvidedImage[]>[] = []
|
||||
for (const imageProvider of AllImageProviders.ImageAttributionSource) {
|
||||
|
|
@ -86,11 +85,11 @@ export default class AllImageProviders {
|
|||
However, we override them if a custom image tag is set, e.g. 'image:menu'
|
||||
*/
|
||||
const prefixes = tagKey ?? imageProvider.defaultKeyPrefixes
|
||||
const singleSource = tags.bindD(tags => imageProvider.getRelevantUrls(tags, prefixes))
|
||||
const singleSource = tags.bindD((tags) => imageProvider.getRelevantUrls(tags, prefixes))
|
||||
allSources.push(singleSource)
|
||||
singleSource.addCallbackAndRunD((_) => {
|
||||
const all: ProvidedImage[] = [].concat(...allSources.map((source) => source.data))
|
||||
const dedup = Utils.DedupOnId(all, i => i?.id ?? i?.url)
|
||||
const dedup = Utils.DedupOnId(all, (i) => i?.id ?? i?.url)
|
||||
source.set(dedup)
|
||||
})
|
||||
}
|
||||
|
|
@ -103,7 +102,7 @@ export default class AllImageProviders {
|
|||
*/
|
||||
public static loadImagesFrom(urls: string[]): Store<ProvidedImage[]> {
|
||||
const tags = {
|
||||
id:"na"
|
||||
id: "na",
|
||||
}
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i]
|
||||
|
|
|
|||
|
|
@ -27,12 +27,14 @@ export default class GenericImageProvider extends ImageProvider {
|
|||
return undefined
|
||||
}
|
||||
|
||||
return [{
|
||||
key: key,
|
||||
url: value,
|
||||
provider: this,
|
||||
id: value,
|
||||
}]
|
||||
return [
|
||||
{
|
||||
key: key,
|
||||
url: value,
|
||||
provider: this,
|
||||
id: value,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
SourceIcon() {
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@ export interface ProvidedImage {
|
|||
key: string
|
||||
provider: ImageProvider
|
||||
id: string
|
||||
date?: Date,
|
||||
date?: Date
|
||||
status?: string | "ready"
|
||||
/**
|
||||
* Compass angle of the taken image
|
||||
* 0 = north, 90° = East
|
||||
*/
|
||||
rotation?: number
|
||||
lat?: number,
|
||||
lon?: number,
|
||||
lat?: number
|
||||
lon?: number
|
||||
host?: string
|
||||
}
|
||||
|
||||
|
|
@ -26,8 +26,10 @@ export default abstract class ImageProvider {
|
|||
|
||||
public abstract readonly name: string
|
||||
|
||||
public abstract SourceIcon(img?: {id: string, url: string, host?: string}, location?: { lon: number; lat: number }): BaseUIElement
|
||||
|
||||
public abstract SourceIcon(
|
||||
img?: { id: string; url: string; host?: string },
|
||||
location?: { lon: number; lat: number }
|
||||
): BaseUIElement
|
||||
|
||||
/**
|
||||
* Gets all the relevant URLS for the given tags and for the given prefixes;
|
||||
|
|
@ -35,12 +37,19 @@ export default abstract class ImageProvider {
|
|||
* @param tags
|
||||
* @param prefixes
|
||||
*/
|
||||
public async getRelevantUrlsFor(tags: Record<string, string>, prefixes: string[]): Promise<ProvidedImage[]> {
|
||||
public async getRelevantUrlsFor(
|
||||
tags: Record<string, string>,
|
||||
prefixes: string[]
|
||||
): Promise<ProvidedImage[]> {
|
||||
const relevantUrls: ProvidedImage[] = []
|
||||
const seenValues = new Set<string>()
|
||||
|
||||
for (const key in tags) {
|
||||
if (!prefixes.some((prefix) => key === prefix || key.match(new RegExp(prefix+":[0-9]+")))) {
|
||||
if (
|
||||
!prefixes.some(
|
||||
(prefix) => key === prefix || key.match(new RegExp(prefix + ":[0-9]+"))
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const values = Utils.NoEmpty(tags[key]?.split(";")?.map((v) => v.trim()) ?? [])
|
||||
|
|
@ -50,10 +59,10 @@ export default abstract class ImageProvider {
|
|||
}
|
||||
seenValues.add(value)
|
||||
let images = this.ExtractUrls(key, value)
|
||||
if(!Array.isArray(images)){
|
||||
images = await images
|
||||
if (!Array.isArray(images)) {
|
||||
images = await images
|
||||
}
|
||||
if(images){
|
||||
if (images) {
|
||||
relevantUrls.push(...images)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,12 +70,17 @@ export default abstract class ImageProvider {
|
|||
return relevantUrls
|
||||
}
|
||||
|
||||
public getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
public getRelevantUrls(
|
||||
tags: Record<string, string>,
|
||||
prefixes: string[]
|
||||
): Store<ProvidedImage[]> {
|
||||
return Stores.FromPromise(this.getRelevantUrlsFor(tags, prefixes))
|
||||
}
|
||||
|
||||
|
||||
public abstract ExtractUrls(key: string, value: string): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
|
||||
public abstract ExtractUrls(
|
||||
key: string,
|
||||
value: string
|
||||
): undefined | ProvidedImage[] | Promise<ProvidedImage[]>
|
||||
|
||||
public abstract DownloadAttribution(providedImage: {
|
||||
url: string
|
||||
|
|
|
|||
|
|
@ -28,7 +28,10 @@ export class ImageUploadManager {
|
|||
private readonly _osmConnection: OsmConnection
|
||||
private readonly _changes: Changes
|
||||
public readonly isUploading: Store<boolean>
|
||||
private readonly _reportError: (message: (string | Error | XMLHttpRequest), extramessage?: string) => Promise<void>
|
||||
private readonly _reportError: (
|
||||
message: string | Error | XMLHttpRequest,
|
||||
extramessage?: string
|
||||
) => Promise<void>
|
||||
|
||||
constructor(
|
||||
layout: ThemeConfig,
|
||||
|
|
@ -38,7 +41,10 @@ export class ImageUploadManager {
|
|||
changes: Changes,
|
||||
gpsLocation: Store<GeolocationCoordinates | undefined>,
|
||||
allFeatures: IndexedFeatureSource,
|
||||
reportError: (message: string | Error | XMLHttpRequest, extramessage?: string ) => Promise<void>
|
||||
reportError: (
|
||||
message: string | Error | XMLHttpRequest,
|
||||
extramessage?: string
|
||||
) => Promise<void>
|
||||
) {
|
||||
this._uploader = uploader
|
||||
this._featureProperties = featureProperties
|
||||
|
|
@ -56,7 +62,7 @@ export class ImageUploadManager {
|
|||
(startedCount) => {
|
||||
return startedCount > failed.data + done.data
|
||||
},
|
||||
[failed, done],
|
||||
[failed, done]
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +111,7 @@ export class ImageUploadManager {
|
|||
file: File,
|
||||
tagsStore: UIEventSource<OsmTags>,
|
||||
targetKey: string,
|
||||
noblur: boolean,
|
||||
noblur: boolean
|
||||
): Promise<void> {
|
||||
const canBeUploaded = this.canBeUploaded(file)
|
||||
if (canBeUploaded !== true) {
|
||||
|
|
@ -130,10 +136,16 @@ export class ImageUploadManager {
|
|||
}
|
||||
const properties = this._featureProperties.getStore(featureId)
|
||||
|
||||
const action = new LinkImageAction(featureId, uploadResult. key, uploadResult . value, properties, {
|
||||
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
|
||||
changeType: "add-image",
|
||||
})
|
||||
const action = new LinkImageAction(
|
||||
featureId,
|
||||
uploadResult.key,
|
||||
uploadResult.value,
|
||||
properties,
|
||||
{
|
||||
theme: tags?.data?.["_orig_theme"] ?? this._theme.id,
|
||||
changeType: "add-image",
|
||||
}
|
||||
)
|
||||
|
||||
await this._changes.applyAction(action)
|
||||
}
|
||||
|
|
@ -153,34 +165,51 @@ export class ImageUploadManager {
|
|||
if (this._gps.data) {
|
||||
location = [this._gps.data.longitude, this._gps.data.latitude]
|
||||
}
|
||||
if (location === undefined || location?.some(l => l === undefined)) {
|
||||
if (location === undefined || location?.some((l) => l === undefined)) {
|
||||
const feature = this._indexedFeatures.featuresById.data.get(featureId)
|
||||
location = GeoOperations.centerpointCoordinates(feature)
|
||||
}
|
||||
try {
|
||||
({ key, value, absoluteUrl } = await this._uploader.uploadImage(blob, location, author, noblur))
|
||||
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
blob,
|
||||
location,
|
||||
author,
|
||||
noblur
|
||||
))
|
||||
} catch (e) {
|
||||
this.increaseCountFor(this._uploadRetried, featureId)
|
||||
console.error("Could not upload image, trying again:", e)
|
||||
try {
|
||||
({ key, value , absoluteUrl} = await this._uploader.uploadImage(blob, location, author, noblur))
|
||||
;({ key, value, absoluteUrl } = await this._uploader.uploadImage(
|
||||
blob,
|
||||
location,
|
||||
author,
|
||||
noblur
|
||||
))
|
||||
this.increaseCountFor(this._uploadRetriedSuccess, featureId)
|
||||
} catch (e) {
|
||||
console.error("Could again not upload image due to", e)
|
||||
this.increaseCountFor(this._uploadFailed, featureId)
|
||||
await this._reportError(e, JSON.stringify({ctx:"While uploading an image in the Image Upload Manager", featureId, author, targetKey}))
|
||||
await this._reportError(
|
||||
e,
|
||||
JSON.stringify({
|
||||
ctx: "While uploading an image in the Image Upload Manager",
|
||||
featureId,
|
||||
author,
|
||||
targetKey,
|
||||
})
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
console.log("Uploading image done, creating action for", featureId)
|
||||
key = targetKey ?? key
|
||||
if(targetKey){
|
||||
if (targetKey) {
|
||||
// This is a non-standard key, so we use the image link directly
|
||||
value = absoluteUrl
|
||||
}
|
||||
this.increaseCountFor(this._uploadFinished, featureId)
|
||||
return {key, absoluteUrl, value}
|
||||
|
||||
return { key, absoluteUrl, value }
|
||||
}
|
||||
|
||||
private getCounterFor(collection: Map<string, UIEventSource<number>>, key: string | "*") {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ export interface ImageUploader {
|
|||
*/
|
||||
uploadImage(
|
||||
blob: File,
|
||||
currentGps: [number,number],
|
||||
currentGps: [number, number],
|
||||
author: string,
|
||||
noblur: boolean
|
||||
): Promise<UploadResult>
|
||||
}
|
||||
|
||||
export interface UploadResult{ key: string; value: string, absoluteUrl: string }
|
||||
export interface UploadResult {
|
||||
key: string
|
||||
value: string
|
||||
absoluteUrl: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export class Imgur extends ImageProvider {
|
|||
return [Imgur.apiUrl]
|
||||
}
|
||||
|
||||
|
||||
SourceIcon(): BaseUIElement {
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -32,7 +31,7 @@ export class Imgur extends ImageProvider {
|
|||
key: key,
|
||||
provider: this,
|
||||
id: value,
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
return undefined
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export class Mapillary extends ImageProvider {
|
|||
}
|
||||
|
||||
SourceIcon(
|
||||
img: {id: string, url: string},
|
||||
img: { id: string; url: string },
|
||||
location?: {
|
||||
lon: number
|
||||
lat: number
|
||||
|
|
@ -182,7 +182,7 @@ export class Mapillary extends ImageProvider {
|
|||
key,
|
||||
rotation,
|
||||
lat: geometry.coordinates[1],
|
||||
lon: geometry.coordinates[0]
|
||||
lon: geometry.coordinates[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,27 +11,35 @@ import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
|||
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
|
||||
import Link from "../../UI/Base/Link"
|
||||
|
||||
|
||||
export default class PanoramaxImageProvider extends ImageProvider {
|
||||
|
||||
public static readonly singleton = new PanoramaxImageProvider()
|
||||
private static readonly xyz = new PanoramaxXYZ()
|
||||
private static defaultPanoramax = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token)
|
||||
private static defaultPanoramax = new AuthorizedPanoramax(
|
||||
Constants.panoramax.url,
|
||||
Constants.panoramax.token
|
||||
)
|
||||
|
||||
public defaultKeyPrefixes: string[] = ["panoramax"]
|
||||
public readonly name: string = "panoramax"
|
||||
|
||||
private static knownMeta: Record<string, { data: ImageData, time: Date }> = {}
|
||||
private static knownMeta: Record<string, { data: ImageData; time: Date }> = {}
|
||||
|
||||
public SourceIcon(img?: { id: string, url: string, host?: string }, location?: {
|
||||
lon: number;
|
||||
lat: number;
|
||||
}): BaseUIElement {
|
||||
public SourceIcon(
|
||||
img?: { id: string; url: string; host?: string },
|
||||
location?: {
|
||||
lon: number
|
||||
lat: number
|
||||
}
|
||||
): BaseUIElement {
|
||||
const p = new Panoramax(img.host)
|
||||
return new Link(new SvelteUIElement(Panoramax_bw), p.createViewLink({
|
||||
imageId: img?.id,
|
||||
location,
|
||||
}), true)
|
||||
return new Link(
|
||||
new SvelteUIElement(Panoramax_bw),
|
||||
p.createViewLink({
|
||||
imageId: img?.id,
|
||||
location,
|
||||
}),
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
public addKnownMeta(meta: ImageData) {
|
||||
|
|
@ -43,25 +51,24 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
* @param id
|
||||
* @private
|
||||
*/
|
||||
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData, url: string }> {
|
||||
private async getInfoFromMapComplete(id: string): Promise<{ data: ImageData; url: string }> {
|
||||
const sequence = "6e702976-580b-419c-8fb3-cf7bd364e6f8" // We always reuse this sequence
|
||||
const url = `https://panoramax.mapcomplete.org/`
|
||||
const data = await PanoramaxImageProvider.defaultPanoramax.imageInfo(id, sequence)
|
||||
return { url, data }
|
||||
}
|
||||
|
||||
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData, url: string }> {
|
||||
private async getInfoFromXYZ(imageId: string): Promise<{ data: ImageData; url: string }> {
|
||||
const data = await PanoramaxImageProvider.xyz.imageInfo(imageId)
|
||||
return { data, url: "https://api.panoramax.xyz/" }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads a geovisio-somewhat-looking-like-geojson object and converts it to a provided image
|
||||
* @param meta
|
||||
* @private
|
||||
*/
|
||||
private featureToImage(info: { data: ImageData, url: string }) {
|
||||
private featureToImage(info: { data: ImageData; url: string }) {
|
||||
const meta = info?.data
|
||||
if (!meta) {
|
||||
return undefined
|
||||
|
|
@ -82,8 +89,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
id: meta.id,
|
||||
url: makeAbsolute(meta.assets.sd.href),
|
||||
url_hd: makeAbsolute(meta.assets.hd.href),
|
||||
host: meta["links"].find(l => l.rel === "root")?.href,
|
||||
lon, lat,
|
||||
host: meta["links"].find((l) => l.rel === "root")?.href,
|
||||
lon,
|
||||
lat,
|
||||
key: "panoramax",
|
||||
provider: this,
|
||||
status: meta.properties["geovisio:status"],
|
||||
|
|
@ -92,14 +100,13 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
private async getInfoFor(id: string): Promise<{ data: ImageData, url: string }> {
|
||||
private async getInfoFor(id: string): Promise<{ data: ImageData; url: string }> {
|
||||
if (!id.match(/^[a-zA-Z0-9-]+$/)) {
|
||||
return undefined
|
||||
}
|
||||
const cached = PanoramaxImageProvider.knownMeta[id]
|
||||
if (cached) {
|
||||
if (new Date().getTime() - cached.time.getTime() < 1000) {
|
||||
|
||||
return { data: cached.data, url: undefined }
|
||||
}
|
||||
}
|
||||
|
|
@ -120,10 +127,9 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
if (!Panoramax.isId(value)) {
|
||||
return undefined
|
||||
}
|
||||
return [await this.getInfoFor(value).then(r => this.featureToImage(<any>r))]
|
||||
return [await this.getInfoFor(value).then((r) => this.featureToImage(<any>r))]
|
||||
}
|
||||
|
||||
|
||||
getRelevantUrls(tags: Record<string, string>, prefixes: string[]): Store<ProvidedImage[]> {
|
||||
const source = UIEventSource.FromPromise(super.getRelevantUrlsFor(tags, prefixes))
|
||||
|
||||
|
|
@ -131,14 +137,15 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
if (data === undefined) {
|
||||
return true
|
||||
}
|
||||
return data?.some(img => img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken")
|
||||
return data?.some(
|
||||
(img) =>
|
||||
img?.status !== undefined && img?.status !== "ready" && img?.status !== "broken"
|
||||
)
|
||||
}
|
||||
|
||||
Stores.Chronic(1500, () =>
|
||||
hasLoading(source.data),
|
||||
).addCallback(_ => {
|
||||
Stores.Chronic(1500, () => hasLoading(source.data)).addCallback((_) => {
|
||||
console.log("UPdating... ")
|
||||
super.getRelevantUrlsFor(tags, prefixes).then(data => {
|
||||
super.getRelevantUrlsFor(tags, prefixes).then((data) => {
|
||||
console.log("New panoramax data is", data, hasLoading(data))
|
||||
source.set(data)
|
||||
return !hasLoading(data)
|
||||
|
|
@ -148,7 +155,10 @@ export default class PanoramaxImageProvider extends ImageProvider {
|
|||
return source
|
||||
}
|
||||
|
||||
public async DownloadAttribution(providedImage: { url: string; id: string; }): Promise<LicenseInfo> {
|
||||
public async DownloadAttribution(providedImage: {
|
||||
url: string
|
||||
id: string
|
||||
}): Promise<LicenseInfo> {
|
||||
const meta = await this.getInfoFor(providedImage.id)
|
||||
|
||||
return {
|
||||
|
|
@ -171,9 +181,14 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
this._panoramax = new AuthorizedPanoramax(url, token)
|
||||
}
|
||||
|
||||
async uploadImage(blob: File, currentGps: [number, number], author: string, noblur: boolean = false): Promise<{
|
||||
key: string;
|
||||
value: string;
|
||||
async uploadImage(
|
||||
blob: File,
|
||||
currentGps: [number, number],
|
||||
author: string,
|
||||
noblur: boolean = false
|
||||
): Promise<{
|
||||
key: string
|
||||
value: string
|
||||
absoluteUrl: string
|
||||
}> {
|
||||
// https://panoramax.openstreetmap.fr/api/docs/swagger#/
|
||||
|
|
@ -183,7 +198,7 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
let hasGPS = false
|
||||
try {
|
||||
const tags = await ExifReader.load(blob)
|
||||
hasDate = tags?.DateTime !== undefined
|
||||
hasDate = tags?.DateTime !== undefined
|
||||
hasGPS = tags?.GPSLatitude !== undefined && tags?.GPSLongitude !== undefined
|
||||
} catch (e) {
|
||||
console.error("Could not read EXIF-tags")
|
||||
|
|
@ -203,7 +218,6 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
exifOverride: {
|
||||
Artist: author,
|
||||
},
|
||||
|
||||
})
|
||||
PanoramaxImageProvider.singleton.addKnownMeta(img)
|
||||
return {
|
||||
|
|
@ -212,5 +226,4 @@ export class PanoramaxUploader implements ImageUploader {
|
|||
absoluteUrl: img.assets.hd.href,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ 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)])
|
||||
private static readonly keyBlacklist: ReadonlySet<string> = new Set([
|
||||
"mapillary",
|
||||
...Utils.Times((i) => "mapillary:" + i, 10),
|
||||
])
|
||||
|
||||
private constructor() {
|
||||
super()
|
||||
|
|
@ -26,7 +28,7 @@ export class WikidataImageProvider extends ImageProvider {
|
|||
return new SvelteUIElement(Wikidata_icon)
|
||||
}
|
||||
|
||||
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
|
||||
public async ExtractUrls(key: string, value: string): Promise<ProvidedImage[]> {
|
||||
if (WikidataImageProvider.keyBlacklist.has(key)) {
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return value
|
||||
}
|
||||
const baseUrl = `https://commons.wikimedia.org/wiki/Special:FilePath/${encodeURIComponent(
|
||||
value,
|
||||
value
|
||||
)}`
|
||||
if (useHd) {
|
||||
return baseUrl
|
||||
|
|
@ -106,7 +106,8 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
value = WikimediaImageProvider.removeCommonsPrefix(value)
|
||||
if (value.startsWith("Category:")) {
|
||||
const urls = await Wikimedia.GetCategoryContents(value)
|
||||
return urls.filter((url) => url.startsWith("File:"))
|
||||
return urls
|
||||
.filter((url) => url.startsWith("File:"))
|
||||
.map((image) => this.UrlForImage(image))
|
||||
}
|
||||
if (value.startsWith("File:")) {
|
||||
|
|
@ -117,7 +118,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
return undefined
|
||||
}
|
||||
// We do a last effort and assume this is a file
|
||||
return [(this.UrlForImage("File:" + value))]
|
||||
return [this.UrlForImage("File:" + value)]
|
||||
}
|
||||
|
||||
public async DownloadAttribution(img: { url: string }): Promise<LicenseInfo> {
|
||||
|
|
@ -147,7 +148,7 @@ export class WikimediaImageProvider extends ImageProvider {
|
|||
console.warn(
|
||||
"The file",
|
||||
filename,
|
||||
"has no usable metedata or license attached... Please fix the license info file yourself!",
|
||||
"has no usable metedata or license attached... Please fix the license info file yourself!"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default class ChangeLocationAction extends OsmChangeAction {
|
|||
meta: {
|
||||
theme: string
|
||||
reason: string
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(id, true)
|
||||
this.state = state
|
||||
|
|
@ -66,12 +66,10 @@ export default class ChangeLocationAction extends OsmChangeAction {
|
|||
return [d]
|
||||
}
|
||||
|
||||
const insertIntoWay = new InsertPointIntoWayAction(
|
||||
lat, lon, this._id, snapToWay, {
|
||||
allowReuseOfPreviouslyCreatedPoints: false,
|
||||
reusePointWithinMeters: 0.25,
|
||||
},
|
||||
).prepareChangeDescription()
|
||||
const insertIntoWay = new InsertPointIntoWayAction(lat, lon, this._id, snapToWay, {
|
||||
allowReuseOfPreviouslyCreatedPoints: false,
|
||||
reusePointWithinMeters: 0.25,
|
||||
}).prepareChangeDescription()
|
||||
|
||||
return [d, { ...insertIntoWay, meta: d.meta }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
theme: string
|
||||
changeType: "create" | "import" | null
|
||||
specialMotivation?: string
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(null, basicTags !== undefined && basicTags.length > 0)
|
||||
this._basicTags = basicTags
|
||||
|
|
@ -102,21 +102,12 @@ export default class CreateNewNodeAction extends OsmCreateAction {
|
|||
return [newPointChange]
|
||||
}
|
||||
|
||||
const change = new InsertPointIntoWayAction(
|
||||
this._lat,
|
||||
this._lon,
|
||||
id,
|
||||
this._snapOnto,
|
||||
{
|
||||
reusePointWithinMeters: this._reusePointDistance,
|
||||
allowReuseOfPreviouslyCreatedPoints: this._reusePreviouslyCreatedPoint,
|
||||
},
|
||||
).prepareChangeDescription()
|
||||
const change = new InsertPointIntoWayAction(this._lat, this._lon, id, this._snapOnto, {
|
||||
reusePointWithinMeters: this._reusePointDistance,
|
||||
allowReuseOfPreviouslyCreatedPoints: this._reusePreviouslyCreatedPoint,
|
||||
}).prepareChangeDescription()
|
||||
|
||||
return [
|
||||
newPointChange,
|
||||
{ ...change, meta: this.meta },
|
||||
]
|
||||
return [newPointChange, { ...change, meta: this.meta }]
|
||||
}
|
||||
|
||||
private setElementId(id: number) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { ChangeDescription } from "./ChangeDescription"
|
|||
import { GeoOperations } from "../../GeoOperations"
|
||||
import { OsmWay } from "../OsmObject"
|
||||
|
||||
export default class InsertPointIntoWayAction {
|
||||
export default class InsertPointIntoWayAction {
|
||||
private readonly _lat: number
|
||||
private readonly _lon: number
|
||||
private readonly _idToInsert: number
|
||||
|
|
@ -21,22 +21,19 @@ export default class InsertPointIntoWayAction {
|
|||
allowReuseOfPreviouslyCreatedPoints?: boolean
|
||||
reusePointWithinMeters?: number
|
||||
}
|
||||
){
|
||||
) {
|
||||
this._lat = lat
|
||||
this._lon = lon
|
||||
this._idToInsert = idToInsert
|
||||
this._snapOnto = snapOnto
|
||||
this._options = options
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to create the changedescription of the way where the point is inserted
|
||||
* Returns `undefined` if inserting failed
|
||||
*/
|
||||
public prepareChangeDescription(): Omit<ChangeDescription, "meta"> | undefined {
|
||||
|
||||
|
||||
public prepareChangeDescription(): Omit<ChangeDescription, "meta"> | undefined {
|
||||
// Project the point onto the way
|
||||
console.log("Snapping a node onto an existing way...")
|
||||
const geojson = this._snapOnto.asGeoJson()
|
||||
|
|
@ -59,13 +56,19 @@ export default class InsertPointIntoWayAction {
|
|||
}
|
||||
|
||||
const prev = outerring[index]
|
||||
if (GeoOperations.distanceBetween(prev, projectedCoor) < this._options.reusePointWithinMeters) {
|
||||
if (
|
||||
GeoOperations.distanceBetween(prev, projectedCoor) <
|
||||
this._options.reusePointWithinMeters
|
||||
) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index]
|
||||
reusedPointCoordinates = this._snapOnto.coordinates[index]
|
||||
}
|
||||
const next = outerring[index + 1]
|
||||
if (GeoOperations.distanceBetween(next, projectedCoor) < this._options.reusePointWithinMeters) {
|
||||
if (
|
||||
GeoOperations.distanceBetween(next, projectedCoor) <
|
||||
this._options.reusePointWithinMeters
|
||||
) {
|
||||
// We reuse this point instead!
|
||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||
reusedPointCoordinates = this._snapOnto.coordinates[index + 1]
|
||||
|
|
@ -82,15 +85,13 @@ export default class InsertPointIntoWayAction {
|
|||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||
ids.splice(index + 1, 0, this._idToInsert)
|
||||
|
||||
return {
|
||||
return {
|
||||
type: "way",
|
||||
id: this._snapOnto.id,
|
||||
changes: {
|
||||
coordinates: locations,
|
||||
nodes: ids,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction implements Pr
|
|||
const url = `${
|
||||
this.state.osmConnection?._oauth_config?.url ?? "https://api.openstreetmap.org"
|
||||
}/api/0.6/${this.wayToReplaceId}/full`
|
||||
const rawData = await Utils.downloadJsonCached<{elements: any[]}>(url, 1000)
|
||||
const rawData = await Utils.downloadJsonCached<{ elements: any[] }>(url, 1000)
|
||||
parsed = OsmObject.ParseObjects(rawData.elements)
|
||||
}
|
||||
const allNodes = parsed.filter((o) => o.type === "node")
|
||||
|
|
|
|||
|
|
@ -49,11 +49,11 @@ export class Changes {
|
|||
featureSwitches: {
|
||||
featureSwitchMorePrivacy?: Store<boolean>
|
||||
featureSwitchIsTesting?: Store<boolean>
|
||||
},
|
||||
osmConnection: OsmConnection,
|
||||
reportError?: (error: string) => void,
|
||||
featureProperties?: FeaturePropertiesStore,
|
||||
historicalUserLocations?: FeatureSource,
|
||||
}
|
||||
osmConnection: OsmConnection
|
||||
reportError?: (error: string) => void
|
||||
featureProperties?: FeaturePropertiesStore
|
||||
historicalUserLocations?: FeatureSource
|
||||
allElements?: IndexedFeatureSource
|
||||
},
|
||||
leftRightSensitive: boolean = false,
|
||||
|
|
@ -64,8 +64,11 @@ export class Changes {
|
|||
this.allChanges.setData([...this.pendingChanges.data])
|
||||
// If a pending change contains a negative ID, we save that
|
||||
this._nextId = Math.min(-1, ...(this.pendingChanges.data?.map((pch) => pch.id ?? 0) ?? []))
|
||||
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("."))
|
||||
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._nextId = -100
|
||||
}
|
||||
this.state = state
|
||||
|
|
@ -84,12 +87,12 @@ export class Changes {
|
|||
// This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
|
||||
}
|
||||
|
||||
public static createTestObject(): Changes{
|
||||
public static createTestObject(): Changes {
|
||||
return new Changes({
|
||||
osmConnection: new OsmConnection(),
|
||||
featureSwitches:{
|
||||
featureSwitchIsTesting: new ImmutableStore(true)
|
||||
}
|
||||
featureSwitches: {
|
||||
featureSwitchIsTesting: new ImmutableStore(true),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -837,12 +840,16 @@ export class Changes {
|
|||
)
|
||||
|
||||
// We keep all the refused changes to try them again
|
||||
this.pendingChanges.setData(refusedChanges.flatMap((c) => c).filter(c => {
|
||||
if(c.id === null || c.id === undefined){
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}))
|
||||
this.pendingChanges.setData(
|
||||
refusedChanges
|
||||
.flatMap((c) => c)
|
||||
.filter((c) => {
|
||||
if (c.id === null || c.id === undefined) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those",
|
||||
|
|
|
|||
|
|
@ -205,12 +205,12 @@ export class ChangesetHandler {
|
|||
try {
|
||||
return await this.UploadWithNew(generateChangeXML, openChangeset, extraMetaTags)
|
||||
} catch (e) {
|
||||
const req = (<XMLHttpRequest>e)
|
||||
const req = <XMLHttpRequest>e
|
||||
if (req.status === 403) {
|
||||
// Someone got the banhammer
|
||||
// This is the message that OSM returned, will be something like "you have an important message, go to osm.org"
|
||||
const msg = req.responseText
|
||||
alert(msg+"\n\nWe'll take you to openstreetmap.org now")
|
||||
const msg = req.responseText
|
||||
alert(msg + "\n\nWe'll take you to openstreetmap.org now")
|
||||
window.location.replace(this.osmConnection.Backend())
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,14 +45,14 @@ export class OsmConnection {
|
|||
public userDetails: UIEventSource<UserDetails>
|
||||
public isLoggedIn: Store<boolean>
|
||||
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||
"unknown",
|
||||
"unknown"
|
||||
)
|
||||
public apiIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
|
||||
"unknown",
|
||||
"unknown"
|
||||
)
|
||||
|
||||
public loadingStatus = new UIEventSource<"not-attempted" | "loading" | "error" | "logged-in">(
|
||||
"not-attempted",
|
||||
"not-attempted"
|
||||
)
|
||||
public preferencesHandler: OsmPreferences
|
||||
public readonly _oauth_config: AuthConfig
|
||||
|
|
@ -96,7 +96,7 @@ export class OsmConnection {
|
|||
|
||||
this.userDetails = new UIEventSource<UserDetails>(
|
||||
new UserDetails(this._oauth_config.url),
|
||||
"userDetails",
|
||||
"userDetails"
|
||||
)
|
||||
if (options.fakeUser) {
|
||||
const ud = this.userDetails.data
|
||||
|
|
@ -117,7 +117,7 @@ export class OsmConnection {
|
|||
(user) =>
|
||||
user.loggedIn &&
|
||||
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
|
||||
[this.apiIsOnline],
|
||||
[this.apiIsOnline]
|
||||
)
|
||||
this.isLoggedIn.addCallback((isLoggedIn) => {
|
||||
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
|
||||
|
|
@ -160,17 +160,16 @@ export class OsmConnection {
|
|||
defaultValue: string = undefined,
|
||||
options?: {
|
||||
prefix?: string
|
||||
},
|
||||
}
|
||||
): UIEventSource<T | undefined> {
|
||||
const prefix = options?.prefix ?? "mapcomplete-"
|
||||
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
|
||||
|
||||
}
|
||||
|
||||
public getPreference<T extends string = string>(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
prefix: string = "mapcomplete-",
|
||||
prefix: string = "mapcomplete-"
|
||||
): UIEventSource<T | undefined> {
|
||||
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
|
||||
}
|
||||
|
|
@ -214,7 +213,7 @@ export class OsmConnection {
|
|||
this.updateAuthObject()
|
||||
|
||||
LocalStorageSource.get("location_before_login").setData(
|
||||
Utils.runningFromConsole ? undefined : window.location.href,
|
||||
Utils.runningFromConsole ? undefined : window.location.href
|
||||
)
|
||||
this.auth.xhr(
|
||||
{
|
||||
|
|
@ -252,13 +251,13 @@ export class OsmConnection {
|
|||
data.account_created = userInfo.getAttribute("account_created")
|
||||
data.uid = Number(userInfo.getAttribute("id"))
|
||||
data.languages = Array.from(
|
||||
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang"),
|
||||
userInfo.getElementsByTagName("languages")[0].getElementsByTagName("lang")
|
||||
).map((l) => l.textContent)
|
||||
data.csCount = Number.parseInt(
|
||||
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0",
|
||||
userInfo.getElementsByTagName("changesets")[0].getAttribute("count") ?? "0"
|
||||
)
|
||||
data.tracesCount = Number.parseInt(
|
||||
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0",
|
||||
userInfo.getElementsByTagName("traces")[0].getAttribute("count") ?? "0"
|
||||
)
|
||||
|
||||
data.img = undefined
|
||||
|
|
@ -290,7 +289,7 @@ export class OsmConnection {
|
|||
action(this.userDetails.data)
|
||||
}
|
||||
this._onLoggedIn = []
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -308,7 +307,7 @@ export class OsmConnection {
|
|||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
header?: Record<string, string>,
|
||||
content?: string,
|
||||
allowAnonymous: boolean = false,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
const connection: osmAuth = this.auth
|
||||
if (allowAnonymous && !this.auth.authenticated()) {
|
||||
|
|
@ -316,7 +315,7 @@ export class OsmConnection {
|
|||
`${this.Backend()}/api/0.6/${path}`,
|
||||
header,
|
||||
method,
|
||||
content,
|
||||
content
|
||||
)
|
||||
if (possibleResult["content"]) {
|
||||
return possibleResult["content"]
|
||||
|
|
@ -333,13 +332,13 @@ export class OsmConnection {
|
|||
content,
|
||||
path: `/api/0.6/${path}`,
|
||||
},
|
||||
function(err, response) {
|
||||
function (err, response) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok(response)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -348,7 +347,7 @@ export class OsmConnection {
|
|||
path: string,
|
||||
content?: string,
|
||||
header?: Record<string, string>,
|
||||
allowAnonymous: boolean = false,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<T> {
|
||||
return <T>await this.interact(path, "POST", header, content, allowAnonymous)
|
||||
}
|
||||
|
|
@ -356,7 +355,7 @@ export class OsmConnection {
|
|||
public async put<T extends string>(
|
||||
path: string,
|
||||
content?: string,
|
||||
header?: Record<string, string>,
|
||||
header?: Record<string, string>
|
||||
): Promise<T> {
|
||||
return <T>await this.interact(path, "PUT", header, content)
|
||||
}
|
||||
|
|
@ -364,7 +363,7 @@ export class OsmConnection {
|
|||
public async get(
|
||||
path: string,
|
||||
header?: Record<string, string>,
|
||||
allowAnonymous: boolean = false,
|
||||
allowAnonymous: boolean = false
|
||||
): Promise<string> {
|
||||
return await this.interact(path, "GET", header, undefined, allowAnonymous)
|
||||
}
|
||||
|
|
@ -403,7 +402,7 @@ export class OsmConnection {
|
|||
return new Promise<{ id: number }>((ok) => {
|
||||
window.setTimeout(
|
||||
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
||||
Math.random() * 5000,
|
||||
Math.random() * 5000
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -415,7 +414,7 @@ export class OsmConnection {
|
|||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
},
|
||||
true,
|
||||
true
|
||||
)
|
||||
const parsed = JSON.parse(response)
|
||||
console.log("Got result:", parsed)
|
||||
|
|
@ -438,14 +437,14 @@ export class OsmConnection {
|
|||
* Note: these are called 'tags' on the wiki, but I opted to name them 'labels' instead as they aren't "key=value" tags, but just words.
|
||||
*/
|
||||
labels: string[]
|
||||
},
|
||||
}
|
||||
): Promise<{ id: number }> {
|
||||
if (this._dryRun.data) {
|
||||
console.warn("Dryrun enabled - not actually uploading GPX ", gpx)
|
||||
return new Promise<{ id: number }>((ok) => {
|
||||
window.setTimeout(
|
||||
() => ok({ id: Math.floor(Math.random() * 1000) }),
|
||||
Math.random() * 5000,
|
||||
Math.random() * 5000
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -462,9 +461,9 @@ export class OsmConnection {
|
|||
}
|
||||
const extras = {
|
||||
file:
|
||||
"; filename=\"" +
|
||||
'; filename="' +
|
||||
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
|
||||
"\"\r\nContent-Type: application/gpx+xml",
|
||||
'"\r\nContent-Type: application/gpx+xml',
|
||||
}
|
||||
|
||||
const boundary = "987654"
|
||||
|
|
@ -472,7 +471,7 @@ export class OsmConnection {
|
|||
let body = ""
|
||||
for (const key in contents) {
|
||||
body += "--" + boundary + "\r\n"
|
||||
body += "Content-Disposition: form-data; name=\"" + key + "\""
|
||||
body += 'Content-Disposition: form-data; name="' + key + '"'
|
||||
if (extras[key] !== undefined) {
|
||||
body += extras[key]
|
||||
}
|
||||
|
|
@ -506,13 +505,13 @@ export class OsmConnection {
|
|||
|
||||
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
|
||||
},
|
||||
function(err) {
|
||||
function (err) {
|
||||
if (err !== null) {
|
||||
error(err)
|
||||
} else {
|
||||
ok()
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -521,7 +520,7 @@ export class OsmConnection {
|
|||
* To be called by land.html
|
||||
*/
|
||||
public finishLogin(callback: (previousURL: string) => void) {
|
||||
this.auth.authenticate(function() {
|
||||
this.auth.authenticate(function () {
|
||||
// Fully authed at this point
|
||||
console.log("Authentication successful!")
|
||||
const previousLocation = LocalStorageSource.get("location_before_login")
|
||||
|
|
@ -538,8 +537,8 @@ export class OsmConnection {
|
|||
? "https://mapcomplete.org/land.html"
|
||||
: window.location.protocol + "//" + window.location.host + "/land.html",
|
||||
/* We use 'singlePage' as much as possible, it is the most stable - including in PWA.
|
||||
* However, this breaks in iframes so we open a popup in that case
|
||||
*/
|
||||
* However, this breaks in iframes so we open a popup in that case
|
||||
*/
|
||||
singlepage: !this._iframeMode,
|
||||
auto: true,
|
||||
apiUrl: this._oauth_config.api_url ?? this._oauth_config.url,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import OSMAuthInstance = OSMAuth.osmAuth
|
|||
import { Utils } from "../../Utils"
|
||||
|
||||
export class OsmPreferences {
|
||||
|
||||
/**
|
||||
* A 'cache' of all the preference stores
|
||||
* @private
|
||||
|
|
@ -39,7 +38,6 @@ export class OsmPreferences {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
private setPreferencesAll(key: string, value: string) {
|
||||
if (this._allPreferences.data[key] !== value) {
|
||||
this._allPreferences.data[key] = value
|
||||
|
|
@ -54,11 +52,11 @@ export class OsmPreferences {
|
|||
}
|
||||
return this.preferences[key]
|
||||
}
|
||||
const pref = this.preferences[key] = new UIEventSource(value, "preference: " + key)
|
||||
const pref = (this.preferences[key] = new UIEventSource(value, "preference: " + key))
|
||||
if (value) {
|
||||
this.setPreferencesAll(key, value)
|
||||
}
|
||||
pref.addCallback(v => {
|
||||
pref.addCallback((v) => {
|
||||
console.log("Got an update:", key, "--->", v)
|
||||
this.uploadKvSplit(key, v)
|
||||
this.setPreferencesAll(key, v)
|
||||
|
|
@ -71,7 +69,7 @@ export class OsmPreferences {
|
|||
this.seenKeys = Object.keys(prefs)
|
||||
const legacy = OsmPreferences.getLegacyCombinedItems(prefs)
|
||||
const merged = OsmPreferences.mergeDict(prefs)
|
||||
if(Object.keys(legacy).length > 0){
|
||||
if (Object.keys(legacy).length > 0) {
|
||||
await this.removeLegacy(legacy)
|
||||
}
|
||||
for (const key in merged) {
|
||||
|
|
@ -82,12 +80,7 @@ export class OsmPreferences {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public getPreference(
|
||||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
prefix?: string,
|
||||
) {
|
||||
public getPreference(key: string, defaultValue: string = undefined, prefix?: string) {
|
||||
return this.getPreferenceSeedFromlocal(key, defaultValue, { prefix })
|
||||
}
|
||||
|
||||
|
|
@ -100,27 +93,29 @@ export class OsmPreferences {
|
|||
key: string,
|
||||
defaultValue: string = undefined,
|
||||
options?: {
|
||||
prefix?: string,
|
||||
prefix?: string
|
||||
saveToLocalStorage?: true | boolean
|
||||
},
|
||||
}
|
||||
): UIEventSource<string> {
|
||||
if (options?.prefix) {
|
||||
key = options.prefix + key
|
||||
}
|
||||
key = key.replace(/[:/"' {}.%\\]/g, "")
|
||||
|
||||
|
||||
const localStorage = LocalStorageSource.get(key) // cached
|
||||
if (localStorage.data === "null" || localStorage.data === "undefined") {
|
||||
localStorage.set(undefined)
|
||||
}
|
||||
const pref: UIEventSource<string> = this.initPreference(key, localStorage.data ?? defaultValue) // cached
|
||||
const pref: UIEventSource<string> = this.initPreference(
|
||||
key,
|
||||
localStorage.data ?? defaultValue
|
||||
) // cached
|
||||
if (this.localStorageInited.has(key)) {
|
||||
return pref
|
||||
}
|
||||
|
||||
if (options?.saveToLocalStorage ?? true) {
|
||||
pref.addCallback(v => localStorage.set(v)) // Keep a local copy
|
||||
pref.addCallback((v) => localStorage.set(v)) // Keep a local copy
|
||||
}
|
||||
this.localStorageInited.add(key)
|
||||
return pref
|
||||
|
|
@ -134,7 +129,7 @@ export class OsmPreferences {
|
|||
public async removeLegacy(legacyDict: Record<string, string>) {
|
||||
for (const k in legacyDict) {
|
||||
const v = legacyDict[k]
|
||||
console.log("Upgrading legacy preference",k )
|
||||
console.log("Upgrading legacy preference", k)
|
||||
await this.removeAllWithPrefix(k)
|
||||
this.osmConnection.getPreference(k).set(v)
|
||||
}
|
||||
|
|
@ -148,20 +143,19 @@ export class OsmPreferences {
|
|||
const newDict = {}
|
||||
|
||||
const allKeys: string[] = Object.keys(dict)
|
||||
const normalKeys = allKeys.filter(k => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/))
|
||||
const normalKeys = allKeys.filter((k) => !k.match(/[a-z-_0-9A-Z]*:[0-9]+/))
|
||||
for (const normalKey of normalKeys) {
|
||||
if (normalKey.match(/-combined-[0-9]*$/) || normalKey.match(/-combined-length$/)) {
|
||||
// Ignore legacy keys
|
||||
continue
|
||||
}
|
||||
const partKeys = OsmPreferences.keysStartingWith(allKeys, normalKey)
|
||||
const parts = partKeys.map(k => dict[k])
|
||||
const parts = partKeys.map((k) => dict[k])
|
||||
newDict[normalKey] = parts.join("")
|
||||
}
|
||||
return newDict
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets all items which have a 'combined'-string, the legacy long preferences
|
||||
*
|
||||
|
|
@ -180,7 +174,9 @@ export class OsmPreferences {
|
|||
public static getLegacyCombinedItems(dict: Record<string, string>): Record<string, string> {
|
||||
const merged: Record<string, string> = {}
|
||||
const keys = Object.keys(dict)
|
||||
const toCheck = Utils.NoNullInplace(Utils.Dedup(keys.map(k => k.match(/(.*)-combined-[0-9]+$/)?.[1])))
|
||||
const toCheck = Utils.NoNullInplace(
|
||||
Utils.Dedup(keys.map((k) => k.match(/(.*)-combined-[0-9]+$/)?.[1]))
|
||||
)
|
||||
for (const key of toCheck) {
|
||||
let i = 0
|
||||
let str = ""
|
||||
|
|
@ -195,7 +191,6 @@ export class OsmPreferences {
|
|||
return merged
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Bulk-downloads all preferences
|
||||
* @private
|
||||
|
|
@ -221,10 +216,9 @@ export class OsmPreferences {
|
|||
dict[k] = pref.getAttribute("v")
|
||||
}
|
||||
resolve(dict)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -238,7 +232,7 @@ export class OsmPreferences {
|
|||
*
|
||||
*/
|
||||
private static keysStartingWith(allKeys: string[], key: string): string[] {
|
||||
const keys = allKeys.filter(k => k === key || k.match(new RegExp(key + ":[0-9]+")))
|
||||
const keys = allKeys.filter((k) => k === key || k.match(new RegExp(key + ":[0-9]+")))
|
||||
keys.sort()
|
||||
return keys
|
||||
}
|
||||
|
|
@ -249,14 +243,12 @@ export class OsmPreferences {
|
|||
*
|
||||
*/
|
||||
private async uploadKvSplit(k: string, v: string) {
|
||||
|
||||
if (v === null || v === undefined || v === "" || v === "undefined" || v === "null") {
|
||||
const keysToDelete = OsmPreferences.keysStartingWith(this.seenKeys, k)
|
||||
await Promise.all(keysToDelete.map(k => this.deleteKeyDirectly(k)))
|
||||
await Promise.all(keysToDelete.map((k) => this.deleteKeyDirectly(k)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
await this.uploadKeyDirectly(k, v.slice(0, 255))
|
||||
v = v.slice(255)
|
||||
let i = 0
|
||||
|
|
@ -265,7 +257,6 @@ export class OsmPreferences {
|
|||
v = v.slice(255)
|
||||
i++
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -283,25 +274,23 @@ export class OsmPreferences {
|
|||
return
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
console.warn("Could not remove preference", error)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
console.debug("Preference ", k, "removed!")
|
||||
resolve()
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: "/api/0.6/user/preferences/" + encodeURIComponent(k),
|
||||
headers: { "Content-Type": "text/plain" },
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
console.warn("Could not remove preference", error)
|
||||
reject(error)
|
||||
return
|
||||
}
|
||||
console.debug("Preference ", k, "removed!")
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -328,7 +317,6 @@ export class OsmPreferences {
|
|||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
||||
this.auth.xhr(
|
||||
{
|
||||
method: "PUT",
|
||||
|
|
@ -343,7 +331,7 @@ export class OsmPreferences {
|
|||
return
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -351,12 +339,10 @@ export class OsmPreferences {
|
|||
async removeAllWithPrefix(prefix: string) {
|
||||
const keys = this.seenKeys
|
||||
for (const key of keys) {
|
||||
if(!key.startsWith(prefix)){
|
||||
if (!key.startsWith(prefix)) {
|
||||
continue
|
||||
}
|
||||
await this.deleteKeyDirectly(key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
import GeocodingProvider, { SearchResult, GeocodingOptions, GeocodeResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, {
|
||||
SearchResult,
|
||||
GeocodingOptions,
|
||||
GeocodeResult,
|
||||
} from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
|
||||
|
|
@ -8,7 +12,7 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
|
||||
constructor(...providers: ReadonlyArray<GeocodingProvider>) {
|
||||
this._providers = Utils.NoNull(providers)
|
||||
this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined)
|
||||
this._providersWithSuggest = this._providers.filter((pr) => pr.suggest !== undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -21,12 +25,10 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
const results: GeocodeResult[] = []
|
||||
const seenIds = new Set<string>()
|
||||
for (const geocodedElement of geocoded) {
|
||||
if(geocodedElement === undefined){
|
||||
if (geocodedElement === undefined) {
|
||||
continue
|
||||
}
|
||||
for (const entry of geocodedElement) {
|
||||
|
||||
|
||||
if (entry.osm_id === undefined) {
|
||||
throw "Invalid search result: a search result always must have an osm_id to be able to merge results from different sources"
|
||||
}
|
||||
|
|
@ -42,14 +44,13 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
|
||||
const results = await Promise.all(this._providers.map((pr) => pr.search(query, options)))
|
||||
return CombinedSearcher.merge(results)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return Stores.concat(
|
||||
this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||
.map(gcrss => CombinedSearcher.merge(gcrss))
|
||||
|
||||
this._providersWithSuggest.map((pr) => pr.suggest(query, options))
|
||||
).map((gcrss) => CombinedSearcher.merge(gcrss))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
/^(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/,
|
||||
/lon[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/,
|
||||
/lng[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?[ ,;&]+lat[:=]? *['"]?(-?[0-9]+\.[0-9]+)['"]?/,
|
||||
|
||||
]
|
||||
|
||||
/**
|
||||
|
|
@ -60,16 +59,18 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
* results[0] // => {lat: -57.5802905, lon: -12.7202538, "display_name": "lon: -12.720254, lat: -57.58029", "category": "coordinate","osm_id": "-12.720254/-57.58029", "source": "coordinate:latlon"}
|
||||
*/
|
||||
private directSearch(query: string): GeocodeResult[] {
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r)))
|
||||
.map(m => CoordinateSearch.asResult(m[2], m[1], "latlon") )
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map((r) => query.match(r))).map(
|
||||
(m) => CoordinateSearch.asResult(m[2], m[1], "latlon")
|
||||
)
|
||||
|
||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||
.map(m => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
|
||||
const matchesLonLat = Utils.NoNull(
|
||||
CoordinateSearch.lonLatRegexes.map((r) => query.match(r))
|
||||
).map((m) => CoordinateSearch.asResult(m[1], m[2], "lonlat"))
|
||||
return matches.concat(matchesLonLat)
|
||||
}
|
||||
|
||||
private static round6(n: number): string {
|
||||
return "" + (Math.round(n * 1000000) / 1000000)
|
||||
return "" + Math.round(n * 1000000) / 1000000
|
||||
}
|
||||
|
||||
private static asResult(lonIn: string, latIn: string, source: string): GeocodeResult {
|
||||
|
|
@ -82,7 +83,7 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
lon,
|
||||
display_name: "lon: " + lonStr + ", lat: " + latStr,
|
||||
category: "coordinate",
|
||||
source: "coordinate:"+source,
|
||||
source: "coordinate:" + source,
|
||||
osm_id: lonStr + "/" + latStr,
|
||||
}
|
||||
}
|
||||
|
|
@ -94,5 +95,4 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
async search(query: string): Promise<GeocodeResult[]> {
|
||||
return this.directSearch(query)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,20 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
|||
import LayerState from "../State/LayerState"
|
||||
import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
||||
|
||||
export type FilterSearchResult = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
|
||||
|
||||
export type FilterSearchResult = {
|
||||
option: FilterConfigOption
|
||||
filter: FilterConfig
|
||||
layer: LayerConfig
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches matching filters
|
||||
*/
|
||||
export default class FilterSearch {
|
||||
private readonly _state: {layerState: LayerState, theme: ThemeConfig}
|
||||
private readonly _state: { layerState: LayerState; theme: ThemeConfig }
|
||||
|
||||
constructor(state: {layerState: LayerState, theme: ThemeConfig}) {
|
||||
constructor(state: { layerState: LayerState; theme: ThemeConfig }) {
|
||||
this._state = state
|
||||
}
|
||||
|
||||
|
|
@ -23,12 +27,15 @@ export default class FilterSearch {
|
|||
if (query.length === 0) {
|
||||
return []
|
||||
}
|
||||
const queries = query.split(" ").map(query => {
|
||||
if (!Utils.isEmoji(query)) {
|
||||
return Utils.simplifyStringForSearch(query)
|
||||
}
|
||||
return query
|
||||
}).filter(q => q.length > 0)
|
||||
const queries = query
|
||||
.split(" ")
|
||||
.map((query) => {
|
||||
if (!Utils.isEmoji(query)) {
|
||||
return Utils.simplifyStringForSearch(query)
|
||||
}
|
||||
return query
|
||||
})
|
||||
.filter((q) => q.length > 0)
|
||||
const possibleFilters: FilterSearchResult[] = []
|
||||
for (const layer of this._state.theme.layers) {
|
||||
if (!Array.isArray(layer.filters)) {
|
||||
|
|
@ -46,29 +53,36 @@ export default class FilterSearch {
|
|||
if (!option.osmTags) {
|
||||
continue
|
||||
}
|
||||
if(option.fields.length > 0){
|
||||
if (option.fields.length > 0) {
|
||||
// Filters with a search field are not supported as of now, see #2141
|
||||
continue
|
||||
}
|
||||
let terms = ([option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])]
|
||||
.flatMap(term => [term, ...(term?.split(" ") ?? [])]))
|
||||
terms = terms.map(t => Utils.simplifyStringForSearch(t))
|
||||
let terms = [
|
||||
option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ??
|
||||
option.searchTerms?.["en"] ??
|
||||
[]),
|
||||
].flatMap((term) => [term, ...(term?.split(" ") ?? [])])
|
||||
terms = terms.map((t) => Utils.simplifyStringForSearch(t))
|
||||
terms.push(option.emoji)
|
||||
Utils.NoNullInplace(terms)
|
||||
const distances = queries.flatMap(query => terms.map(entry => {
|
||||
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
|
||||
const dRelative = d / query.length
|
||||
return dRelative
|
||||
}))
|
||||
const distances = queries.flatMap((query) =>
|
||||
terms.map((entry) => {
|
||||
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
|
||||
const dRelative = d / query.length
|
||||
return dRelative
|
||||
})
|
||||
)
|
||||
|
||||
const levehnsteinD = Math.min(...distances)
|
||||
if (levehnsteinD > 0.25) {
|
||||
continue
|
||||
}
|
||||
possibleFilters.push({
|
||||
option, layer, filter, index:
|
||||
i,
|
||||
option,
|
||||
layer,
|
||||
filter,
|
||||
index: i,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +99,7 @@ export default class FilterSearch {
|
|||
if (!Array.isArray(filteredLayer.layerDef.filters)) {
|
||||
continue
|
||||
}
|
||||
if (Constants.priviliged_layers.indexOf(<any> id) >= 0) {
|
||||
if (Constants.priviliged_layers.indexOf(<any>id) >= 0) {
|
||||
continue
|
||||
}
|
||||
for (const filter of filteredLayer.layerDef.filters) {
|
||||
|
|
@ -116,13 +130,16 @@ export default class FilterSearch {
|
|||
* Note that this depends on the language and the displayed text. For example, two filters {"en": "A", "nl": "B"} and {"en": "X", "nl": "B"} will be joined for dutch but not for English
|
||||
*
|
||||
*/
|
||||
static mergeSemiIdenticalLayers<T extends FilterSearchResult = FilterSearchResult>(filters: ReadonlyArray<T>, language: string):T[][] {
|
||||
const results : Record<string, T[]> = {}
|
||||
static mergeSemiIdenticalLayers<T extends FilterSearchResult = FilterSearchResult>(
|
||||
filters: ReadonlyArray<T>,
|
||||
language: string
|
||||
): T[][] {
|
||||
const results: Record<string, T[]> = {}
|
||||
for (const filter of filters) {
|
||||
const txt = filter.option.question.textFor(language)
|
||||
if(results[txt]){
|
||||
if (results[txt]) {
|
||||
results[txt].push(filter)
|
||||
}else{
|
||||
} else {
|
||||
results[txt] = [filter]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ export default class GeocodingFeatureSource implements FeatureSource {
|
|||
public features: Store<Feature<Geometry, Record<string, string>>[]>
|
||||
|
||||
constructor(provider: Store<SearchResult[]>) {
|
||||
this.features = provider.map(geocoded => {
|
||||
if(geocoded === undefined){
|
||||
this.features = provider.map((geocoded) => {
|
||||
if (geocoded === undefined) {
|
||||
return []
|
||||
}
|
||||
const features: Feature[] = []
|
||||
|
|
@ -28,18 +28,16 @@ export default class GeocodingFeatureSource implements FeatureSource {
|
|||
osm_id: gc.osm_type + "/" + gc.osm_id,
|
||||
osm_key: gc.feature?.properties?.osm_key,
|
||||
osm_value: gc.feature?.properties?.osm_value,
|
||||
source: gc.source
|
||||
source: gc.source,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [gc.lon, gc.lat]
|
||||
}
|
||||
coordinates: [gc.lon, gc.lat],
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return features
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
|||
import { GeoOperations } from "../GeoOperations"
|
||||
|
||||
export type GeocodingCategory =
|
||||
"coordinate"
|
||||
| "coordinate"
|
||||
| "city"
|
||||
| "house"
|
||||
| "street"
|
||||
|
|
@ -19,7 +19,7 @@ export type GeocodingCategory =
|
|||
| "airport"
|
||||
| "shop"
|
||||
|
||||
export type GeocodeResult = {
|
||||
export type GeocodeResult = {
|
||||
/**
|
||||
* The name of the feature being displayed
|
||||
*/
|
||||
|
|
@ -27,8 +27,8 @@ export type GeocodeResult = {
|
|||
/**
|
||||
* Some optional, extra information
|
||||
*/
|
||||
description?: string | Promise<string>,
|
||||
feature?: Feature,
|
||||
description?: string | Promise<string>
|
||||
feature?: Feature
|
||||
lat: number
|
||||
lon: number
|
||||
/**
|
||||
|
|
@ -37,22 +37,18 @@ export type GeocodeResult = {
|
|||
*/
|
||||
boundingbox?: number[]
|
||||
osm_type?: "node" | "way" | "relation"
|
||||
osm_id: string,
|
||||
category?: GeocodingCategory,
|
||||
payload?: object,
|
||||
osm_id: string
|
||||
category?: GeocodingCategory
|
||||
payload?: object
|
||||
source?: string
|
||||
}
|
||||
export type SearchResult =
|
||||
| GeocodeResult
|
||||
export type SearchResult = GeocodeResult
|
||||
|
||||
export interface GeocodingOptions {
|
||||
bbox?: BBox
|
||||
}
|
||||
|
||||
|
||||
export default interface GeocodingProvider {
|
||||
|
||||
|
||||
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
|
||||
|
||||
/**
|
||||
|
|
@ -62,26 +58,28 @@ export default interface GeocodingProvider {
|
|||
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]>
|
||||
}
|
||||
|
||||
export type ReverseGeocodingResult = Feature<Geometry, {
|
||||
osm_id: number,
|
||||
osm_type: "node" | "way" | "relation",
|
||||
country: string,
|
||||
city: string,
|
||||
countrycode: string,
|
||||
type: GeocodingCategory,
|
||||
street: string
|
||||
}>
|
||||
export type ReverseGeocodingResult = Feature<
|
||||
Geometry,
|
||||
{
|
||||
osm_id: number
|
||||
osm_type: "node" | "way" | "relation"
|
||||
country: string
|
||||
city: string
|
||||
countrycode: string
|
||||
type: GeocodingCategory
|
||||
street: string
|
||||
}
|
||||
>
|
||||
|
||||
export interface ReverseGeocodingProvider {
|
||||
reverseSearch(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number,
|
||||
language?: string,
|
||||
): Promise<ReverseGeocodingResult[]>;
|
||||
language?: string
|
||||
): Promise<ReverseGeocodingResult[]>
|
||||
}
|
||||
|
||||
export class GeocodingUtils {
|
||||
|
||||
public static searchLayer = GeocodingUtils.initSearchLayer()
|
||||
|
||||
private static initSearchLayer(): LayerConfig {
|
||||
|
|
@ -103,16 +101,14 @@ export class GeocodingUtils {
|
|||
train_station: 14,
|
||||
airport: 13,
|
||||
shop: 16,
|
||||
|
||||
}
|
||||
|
||||
public static mergeSimilarResults(results: GeocodeResult[]){
|
||||
public static mergeSimilarResults(results: GeocodeResult[]) {
|
||||
const byName: Record<string, GeocodeResult[]> = {}
|
||||
|
||||
|
||||
for (const result of results) {
|
||||
const nm = result.display_name
|
||||
if(!byName[nm]) {
|
||||
if (!byName[nm]) {
|
||||
byName[nm] = []
|
||||
}
|
||||
byName[nm].push(result)
|
||||
|
|
@ -123,11 +119,13 @@ export class GeocodingUtils {
|
|||
const options = byName[nm]
|
||||
const added = options[0]
|
||||
merged.push(added)
|
||||
const centers: [number,number][] = [[added.lon, added.lat]]
|
||||
const centers: [number, number][] = [[added.lon, added.lat]]
|
||||
for (const other of options) {
|
||||
const otherCenter:[number,number] = [other.lon, other.lat]
|
||||
const nearbyFound= centers.some(center => GeoOperations.distanceBetween(center, otherCenter) < 500)
|
||||
if(!nearbyFound){
|
||||
const otherCenter: [number, number] = [other.lon, other.lat]
|
||||
const nearbyFound = centers.some(
|
||||
(center) => GeoOperations.distanceBetween(center, otherCenter) < 500
|
||||
)
|
||||
if (!nearbyFound) {
|
||||
merged.push(other)
|
||||
centers.push(otherCenter)
|
||||
}
|
||||
|
|
@ -136,7 +134,6 @@ export class GeocodingUtils {
|
|||
return merged
|
||||
}
|
||||
|
||||
|
||||
public static categoryToIcon: Record<GeocodingCategory, DefaultPinIcon> = {
|
||||
city: "building_office_2",
|
||||
coordinate: "globe_alt",
|
||||
|
|
@ -148,8 +145,5 @@ export class GeocodingUtils {
|
|||
county: "building_office_2",
|
||||
airport: "airport",
|
||||
shop: "building_storefront",
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,22 +5,26 @@ import ThemeConfig from "../../Models/ThemeConfig/ThemeConfig"
|
|||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class LayerSearch {
|
||||
|
||||
private readonly _theme: ThemeConfig
|
||||
private readonly _layerWhitelist: Set<string>
|
||||
|
||||
constructor(theme: ThemeConfig) {
|
||||
this._theme = theme
|
||||
this._layerWhitelist = new Set(theme.layers
|
||||
.filter(l => l.isNormal())
|
||||
.map(l => l.id))
|
||||
this._layerWhitelist = new Set(theme.layers.filter((l) => l.isNormal()).map((l) => l.id))
|
||||
}
|
||||
|
||||
static scoreLayers(query: string, options: {
|
||||
whitelist?: Set<string>, blacklist?: Set<string>
|
||||
}): Record<string, number> {
|
||||
static scoreLayers(
|
||||
query: string,
|
||||
options: {
|
||||
whitelist?: Set<string>
|
||||
blacklist?: Set<string>
|
||||
}
|
||||
): Record<string, number> {
|
||||
const result: Record<string, number> = {}
|
||||
const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
|
||||
const queryParts = query
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((q) => Utils.simplifyStringForSearch(q))
|
||||
for (const id in ThemeSearch.officialThemes.layers) {
|
||||
if (options?.whitelist && !options?.whitelist.has(id)) {
|
||||
continue
|
||||
|
|
@ -29,18 +33,17 @@ export default class LayerSearch {
|
|||
continue
|
||||
}
|
||||
const keywords = ThemeSearch.officialThemes.layers[id]
|
||||
result[id] = Math.min(...queryParts.map(q => SearchUtils.scoreKeywords(q, keywords)))
|
||||
result[id] = Math.min(...queryParts.map((q) => SearchUtils.scoreKeywords(q, keywords)))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
public search(query: string, limit: number, scoreThreshold: number = 2): LayerConfig[] {
|
||||
if (query.length < 1) {
|
||||
return []
|
||||
}
|
||||
const scores = LayerSearch.scoreLayers(query, { whitelist: this._layerWhitelist })
|
||||
const asList: ({ layer: LayerConfig, score: number })[] = []
|
||||
const asList: { layer: LayerConfig; score: number }[] = []
|
||||
for (const layer in scores) {
|
||||
asList.push({
|
||||
layer: this._theme.getLayer(layer),
|
||||
|
|
@ -50,10 +53,8 @@ export default class LayerSearch {
|
|||
asList.sort((a, b) => a.score - b.score)
|
||||
|
||||
return asList
|
||||
.filter(sorted => sorted.score < scoreThreshold)
|
||||
.filter((sorted) => sorted.score < scoreThreshold)
|
||||
.slice(0, limit)
|
||||
.map(l => l.layer)
|
||||
.map((l) => l.layer)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ import { ImmutableStore, Store, Stores } from "../UIEventSource"
|
|||
import OpenStreetMapIdSearch from "./OpenStreetMapIdSearch"
|
||||
|
||||
type IntermediateResult = {
|
||||
feature: Feature,
|
||||
feature: Feature
|
||||
/**
|
||||
* Lon, lat
|
||||
*/
|
||||
center: [number, number],
|
||||
levehnsteinD: number,
|
||||
physicalDistance: number,
|
||||
searchTerms: string[],
|
||||
center: [number, number]
|
||||
levehnsteinD: number
|
||||
physicalDistance: number
|
||||
searchTerms: string[]
|
||||
description: string
|
||||
}
|
||||
export default class LocalElementSearch implements GeocodingProvider {
|
||||
|
|
@ -24,37 +24,50 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
constructor(state: ThemeViewState, limit: number) {
|
||||
this._state = state
|
||||
this._limit = limit
|
||||
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
return this.searchEntries(query, options, false).data
|
||||
}
|
||||
|
||||
private getPartialResult(query: string, candidateId: string | undefined, matchStart: boolean, centerpoint: [number, number], features: Feature[]): IntermediateResult[] {
|
||||
const results: IntermediateResult [] = []
|
||||
private getPartialResult(
|
||||
query: string,
|
||||
candidateId: string | undefined,
|
||||
matchStart: boolean,
|
||||
centerpoint: [number, number],
|
||||
features: Feature[]
|
||||
): IntermediateResult[] {
|
||||
const results: IntermediateResult[] = []
|
||||
|
||||
for (const feature of features) {
|
||||
const props = feature.properties
|
||||
const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name,
|
||||
(props["addr:street"] && props["addr:number"]) ?
|
||||
props["addr:street"] + props["addr:number"] : undefined])
|
||||
const searchTerms: string[] = Utils.NoNull([
|
||||
props.name,
|
||||
props.alt_name,
|
||||
props.local_name,
|
||||
props["addr:street"] && props["addr:number"]
|
||||
? props["addr:street"] + props["addr:number"]
|
||||
: undefined,
|
||||
])
|
||||
|
||||
let levehnsteinD: number
|
||||
if (candidateId === props.id) {
|
||||
levehnsteinD = 0
|
||||
} else {
|
||||
levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => {
|
||||
let simplified = Utils.simplifyStringForSearch(entry)
|
||||
if (matchStart) {
|
||||
simplified = simplified.slice(0, query.length)
|
||||
}
|
||||
return Utils.levenshteinDistance(query, simplified)
|
||||
}))
|
||||
levehnsteinD = Math.min(
|
||||
...searchTerms
|
||||
.flatMap((entry) => entry.split(/ /))
|
||||
.map((entry) => {
|
||||
let simplified = Utils.simplifyStringForSearch(entry)
|
||||
if (matchStart) {
|
||||
simplified = simplified.slice(0, query.length)
|
||||
}
|
||||
return Utils.levenshteinDistance(query, simplified)
|
||||
})
|
||||
)
|
||||
}
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
if ((levehnsteinD / query.length) <= 0.3) {
|
||||
|
||||
if (levehnsteinD / query.length <= 0.3) {
|
||||
let description = ""
|
||||
if (feature.properties["addr:street"]) {
|
||||
description += "" + feature.properties["addr:street"]
|
||||
|
|
@ -75,7 +88,11 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
return results
|
||||
}
|
||||
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> {
|
||||
searchEntries(
|
||||
query: string,
|
||||
options?: GeocodingOptions,
|
||||
matchStart?: boolean
|
||||
): Store<SearchResult[]> {
|
||||
if (query.length < 3) {
|
||||
return new ImmutableStore([])
|
||||
}
|
||||
|
|
@ -88,17 +105,26 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
const partials: Store<IntermediateResult[]>[] = []
|
||||
|
||||
for (const [_, geoIndexedStore] of properties) {
|
||||
const partialResult = geoIndexedStore.features.map(features => this.getPartialResult(query, candidateId, matchStart, centerPoint, features))
|
||||
const partialResult = geoIndexedStore.features.map((features) =>
|
||||
this.getPartialResult(query, candidateId, matchStart, centerPoint, features)
|
||||
)
|
||||
partials.push(partialResult)
|
||||
}
|
||||
|
||||
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map(l => l.flatMap(x => x))
|
||||
return listed.mapD(results => {
|
||||
results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25))
|
||||
const listed: Store<IntermediateResult[]> = Stores.concat(partials).map((l) =>
|
||||
l.flatMap((x) => x)
|
||||
)
|
||||
return listed.mapD((results) => {
|
||||
results.sort(
|
||||
(a, b) =>
|
||||
a.physicalDistance +
|
||||
a.levehnsteinD * 25 -
|
||||
(b.physicalDistance + b.levehnsteinD * 25)
|
||||
)
|
||||
if (this._limit) {
|
||||
results = results.slice(0, this._limit)
|
||||
}
|
||||
return results.map(entry => {
|
||||
return results.map((entry) => {
|
||||
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
|
||||
return <SearchResult>{
|
||||
lon: entry.center[0],
|
||||
|
|
@ -113,12 +139,9 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return this.searchEntries(query, options, true)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,24 @@ import Locale from "../../UI/i18n/Locale"
|
|||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
|
||||
export class NominatimGeocoding implements GeocodingProvider {
|
||||
|
||||
private readonly _host ;
|
||||
private readonly _host
|
||||
private readonly limit: number
|
||||
|
||||
constructor(limit: number = 3, host: string = Constants.nominatimEndpoint) {
|
||||
constructor(limit: number = 3, host: string = Constants.nominatimEndpoint) {
|
||||
this.limit = limit
|
||||
this._host = host
|
||||
}
|
||||
|
||||
public search(query: string, options?:GeocodingOptions): Promise<SearchResult[]> {
|
||||
public search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
const b = options?.bbox ?? BBox.global
|
||||
const url = `${
|
||||
this._host
|
||||
}search?format=json&limit=${this.limit}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
||||
const url = `${this._host}search?format=json&limit=${
|
||||
this.limit
|
||||
}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${
|
||||
Locale.language.data
|
||||
}&q=${query}`
|
||||
return Utils.downloadJson(url)
|
||||
}
|
||||
|
||||
|
||||
async reverseSearch(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number = 17,
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
|||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
||||
private static readonly regex = /((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
|
||||
private static readonly regex =
|
||||
/((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
|
||||
|
||||
private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = {
|
||||
"n": "node",
|
||||
"w": "way",
|
||||
"r": "relation",
|
||||
n: "node",
|
||||
w: "way",
|
||||
r: "relation",
|
||||
}
|
||||
|
||||
private readonly _state: SpecialVisualizationState
|
||||
|
|
@ -55,15 +56,17 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
category: "coordinate",
|
||||
osm_type: <"node" | "way" | "relation">osm_type,
|
||||
osm_id,
|
||||
lat: 0, lon: 0,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
source: "osmid",
|
||||
|
||||
}
|
||||
}
|
||||
const [lat, lon] = obj.centerpoint()
|
||||
return {
|
||||
lat, lon,
|
||||
display_name: obj.tags.name ?? obj.tags.alt_name ?? obj.tags.local_name ?? obj.tags.ref ?? id,
|
||||
lat,
|
||||
lon,
|
||||
display_name:
|
||||
obj.tags.name ?? obj.tags.alt_name ?? obj.tags.local_name ?? obj.tags.ref ?? id,
|
||||
description: osm_type,
|
||||
osm_type: <"node" | "way" | "relation">osm_type,
|
||||
osm_id,
|
||||
|
|
@ -72,14 +75,15 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||
|
||||
if (!isNaN(Number(query))) {
|
||||
const n = Number(query)
|
||||
return Utils.NoNullInplace(await Promise.all([
|
||||
this.getInfoAbout(`node/${n}`).catch(x => undefined),
|
||||
this.getInfoAbout(`way/${n}`).catch(x => undefined),
|
||||
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
|
||||
]))
|
||||
return Utils.NoNullInplace(
|
||||
await Promise.all([
|
||||
this.getInfoAbout(`node/${n}`).catch((x) => undefined),
|
||||
this.getInfoAbout(`way/${n}`).catch((x) => undefined),
|
||||
this.getInfoAbout(`relation/${n}`).catch(() => undefined),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
const id = OpenStreetMapIdSearch.extractId(query)
|
||||
|
|
@ -92,5 +96,4 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
suggest?(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
||||
return UIEventSource.FromPromise(this.search(query, options))
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import Constants from "../../Models/Constants"
|
|||
import GeocodingProvider, {
|
||||
GeocodeResult,
|
||||
GeocodingCategory,
|
||||
GeocodingOptions, GeocodingUtils,
|
||||
GeocodingOptions,
|
||||
GeocodingUtils,
|
||||
ReverseGeocodingProvider,
|
||||
ReverseGeocodingResult,
|
||||
} from "./GeocodingProvider"
|
||||
|
|
@ -16,31 +17,35 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
private _endpoint: string
|
||||
private supportedLanguages = ["en", "de", "fr"]
|
||||
private static readonly types = {
|
||||
"R": "relation",
|
||||
"W": "way",
|
||||
"N": "node",
|
||||
R: "relation",
|
||||
W: "way",
|
||||
N: "node",
|
||||
}
|
||||
private readonly suggestionLimit: number = 5
|
||||
private readonly searchLimit: number = 1
|
||||
|
||||
|
||||
constructor(suggestionLimit:number = 5, searchLimit:number = 1, endpoint?: string) {
|
||||
constructor(suggestionLimit: number = 5, searchLimit: number = 1, endpoint?: string) {
|
||||
this.suggestionLimit = suggestionLimit
|
||||
this.searchLimit = searchLimit
|
||||
this._endpoint = endpoint ?? Constants.photonEndpoint ?? "https://photon.komoot.io/"
|
||||
}
|
||||
|
||||
async reverseSearch(coordinate: {
|
||||
lon: number;
|
||||
lat: number
|
||||
}, zoom: number, language?: string): Promise<ReverseGeocodingResult[]> {
|
||||
const url = `${this._endpoint}/reverse?lon=${coordinate.lon}&lat=${coordinate.lat}&${this.getLanguage(language)}`
|
||||
async reverseSearch(
|
||||
coordinate: {
|
||||
lon: number
|
||||
lat: number
|
||||
},
|
||||
zoom: number,
|
||||
language?: string
|
||||
): Promise<ReverseGeocodingResult[]> {
|
||||
const url = `${this._endpoint}/reverse?lon=${coordinate.lon}&lat=${
|
||||
coordinate.lat
|
||||
}&${this.getLanguage(language)}`
|
||||
const result = await Utils.downloadJsonCached<FeatureCollection>(url, 1000 * 60 * 60)
|
||||
for (const f of result.features) {
|
||||
f.properties.osm_type = PhotonSearch.types[f.properties.osm_type]
|
||||
}
|
||||
return <ReverseGeocodingResult[]>result.features
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -49,13 +54,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
* @private
|
||||
*/
|
||||
private getLanguage(language?: string): string {
|
||||
|
||||
language ??= Locale.language.data
|
||||
if (this.supportedLanguages.indexOf(language) < 0) {
|
||||
return ""
|
||||
}
|
||||
return `&lang=${language}`
|
||||
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
||||
|
|
@ -75,7 +78,6 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
|
||||
switch (type) {
|
||||
case "house": {
|
||||
|
||||
const addr = ifdef("", p.street) + ifdef(" ", p.housenumber)
|
||||
if (!addr) {
|
||||
return p.city
|
||||
|
|
@ -94,7 +96,6 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
case "country":
|
||||
return undefined
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private getCategory(entry: Feature) {
|
||||
|
|
@ -111,7 +112,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
return p.type
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions, limit?: number): Promise<GeocodeResult[]> {
|
||||
async search(
|
||||
query: string,
|
||||
options?: GeocodingOptions,
|
||||
limit?: number
|
||||
): Promise<GeocodeResult[]> {
|
||||
if (query.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -121,9 +126,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
const [lon, lat] = options.bbox.center()
|
||||
bbox = `&lon=${lon}&lat=${lat}`
|
||||
}
|
||||
const url = `${this._endpoint}/api/?q=${encodeURIComponent(query)}&limit=${limit}${this.getLanguage()}${bbox}`
|
||||
const url = `${this._endpoint}/api/?q=${encodeURIComponent(
|
||||
query
|
||||
)}&limit=${limit}${this.getLanguage()}${bbox}`
|
||||
const results = await Utils.downloadJsonCached<FeatureCollection>(url, 1000 * 60 * 60)
|
||||
const encoded= results.features.map(f => {
|
||||
const encoded = results.features.map((f) => {
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(f)
|
||||
let boundingbox: number[] = undefined
|
||||
if (f.properties.extent) {
|
||||
|
|
@ -138,11 +145,11 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
osm_type: PhotonSearch.types[f.properties.osm_type],
|
||||
category: this.getCategory(f),
|
||||
boundingbox,
|
||||
lon, lat,
|
||||
lon,
|
||||
lat,
|
||||
source: this._endpoint,
|
||||
}
|
||||
})
|
||||
return GeocodingUtils.mergeSimilarResults(encoded)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,11 @@ import { Utils } from "../../Utils"
|
|||
import ThemeSearch from "./ThemeSearch"
|
||||
|
||||
export default class SearchUtils {
|
||||
|
||||
|
||||
/** Applies special search terms, such as 'studio', 'osmcha', ...
|
||||
* Returns 'false' if nothing is matched.
|
||||
* Doesn't return control flow if a match is found (navigates to another page in this case)
|
||||
*/
|
||||
public static applySpecialSearch(searchTerm: string, ) {
|
||||
public static applySpecialSearch(searchTerm: string) {
|
||||
searchTerm = searchTerm.toLowerCase()
|
||||
if (!searchTerm) {
|
||||
return false
|
||||
|
|
@ -33,10 +31,8 @@ export default class SearchUtils {
|
|||
window.location.href = "./studio.html"
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Searches for the smallest distance in words; will split both the query and the terms
|
||||
*
|
||||
|
|
@ -44,19 +40,26 @@ export default class SearchUtils {
|
|||
* SearchUtils.scoreKeywords("waste", {"en": ["A layer with drinking water points"]}, "en") // => 2
|
||||
*
|
||||
*/
|
||||
public static scoreKeywords(query: string, keywords: Record<string, string[]> | string[], language?: string): number {
|
||||
if(!keywords){
|
||||
public static scoreKeywords(
|
||||
query: string,
|
||||
keywords: Record<string, string[]> | string[],
|
||||
language?: string
|
||||
): number {
|
||||
if (!keywords) {
|
||||
return Infinity
|
||||
}
|
||||
language ??= Locale.language.data
|
||||
const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
|
||||
const queryParts = query
|
||||
.trim()
|
||||
.split(" ")
|
||||
.map((q) => Utils.simplifyStringForSearch(q))
|
||||
let terms: string[]
|
||||
if (Array.isArray(keywords)) {
|
||||
terms = keywords
|
||||
} else {
|
||||
terms = (keywords[language] ?? []).concat(keywords["*"])
|
||||
}
|
||||
const termsAll = Utils.NoNullInplace(terms).flatMap(t => t.split(" "))
|
||||
const termsAll = Utils.NoNullInplace(terms).flatMap((t) => t.split(" "))
|
||||
|
||||
let distanceSummed = 0
|
||||
for (let i = 0; i < queryParts.length; i++) {
|
||||
|
|
|
|||
|
|
@ -8,57 +8,59 @@ import LayerSearch from "./LayerSearch"
|
|||
import SearchUtils from "./SearchUtils"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
|
||||
|
||||
type ThemeSearchScore = {
|
||||
theme: MinimalThemeInformation,
|
||||
lowest: number,
|
||||
perLayer?: Record<string, number>,
|
||||
other: number,
|
||||
theme: MinimalThemeInformation
|
||||
lowest: number
|
||||
perLayer?: Record<string, number>
|
||||
other: number
|
||||
}
|
||||
|
||||
|
||||
export default class ThemeSearch {
|
||||
|
||||
public static readonly officialThemes: {
|
||||
themes: MinimalThemeInformation[],
|
||||
themes: MinimalThemeInformation[]
|
||||
layers: Record<string, Record<string, string[]>>
|
||||
} = <any> themeOverview
|
||||
public static readonly officialThemesById: Map<string, MinimalThemeInformation> = new Map<string, MinimalThemeInformation>()
|
||||
} = <any>themeOverview
|
||||
public static readonly officialThemesById: Map<string, MinimalThemeInformation> = new Map<
|
||||
string,
|
||||
MinimalThemeInformation
|
||||
>()
|
||||
static {
|
||||
for (const th of ThemeSearch.officialThemes.themes ?? []) {
|
||||
ThemeSearch.officialThemesById.set(th.id, th)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private readonly _knownHiddenThemes: Store<Set<string>>
|
||||
private readonly _layersToIgnore: string[]
|
||||
private readonly _otherThemes: MinimalThemeInformation[]
|
||||
|
||||
constructor(state: {osmConnection: OsmConnection, theme: ThemeConfig}) {
|
||||
this._layersToIgnore = state.theme.layers.filter(l => l.isNormal()).map(l => l.id)
|
||||
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map(list => new Set(list))
|
||||
this._otherThemes = ThemeSearch.officialThemes.themes
|
||||
.filter(th => th.id !== state.theme.id)
|
||||
constructor(state: { osmConnection: OsmConnection; theme: ThemeConfig }) {
|
||||
this._layersToIgnore = state.theme.layers.filter((l) => l.isNormal()).map((l) => l.id)
|
||||
this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(
|
||||
state.osmConnection
|
||||
).map((list) => new Set(list))
|
||||
this._otherThemes = ThemeSearch.officialThemes.themes.filter(
|
||||
(th) => th.id !== state.theme.id
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
public search(query: string, limit: number, threshold: number = 3): MinimalThemeInformation[] {
|
||||
if (query.length < 1) {
|
||||
return []
|
||||
}
|
||||
const sorted = ThemeSearch.sortedByLowestScores(query, this._otherThemes, this._layersToIgnore)
|
||||
const sorted = ThemeSearch.sortedByLowestScores(
|
||||
query,
|
||||
this._otherThemes,
|
||||
this._layersToIgnore
|
||||
)
|
||||
return sorted
|
||||
.filter(sorted => sorted.lowest < threshold)
|
||||
.map(th => th.theme)
|
||||
.filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
|
||||
.filter((sorted) => sorted.lowest < threshold)
|
||||
.map((th) => th.theme)
|
||||
.filter((th) => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
public static createUrlFor(
|
||||
layout: { id: string },
|
||||
state?: { layoutToUse?: { id } },
|
||||
): string {
|
||||
public static createUrlFor(layout: { id: string }, state?: { layoutToUse?: { id } }): string {
|
||||
if (layout === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -88,7 +90,6 @@ export default class ThemeSearch {
|
|||
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
|
||||
}
|
||||
|
||||
|
||||
return `${linkPrefix}`
|
||||
}
|
||||
|
||||
|
|
@ -101,17 +102,21 @@ export default class ThemeSearch {
|
|||
* @param ignoreLayers
|
||||
* @private
|
||||
*/
|
||||
private static scoreThemes(query: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = undefined): Record<string, ThemeSearchScore> {
|
||||
private static scoreThemes(
|
||||
query: string,
|
||||
themes: MinimalThemeInformation[],
|
||||
ignoreLayers: string[] = undefined
|
||||
): Record<string, ThemeSearchScore> {
|
||||
if (query?.length < 1) {
|
||||
return undefined
|
||||
}
|
||||
themes = Utils.NoNullInplace(themes)
|
||||
|
||||
let options : {blacklist: Set<string>} = undefined
|
||||
if(ignoreLayers?.length > 0){
|
||||
let options: { blacklist: Set<string> } = undefined
|
||||
if (ignoreLayers?.length > 0) {
|
||||
options = { blacklist: new Set(ignoreLayers) }
|
||||
}
|
||||
const layerScores = query.length < 3 ? {} : LayerSearch.scoreLayers(query, options)
|
||||
const layerScores = query.length < 3 ? {} : LayerSearch.scoreLayers(query, options)
|
||||
const results: Record<string, ThemeSearchScore> = {}
|
||||
for (const layoutInfo of themes) {
|
||||
const theme = layoutInfo.id
|
||||
|
|
@ -122,20 +127,22 @@ export default class ThemeSearch {
|
|||
results[theme] = {
|
||||
theme: layoutInfo,
|
||||
lowest: -1,
|
||||
other: 0
|
||||
other: 0,
|
||||
}
|
||||
continue
|
||||
}
|
||||
const perLayer = Utils.asRecord(
|
||||
layoutInfo.layers ?? [], layer => layerScores[layer],
|
||||
)
|
||||
const perLayer = Utils.asRecord(layoutInfo.layers ?? [], (layer) => layerScores[layer])
|
||||
const language = Locale.language.data
|
||||
|
||||
const keywords = Utils.NoNullInplace([layoutInfo.shortDescription, layoutInfo.title])
|
||||
.map(item => typeof item === "string" ? item : (item[language] ?? item["*"]))
|
||||
const keywords = Utils.NoNullInplace([
|
||||
layoutInfo.shortDescription,
|
||||
layoutInfo.title,
|
||||
]).map((item) => (typeof item === "string" ? item : item[language] ?? item["*"]))
|
||||
|
||||
|
||||
const other = Math.min(SearchUtils.scoreKeywords(query, keywords), SearchUtils.scoreKeywords(query, layoutInfo.keywords))
|
||||
const other = Math.min(
|
||||
SearchUtils.scoreKeywords(query, keywords),
|
||||
SearchUtils.scoreKeywords(query, layoutInfo.keywords)
|
||||
)
|
||||
const lowest = Math.min(other, ...Object.values(perLayer))
|
||||
results[theme] = {
|
||||
theme: layoutInfo,
|
||||
|
|
@ -147,15 +154,21 @@ export default class ThemeSearch {
|
|||
return results
|
||||
}
|
||||
|
||||
public static sortedByLowestScores(search: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = []): ThemeSearchScore[] {
|
||||
public static sortedByLowestScores(
|
||||
search: string,
|
||||
themes: MinimalThemeInformation[],
|
||||
ignoreLayers: string[] = []
|
||||
): ThemeSearchScore[] {
|
||||
const scored = Object.values(this.scoreThemes(search, themes, ignoreLayers))
|
||||
scored.sort((a, b) => a.lowest - b.lowest)
|
||||
return scored
|
||||
}
|
||||
|
||||
public static sortedByLowest(search: string, themes: MinimalThemeInformation[], ignoreLayers: string[] = []): MinimalThemeInformation[] {
|
||||
return this.sortedByLowestScores(search, themes, ignoreLayers)
|
||||
.map(th => th.theme)
|
||||
public static sortedByLowest(
|
||||
search: string,
|
||||
themes: MinimalThemeInformation[],
|
||||
ignoreLayers: string[] = []
|
||||
): MinimalThemeInformation[] {
|
||||
return this.sortedByLowestScores(search, themes, ignoreLayers).map((th) => th.theme)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ export class OsmConnectionFeatureSwitches {
|
|||
}
|
||||
|
||||
export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
|
||||
|
||||
public readonly featureSwitchEnableLogin: UIEventSource<boolean>
|
||||
public readonly featureSwitchSearch: UIEventSource<boolean>
|
||||
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
|||
import Constants from "../../Models/Constants"
|
||||
|
||||
export type ActiveFilter = {
|
||||
layer: LayerConfig,
|
||||
filter: FilterConfig,
|
||||
layer: LayerConfig
|
||||
filter: FilterConfig
|
||||
control: UIEventSource<string | number | undefined>
|
||||
}
|
||||
/**
|
||||
|
|
@ -36,9 +36,13 @@ export default class LayerState {
|
|||
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
|
||||
|
||||
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
|
||||
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
|
||||
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<
|
||||
FilteredLayer[]
|
||||
>(undefined)
|
||||
public readonly activeLayers: Store<FilteredLayer[]> = this._activeLayers
|
||||
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
|
||||
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<
|
||||
FilteredLayer[]
|
||||
>(undefined)
|
||||
public readonly nonactiveLayers: Store<FilteredLayer[]> = this._nonactiveLayers
|
||||
private readonly osmConnection: OsmConnection
|
||||
|
||||
|
|
@ -71,7 +75,7 @@ export default class LayerState {
|
|||
this.filteredLayers = filteredLayers
|
||||
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
|
||||
|
||||
this.filteredLayers.forEach(fl => {
|
||||
this.filteredLayers.forEach((fl) => {
|
||||
fl.isDisplayed.addCallback(() => this.updateActiveFilters())
|
||||
for (const [_, appliedFilter] of fl.appliedFilters) {
|
||||
appliedFilter.addCallback(() => this.updateActiveFilters())
|
||||
|
|
@ -80,27 +84,27 @@ export default class LayerState {
|
|||
this.updateActiveFilters()
|
||||
}
|
||||
|
||||
private updateActiveFilters(){
|
||||
private updateActiveFilters() {
|
||||
const filters: ActiveFilter[] = []
|
||||
const activeLayers: FilteredLayer[] = []
|
||||
const nonactiveLayers: FilteredLayer[] = []
|
||||
this.filteredLayers.forEach(fl => {
|
||||
if(!fl.isDisplayed.data){
|
||||
const nonactiveLayers: FilteredLayer[] = []
|
||||
this.filteredLayers.forEach((fl) => {
|
||||
if (!fl.isDisplayed.data) {
|
||||
nonactiveLayers.push(fl)
|
||||
return
|
||||
}
|
||||
activeLayers.push(fl)
|
||||
|
||||
if(fl.layerDef.filterIsSameAs){
|
||||
if (fl.layerDef.filterIsSameAs) {
|
||||
return
|
||||
}
|
||||
for (const [filtername, appliedFilter] of fl.appliedFilters) {
|
||||
if (appliedFilter.data === undefined) {
|
||||
continue
|
||||
}
|
||||
const filter = fl.layerDef.filters.find(f => f.id === filtername)
|
||||
if(typeof appliedFilter.data === "number"){
|
||||
if(filter.options[appliedFilter.data].osmTags === undefined){
|
||||
const filter = fl.layerDef.filters.find((f) => f.id === filtername)
|
||||
if (typeof appliedFilter.data === "number") {
|
||||
if (filter.options[appliedFilter.data].osmTags === undefined) {
|
||||
// This is probably the first, generic option which doesn't _actually_ filter
|
||||
continue
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { FeatureSource } from "../FeatureSource/FeatureSource"
|
|||
import { Feature } from "geojson"
|
||||
|
||||
export default class SearchState {
|
||||
|
||||
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
|
||||
public readonly searchIsFocused = new UIEventSource(false)
|
||||
|
|
@ -39,57 +38,66 @@ export default class SearchState {
|
|||
new LocalElementSearch(state, 5),
|
||||
new CoordinateSearch(),
|
||||
new OpenStreetMapIdSearch(state),
|
||||
new PhotonSearch() // new NominatimGeocoding(),
|
||||
new PhotonSearch(), // new NominatimGeocoding(),
|
||||
]
|
||||
|
||||
const bounds = state.mapProperties.bounds
|
||||
const suggestionsList = this.searchTerm.stabilized(250).mapD(search => {
|
||||
const suggestionsList = this.searchTerm.stabilized(250).mapD(
|
||||
(search) => {
|
||||
if (search.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data }))
|
||||
|
||||
}, [bounds]
|
||||
return this.locationSearchers.map((ls) => ls.suggest(search, { bbox: bounds.data }))
|
||||
},
|
||||
[bounds]
|
||||
)
|
||||
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
|
||||
this.suggestionsSearchRunning = suggestionsList.bind((suggestions) => {
|
||||
if (suggestions === undefined) {
|
||||
return new ImmutableStore(true)
|
||||
}
|
||||
return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined))
|
||||
return Stores.concat(suggestions).map((suggestions) =>
|
||||
suggestions.some((list) => list === undefined)
|
||||
)
|
||||
})
|
||||
this.suggestions = suggestionsList.bindD(suggestions =>
|
||||
Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions))
|
||||
this.suggestions = suggestionsList.bindD((suggestions) =>
|
||||
Stores.concat(suggestions).map((suggestions) => CombinedSearcher.merge(suggestions))
|
||||
)
|
||||
|
||||
const themeSearch = new ThemeSearch(state)
|
||||
this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.search(query, 3))
|
||||
this.themeSuggestions = this.searchTerm.mapD((query) => themeSearch.search(query, 3))
|
||||
|
||||
const layerSearch = new LayerSearch(state.theme)
|
||||
this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.search(query, 5))
|
||||
this.layerSuggestions = this.searchTerm.mapD((query) => layerSearch.search(query, 5))
|
||||
|
||||
const filterSearch = new FilterSearch(state)
|
||||
this.filterSuggestions = this.searchTerm.stabilized(50)
|
||||
.mapD(query => filterSearch.search(query))
|
||||
.mapD(filterResult => {
|
||||
const active = state.layerState.activeFilters.data
|
||||
return filterResult.filter(({ filter, index, layer }) => {
|
||||
const foundMatch = active.some(active =>
|
||||
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
|
||||
this.filterSuggestions = this.searchTerm
|
||||
.stabilized(50)
|
||||
.mapD((query) => filterSearch.search(query))
|
||||
.mapD(
|
||||
(filterResult) => {
|
||||
const active = state.layerState.activeFilters.data
|
||||
return filterResult.filter(({ filter, index, layer }) => {
|
||||
const foundMatch = active.some(
|
||||
(active) =>
|
||||
active.filter.id === filter.id &&
|
||||
layer.id === active.layer.id &&
|
||||
active.control.data === index
|
||||
)
|
||||
|
||||
return !foundMatch
|
||||
})
|
||||
}, [state.layerState.activeFilters])
|
||||
return !foundMatch
|
||||
})
|
||||
},
|
||||
[state.layerState.activeFilters]
|
||||
)
|
||||
this.locationResults = new GeocodingFeatureSource(this.suggestions.stabilized(250))
|
||||
|
||||
this.showSearchDrawer = new UIEventSource(false)
|
||||
|
||||
this.searchIsFocused.addCallbackAndRunD(sugg => {
|
||||
this.searchIsFocused.addCallbackAndRunD((sugg) => {
|
||||
if (sugg) {
|
||||
this.showSearchDrawer.set(true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async apply(result: FilterSearchResult[] | LayerConfig) {
|
||||
|
|
@ -108,7 +116,7 @@ export default class SearchState {
|
|||
private async applyFilter(payload: FilterSearchResult[]) {
|
||||
const state = this.state
|
||||
|
||||
const layersToShow = payload.map(fsr => fsr.layer.id)
|
||||
const layersToShow = payload.map((fsr) => fsr.layer.id)
|
||||
console.log("Layers to show are", layersToShow)
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
const layer = otherLayer.layerDef
|
||||
|
|
@ -144,7 +152,7 @@ export default class SearchState {
|
|||
}
|
||||
// This feature might not be loaded because we zoomed out
|
||||
const object = await this.state.osmObjectDownloader.DownloadObjectAsync(osmid)
|
||||
if(object === "deleted"){
|
||||
if (object === "deleted") {
|
||||
return
|
||||
}
|
||||
const f = object.asGeoJson()
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ import Showdown from "showdown"
|
|||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeocodeResult } from "../Search/GeocodingProvider"
|
||||
|
||||
|
||||
export class OptionallySyncedHistory<T> {
|
||||
|
||||
public readonly syncPreference: UIEventSource<"sync" | "local" | "no">
|
||||
public readonly value: Store<T[]>
|
||||
private readonly synced: UIEventSource<T[]>
|
||||
|
|
@ -34,18 +32,26 @@ export class OptionallySyncedHistory<T> {
|
|||
private readonly _isSame: (a: T, b: T) => boolean
|
||||
private osmconnection: OsmConnection
|
||||
|
||||
constructor(key: string, osmconnection: OsmConnection, maxHistory: number = 20, isSame?: (a: T, b: T) => boolean) {
|
||||
constructor(
|
||||
key: string,
|
||||
osmconnection: OsmConnection,
|
||||
maxHistory: number = 20,
|
||||
isSame?: (a: T, b: T) => boolean
|
||||
) {
|
||||
this.osmconnection = osmconnection
|
||||
this._maxHistory = maxHistory
|
||||
this._isSame = isSame
|
||||
this.syncPreference = osmconnection.getPreference(
|
||||
"preference-" + key + "-history",
|
||||
"sync",
|
||||
)
|
||||
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.getPreference(key + "-history"), [])
|
||||
const local = this.local = LocalStorageSource.getParsed<T[]>(key + "-history", [])
|
||||
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:" + key + "(session only)")
|
||||
this.syncPreference.addCallback(syncmode => {
|
||||
this.syncPreference = osmconnection.getPreference("preference-" + key + "-history", "sync")
|
||||
const synced = (this.synced = UIEventSource.asObject<T[]>(
|
||||
osmconnection.getPreference(key + "-history"),
|
||||
[]
|
||||
))
|
||||
const local = (this.local = LocalStorageSource.getParsed<T[]>(key + "-history", []))
|
||||
const thisSession = (this.thisSession = new UIEventSource<T[]>(
|
||||
[],
|
||||
"optionally-synced:" + key + "(session only)"
|
||||
))
|
||||
this.syncPreference.addCallback((syncmode) => {
|
||||
if (syncmode === "sync") {
|
||||
let list = [...thisSession.data, ...synced.data].slice(0, maxHistory)
|
||||
if (this._isSame) {
|
||||
|
|
@ -67,9 +73,7 @@ export class OptionallySyncedHistory<T> {
|
|||
}
|
||||
})
|
||||
|
||||
this.value = this.syncPreference.bind(syncPref => this.getAppropriateStore(syncPref))
|
||||
|
||||
|
||||
this.value = this.syncPreference.bind((syncPref) => this.getAppropriateStore(syncPref))
|
||||
}
|
||||
|
||||
private getAppropriateStore(syncPref?: string) {
|
||||
|
|
@ -87,7 +91,7 @@ export class OptionallySyncedHistory<T> {
|
|||
const store = this.getAppropriateStore()
|
||||
let oldList = store.data ?? []
|
||||
if (this._isSame) {
|
||||
oldList = oldList.filter(x => !this._isSame(t, x))
|
||||
oldList = oldList.filter((x) => !this._isSame(t, x))
|
||||
}
|
||||
store.set([t, ...oldList].slice(0, this._maxHistory))
|
||||
}
|
||||
|
|
@ -100,14 +104,13 @@ export class OptionallySyncedHistory<T> {
|
|||
if (t === undefined) {
|
||||
return
|
||||
}
|
||||
this.osmconnection.isLoggedIn.addCallbackAndRun(loggedIn => {
|
||||
this.osmconnection.isLoggedIn.addCallbackAndRun((loggedIn) => {
|
||||
if (!loggedIn) {
|
||||
return
|
||||
}
|
||||
this.add(t)
|
||||
return true
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
|
@ -157,7 +160,7 @@ export default class UserRelatedState {
|
|||
*/
|
||||
public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
|
||||
7 * 24 * 60 * 60,
|
||||
"gps_location_retention",
|
||||
"gps_location_retention"
|
||||
)
|
||||
|
||||
public readonly addNewFeatureMode = new UIEventSource<
|
||||
|
|
@ -180,18 +183,17 @@ export default class UserRelatedState {
|
|||
public readonly recentlyVisitedThemes: OptionallySyncedHistory<string>
|
||||
public readonly recentlyVisitedSearch: OptionallySyncedHistory<GeocodeResult>
|
||||
|
||||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
layout?: ThemeConfig,
|
||||
featureSwitches?: FeatureSwitchState,
|
||||
mapProperties?: MapProperties,
|
||||
mapProperties?: MapProperties
|
||||
) {
|
||||
this.osmConnection = osmConnection
|
||||
this._mapProperties = mapProperties
|
||||
|
||||
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
|
||||
this.osmConnection.getPreference("show-all-questions", "false"),
|
||||
this.osmConnection.getPreference("show-all-questions", "false")
|
||||
)
|
||||
this.language = this.osmConnection.getPreference("language")
|
||||
this.showTags = this.osmConnection.getPreference("show_tags")
|
||||
|
|
@ -203,15 +205,19 @@ export default class UserRelatedState {
|
|||
|
||||
this.mangroveIdentity = new MangroveIdentity(
|
||||
this.osmConnection.getPreference("identity", undefined, "mangrove"),
|
||||
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove"),
|
||||
this.osmConnection.getPreference("identity-creation-date", undefined, "mangrove")
|
||||
)
|
||||
this.preferredBackgroundLayer = this.osmConnection.getPreference(
|
||||
"preferred-background-layer"
|
||||
)
|
||||
this.preferredBackgroundLayer = this.osmConnection.getPreference("preferred-background-layer")
|
||||
|
||||
this.addNewFeatureMode = this.osmConnection.getPreference(
|
||||
"preferences-add-new-mode",
|
||||
"button_click_right",
|
||||
"button_click_right"
|
||||
)
|
||||
this.showScale = UIEventSource.asBoolean(
|
||||
this.osmConnection.GetPreference("preference-show-scale", "false")
|
||||
)
|
||||
this.showScale = UIEventSource.asBoolean(this.osmConnection.GetPreference("preference-show-scale", "false"))
|
||||
|
||||
this.imageLicense = this.osmConnection.getPreference("pictures-license", "CC0")
|
||||
this.installedUserThemes = UserRelatedState.initInstalledUserThemes(osmConnection)
|
||||
|
|
@ -224,12 +230,13 @@ export default class UserRelatedState {
|
|||
"theme",
|
||||
this.osmConnection,
|
||||
10,
|
||||
(a, b) => a === b,
|
||||
(a, b) => a === b
|
||||
)
|
||||
this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>("places",
|
||||
this.recentlyVisitedSearch = new OptionallySyncedHistory<GeocodeResult>(
|
||||
"places",
|
||||
this.osmConnection,
|
||||
15,
|
||||
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type,
|
||||
(a, b) => a.osm_id === b.osm_id && a.osm_type === b.osm_type
|
||||
)
|
||||
this.syncLanguage()
|
||||
this.recentlyVisitedThemes.addDefferred(layout?.id)
|
||||
|
|
@ -279,9 +286,7 @@ export default class UserRelatedState {
|
|||
*/
|
||||
public addUnofficialTheme(themeInfo: MinimalThemeInformation) {
|
||||
const pref = this.osmConnection.getPreference("unofficial-theme-" + themeInfo.id)
|
||||
this.osmConnection.isLoggedIn.when(
|
||||
() => pref.set(JSON.stringify(themeInfo))
|
||||
)
|
||||
this.osmConnection.isLoggedIn.when(() => pref.set(JSON.stringify(themeInfo)))
|
||||
}
|
||||
|
||||
public getUnofficialTheme(id: string): MinimalThemeInformation | undefined {
|
||||
|
|
@ -298,9 +303,9 @@ export default class UserRelatedState {
|
|||
} catch (e) {
|
||||
console.warn(
|
||||
"Removing theme " +
|
||||
id +
|
||||
" as it could not be parsed from the preferences; the content is:",
|
||||
str,
|
||||
id +
|
||||
" as it could not be parsed from the preferences; the content is:",
|
||||
str
|
||||
)
|
||||
pref.setData(null)
|
||||
return undefined
|
||||
|
|
@ -330,7 +335,7 @@ export default class UserRelatedState {
|
|||
title: layout.title.translations,
|
||||
shortDescription: layout.shortDescription.translations,
|
||||
definition: layout["definition"],
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -340,7 +345,7 @@ export default class UserRelatedState {
|
|||
return osmConnection.preferencesHandler.allPreferences.map((prefs) =>
|
||||
Object.keys(prefs)
|
||||
.filter((k) => k.startsWith(prefix))
|
||||
.map((k) => k.substring(prefix.length)),
|
||||
.map((k) => k.substring(prefix.length))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +359,7 @@ export default class UserRelatedState {
|
|||
return userPreferences.map((preferences) =>
|
||||
Object.keys(preferences)
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length)),
|
||||
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -370,7 +375,7 @@ export default class UserRelatedState {
|
|||
return undefined
|
||||
}
|
||||
return [home.lon, home.lat]
|
||||
}),
|
||||
})
|
||||
).map((homeLonLat) => {
|
||||
if (homeLonLat === undefined) {
|
||||
return empty
|
||||
|
|
@ -400,7 +405,7 @@ export default class UserRelatedState {
|
|||
* */
|
||||
private initAmendedPrefs(
|
||||
layout?: ThemeConfig,
|
||||
featureSwitches?: FeatureSwitchState,
|
||||
featureSwitches?: FeatureSwitchState
|
||||
): UIEventSource<Record<string, string>> {
|
||||
const amendedPrefs = new UIEventSource<Record<string, string>>({
|
||||
_theme: layout?.id,
|
||||
|
|
@ -446,19 +451,19 @@ export default class UserRelatedState {
|
|||
const missingLayers = Utils.Dedup(
|
||||
untranslated
|
||||
.filter((k) => k.startsWith("layers:"))
|
||||
.map((k) => k.slice("layers:".length).split(".")[0]),
|
||||
.map((k) => k.slice("layers:".length).split(".")[0])
|
||||
)
|
||||
|
||||
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,
|
||||
|
|
@ -475,7 +480,7 @@ export default class UserRelatedState {
|
|||
}
|
||||
amendedPrefs.ping()
|
||||
},
|
||||
[this.translationMode],
|
||||
[this.translationMode]
|
||||
)
|
||||
|
||||
this.mangroveIdentity.getKeyId().addCallbackAndRun((kid) => {
|
||||
|
|
@ -494,7 +499,7 @@ export default class UserRelatedState {
|
|||
.makeHtml(userDetails.description)
|
||||
?.replace(/>/g, ">")
|
||||
?.replace(/</g, "<")
|
||||
?.replace(/\n/g, ""),
|
||||
?.replace(/\n/g, "")
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -505,7 +510,7 @@ export default class UserRelatedState {
|
|||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
},
|
||||
}
|
||||
)
|
||||
if (isTranslator) {
|
||||
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
|
||||
|
|
@ -514,7 +519,7 @@ export default class UserRelatedState {
|
|||
(c: { contributor: string; commits: number }) => {
|
||||
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
|
||||
return replaced === simplifiedName
|
||||
},
|
||||
}
|
||||
)
|
||||
if (isCodeContributor) {
|
||||
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
|
||||
|
|
|
|||
|
|
@ -1,14 +1,42 @@
|
|||
import { Utils } from "../../Utils"
|
||||
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
|
||||
export class ThemeMetaTagging {
|
||||
public static readonly themeName = "usersettings"
|
||||
public static readonly themeName = "usersettings"
|
||||
|
||||
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
|
||||
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/</g,'<')?.replace(/>/g,'>') ?? '' )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
|
||||
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
|
||||
feat.properties['__current_backgroun'] = 'initial_value'
|
||||
}
|
||||
}
|
||||
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
|
||||
feat.properties._description
|
||||
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
|
||||
?.at(1)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_d",
|
||||
() => feat.properties._description?.replace(/</g, "<")?.replace(/>/g, ">") ?? ""
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.href.match(/mastodon|en.osm.town/) !== null
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
|
||||
((feat) => {
|
||||
const e = document.createElement("div")
|
||||
e.innerHTML = feat.properties._d
|
||||
return Array.from(e.getElementsByTagName("a")).filter(
|
||||
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
|
||||
)[0]?.href
|
||||
})(feat)
|
||||
)
|
||||
Utils.AddLazyProperty(
|
||||
feat.properties,
|
||||
"_mastodon_candidate",
|
||||
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
|
||||
)
|
||||
feat.properties["__current_backgroun"] = "initial_value"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export class Tag extends TagsFilter {
|
|||
if (value === undefined) {
|
||||
throw `Invalid value while constructing a Tag with key '${key}': value is undefined`
|
||||
}
|
||||
if(value.length > 255 || key.length > 255){
|
||||
if (value.length > 255 || key.length > 255) {
|
||||
throw "Invalid tag: length is over 255"
|
||||
}
|
||||
if (value === "*") {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export class Stores {
|
|||
}
|
||||
|
||||
public static FromPromiseWithErr<T>(
|
||||
promise: Promise<T>,
|
||||
promise: Promise<T>
|
||||
): Store<{ success: T } | { error: any }> {
|
||||
return UIEventSource.FromPromiseWithErr(promise)
|
||||
}
|
||||
|
|
@ -99,7 +99,7 @@ export class Stores {
|
|||
*/
|
||||
static holdDefined<T>(store: Store<T | undefined>): Store<T | undefined> {
|
||||
const newStore = new UIEventSource(store.data)
|
||||
store.addCallbackD(t => {
|
||||
store.addCallbackD((t) => {
|
||||
newStore.setData(t)
|
||||
})
|
||||
return newStore
|
||||
|
|
@ -133,23 +133,27 @@ export abstract class Store<T> implements Readable<T> {
|
|||
abstract map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStoresToWatch: Store<any>[],
|
||||
callbackDestroyFunction: (f: () => void) => void,
|
||||
callbackDestroyFunction: (f: () => void) => void
|
||||
): Store<J>
|
||||
|
||||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraStoresToWatch?: Store<any>[],
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
callbackDestroyFunction?: (f: () => void) => void
|
||||
): Store<J> {
|
||||
return this.map((t) => {
|
||||
if (t === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (t === null) {
|
||||
return null
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
}, extraStoresToWatch, callbackDestroyFunction)
|
||||
return this.map(
|
||||
(t) => {
|
||||
if (t === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (t === null) {
|
||||
return null
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
},
|
||||
extraStoresToWatch,
|
||||
callbackDestroyFunction
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,7 +180,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
abstract addCallbackAndRun(callback: (data: T) => void): () => void
|
||||
|
||||
public withEqualityStabilized(
|
||||
comparator: (t: T | undefined, t1: T | undefined) => boolean,
|
||||
comparator: (t: T | undefined, t1: T | undefined) => boolean
|
||||
): Store<T> {
|
||||
let oldValue = undefined
|
||||
return this.map((v) => {
|
||||
|
|
@ -269,7 +273,10 @@ export abstract class Store<T> implements Readable<T> {
|
|||
return sink
|
||||
}
|
||||
|
||||
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] = []): Store<X> {
|
||||
public bindD<X>(
|
||||
f: (t: Exclude<T, undefined | null>) => Store<X>,
|
||||
extraSources: UIEventSource<object>[] = []
|
||||
): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === null) {
|
||||
return null
|
||||
|
|
@ -343,10 +350,10 @@ export abstract class Store<T> implements Readable<T> {
|
|||
|
||||
public abstract destroy()
|
||||
|
||||
when(callback: () => void, condition?: (v:T) => boolean) {
|
||||
condition ??= v => v === true
|
||||
this.addCallbackAndRunD(v => {
|
||||
if ( condition(v)) {
|
||||
when(callback: () => void, condition?: (v: T) => boolean) {
|
||||
condition ??= (v) => v === true
|
||||
this.addCallbackAndRunD((v) => {
|
||||
if (condition(v)) {
|
||||
callback()
|
||||
return true
|
||||
}
|
||||
|
|
@ -364,8 +371,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
|
||||
|
|
@ -394,7 +400,7 @@ export class ImmutableStore<T> extends Store<T> {
|
|||
map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStores: Store<any>[] = undefined,
|
||||
ondestroyCallback?: (f: () => void) => void,
|
||||
ondestroyCallback?: (f: () => void) => void
|
||||
): ImmutableStore<J> {
|
||||
if (extraStores?.length > 0) {
|
||||
return new MappedStore(this, f, extraStores, undefined, f(this.data), ondestroyCallback)
|
||||
|
|
@ -464,7 +470,7 @@ class ListenerTracker<T> {
|
|||
let endTime = new Date().getTime() / 1000
|
||||
if (endTime - startTime > 500) {
|
||||
console.trace(
|
||||
"Warning: a ping took more then 500ms; this is probably a performance issue",
|
||||
"Warning: a ping took more then 500ms; this is probably a performance issue"
|
||||
)
|
||||
}
|
||||
if (toDelete !== undefined) {
|
||||
|
|
@ -506,7 +512,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
extraStores: Store<any>[],
|
||||
upstreamListenerHandler: ListenerTracker<TIn> | undefined,
|
||||
initialState: T,
|
||||
onDestroy?: (f: () => void) => void,
|
||||
onDestroy?: (f: () => void) => void
|
||||
) {
|
||||
super()
|
||||
this._upstream = upstream
|
||||
|
|
@ -546,7 +552,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
map<J>(
|
||||
f: (t: T) => J,
|
||||
extraStores: Store<any>[] = undefined,
|
||||
ondestroyCallback?: (f: () => void) => void,
|
||||
ondestroyCallback?: (f: () => void) => void
|
||||
): Store<J> {
|
||||
let stores: Store<any>[] = undefined
|
||||
if (extraStores?.length > 0 || this._extraStores?.length > 0) {
|
||||
|
|
@ -568,7 +574,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
stores,
|
||||
this._callbacks,
|
||||
f(this.data),
|
||||
ondestroyCallback,
|
||||
ondestroyCallback
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -624,7 +630,7 @@ class MappedStore<TIn, T> extends Store<T> {
|
|||
|
||||
this._unregisterFromUpstream = this._upstream.addCallback((_) => self.update())
|
||||
this._unregisterFromExtraStores = this._extraStores?.map((store) =>
|
||||
store?.addCallback((_) => self.update()),
|
||||
store?.addCallback((_) => self.update())
|
||||
)
|
||||
this._callbacksAreRegistered = true
|
||||
}
|
||||
|
|
@ -645,8 +651,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>()
|
||||
|
||||
|
|
@ -661,7 +666,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
public static flatten<X>(
|
||||
source: Store<Store<X>>,
|
||||
possibleSources?: Store<object>[],
|
||||
possibleSources?: Store<object>[]
|
||||
): UIEventSource<X> {
|
||||
const sink = new UIEventSource<X>(source.data?.data)
|
||||
|
||||
|
|
@ -690,7 +695,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
*/
|
||||
public static FromPromise<T>(
|
||||
promise: Promise<T>,
|
||||
onError: (e) => void = undefined,
|
||||
onError: (e) => void = undefined
|
||||
): UIEventSource<T> {
|
||||
const src = new UIEventSource<T>(undefined)
|
||||
promise?.then((d) => src.setData(d))
|
||||
|
|
@ -711,7 +716,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
* @constructor
|
||||
*/
|
||||
public static FromPromiseWithErr<T>(
|
||||
promise: Promise<T>,
|
||||
promise: Promise<T>
|
||||
): UIEventSource<{ success: T } | { error: any } | undefined> {
|
||||
const src = new UIEventSource<{ success: T } | { error: any }>(undefined)
|
||||
promise
|
||||
|
|
@ -743,7 +748,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return undefined
|
||||
}
|
||||
return "" + fl
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -774,7 +779,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return undefined
|
||||
}
|
||||
return "" + fl
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -782,11 +787,14 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
return stringUIEventSource.sync(
|
||||
(str) => str === "true",
|
||||
[],
|
||||
(b) => "" + b,
|
||||
(b) => "" + b
|
||||
)
|
||||
}
|
||||
|
||||
static asObject<T extends object>(stringUIEventSource: UIEventSource<string>, defaultV: T): UIEventSource<T> {
|
||||
static asObject<T extends object>(
|
||||
stringUIEventSource: UIEventSource<string>,
|
||||
defaultV: T
|
||||
): UIEventSource<T> {
|
||||
return stringUIEventSource.sync(
|
||||
(str) => {
|
||||
if (str === undefined || str === null || str === "") {
|
||||
|
|
@ -800,7 +808,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
}
|
||||
},
|
||||
[],
|
||||
(b) => JSON.stringify(b) ?? "",
|
||||
(b) => JSON.stringify(b) ?? ""
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -890,7 +898,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
public map<J>(
|
||||
f: (t: T) => J,
|
||||
extraSources: Store<any>[] = [],
|
||||
onDestroy?: (f: () => void) => void,
|
||||
onDestroy?: (f: () => void) => void
|
||||
): Store<J> {
|
||||
return new MappedStore(this, f, extraSources, this._callbacks, f(this.data), onDestroy)
|
||||
}
|
||||
|
|
@ -902,7 +910,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
public mapD<J>(
|
||||
f: (t: Exclude<T, undefined | null>) => J,
|
||||
extraSources: Store<any>[] = [],
|
||||
callbackDestroyFunction?: (f: () => void) => void,
|
||||
callbackDestroyFunction?: (f: () => void) => void
|
||||
): Store<J | undefined> {
|
||||
return new MappedStore(
|
||||
this,
|
||||
|
|
@ -920,7 +928,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
this.data === undefined || this.data === null
|
||||
? <undefined | null>this.data
|
||||
: f(<any>this.data),
|
||||
callbackDestroyFunction,
|
||||
callbackDestroyFunction
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -940,7 +948,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
f: (t: T) => J,
|
||||
extraSources: Store<any>[],
|
||||
g: (j: J, t: T) => T,
|
||||
allowUnregister = false,
|
||||
allowUnregister = false
|
||||
): UIEventSource<J> {
|
||||
const self = this
|
||||
|
||||
|
|
@ -949,7 +957,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
|
|||
|
||||
const newSource = new UIEventSource<J>(f(this.data), "map(" + this.tag + ")@" + callee)
|
||||
|
||||
const update = function() {
|
||||
const update = function () {
|
||||
newSource.setData(f(self.data))
|
||||
return allowUnregister && newSource._callbacks.length() === 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { Utils } from "../../Utils"
|
|||
* UIEventsource-wrapper around localStorage
|
||||
*/
|
||||
export class LocalStorageSource {
|
||||
|
||||
private static readonly _cache: Record<string, UIEventSource<string>> = {}
|
||||
|
||||
static getParsed<T>(key: string, defaultValue: T): UIEventSource<T> {
|
||||
|
|
@ -21,7 +20,7 @@ export class LocalStorageSource {
|
|||
}
|
||||
},
|
||||
[],
|
||||
(value) => JSON.stringify(value),
|
||||
(value) => JSON.stringify(value)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -32,7 +31,6 @@ export class LocalStorageSource {
|
|||
}
|
||||
let saved = defaultValue
|
||||
if (!Utils.runningFromConsole) {
|
||||
|
||||
try {
|
||||
saved = localStorage.getItem(key)
|
||||
if (saved === "undefined") {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export class MangroveIdentity {
|
|||
this.mangroveIdentity = mangroveIdentity
|
||||
this._mangroveIdentityCreationDate = mangroveIdentityCreationDate
|
||||
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
||||
if(data === ""){
|
||||
if (data === "") {
|
||||
return
|
||||
}
|
||||
await this.setKeypair(data)
|
||||
|
|
|
|||
|
|
@ -297,15 +297,13 @@ export default class NameSuggestionIndex {
|
|||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
i.locationSet.include.some((c) => countries.indexOf(c) >= 0)
|
||||
) {
|
||||
if (i.locationSet.include.some((c) => countries.indexOf(c) >= 0)) {
|
||||
// We prefer the countries provided by lonlat2country, they are more precise and are loaded already anyway (cheap)
|
||||
// Country might contain multiple countries, separated by ';'
|
||||
return true
|
||||
}
|
||||
|
||||
if (i.locationSet.exclude?.some(c => countries.indexOf(c) >= 0)) {
|
||||
if (i.locationSet.exclude?.some((c) => countries.indexOf(c) >= 0)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -313,18 +311,20 @@ export default class NameSuggestionIndex {
|
|||
return true
|
||||
}
|
||||
|
||||
const hasSpecial = i.locationSet.include?.some(i => i.endsWith(".geojson") || Array.isArray(i)) || i.locationSet.exclude?.some(i => i.endsWith(".geojson") || Array.isArray(i))
|
||||
const hasSpecial =
|
||||
i.locationSet.include?.some((i) => i.endsWith(".geojson") || Array.isArray(i)) ||
|
||||
i.locationSet.exclude?.some((i) => i.endsWith(".geojson") || Array.isArray(i))
|
||||
if (!hasSpecial) {
|
||||
return false
|
||||
}
|
||||
const key = i.locationSet.include?.join(";") + "-" + i.locationSet.exclude?.join(";")
|
||||
const fromCache = NameSuggestionIndex.resolvedSets[key]
|
||||
const resolvedSet = fromCache ?? NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
|
||||
const resolvedSet =
|
||||
fromCache ?? NameSuggestionIndex.loco.resolveLocationSet(i.locationSet)
|
||||
if (!fromCache) {
|
||||
NameSuggestionIndex.resolvedSets[key] = resolvedSet
|
||||
}
|
||||
|
||||
|
||||
if (resolvedSet) {
|
||||
// We actually have a location set, so we can check if the feature is in it, by determining if our point is inside the MultiPolygon using @turf/boolean-point-in-polygon
|
||||
// This might occur for some extra boundaries, such as counties, ...
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export interface P4CPicture {
|
|||
author?
|
||||
license?
|
||||
detailsUrl?: string
|
||||
direction?: number,
|
||||
direction?: number
|
||||
osmTags?: object /*To copy straight into OSM!*/
|
||||
thumbUrl: string
|
||||
details: {
|
||||
|
|
@ -103,7 +103,7 @@ class P4CImageFetcher implements ImageFetcher {
|
|||
{
|
||||
mindate: new Date().getTime() - maxAgeSeconds,
|
||||
towardscenter: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
} catch (e) {
|
||||
console.log("P4C image fetcher failed with", e)
|
||||
|
|
@ -172,16 +172,13 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
|
|||
constructor(url?: string, radius: number = 100) {
|
||||
this._radius = radius
|
||||
if (url) {
|
||||
|
||||
this._panoramax = new Panoramax(url)
|
||||
} else {
|
||||
this._panoramax = new PanoramaxXYZ()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async fetchImages(lat: number, lon: number): Promise<P4CPicture[]> {
|
||||
|
||||
const bboxObj = new BBox([
|
||||
GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), -45),
|
||||
GeoOperations.destination([lon, lat], this._radius * Math.sqrt(2), 135),
|
||||
|
|
@ -189,16 +186,16 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
|
|||
const bbox: [number, number, number, number] = bboxObj.toLngLatFlat()
|
||||
const images = await this._panoramax.search({ bbox, limit: 1000 })
|
||||
|
||||
return images.map(i => {
|
||||
return images.map((i) => {
|
||||
const [lng, lat] = i.geometry.coordinates
|
||||
return ({
|
||||
return {
|
||||
pictureUrl: i.assets.sd.href,
|
||||
coordinates: { lng, lat },
|
||||
|
||||
provider: "panoramax",
|
||||
direction: i.properties["view:azimuth"],
|
||||
osmTags: {
|
||||
"panoramax": i.id,
|
||||
panoramax: i.id,
|
||||
},
|
||||
thumbUrl: i.assets.thumb.href,
|
||||
date: new Date(i.properties.datetime).getTime(),
|
||||
|
|
@ -206,9 +203,10 @@ class ImagesFromPanoramaxFetcher implements ImageFetcher {
|
|||
author: i.providers.at(-1).name,
|
||||
detailsUrl: i.id,
|
||||
details: {
|
||||
isSpherical: i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular",
|
||||
isSpherical:
|
||||
i.properties["exif"]["Xmp.GPano.ProjectionType"] === "equirectangular",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -236,7 +234,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
|
|||
async fetchImagesForType(
|
||||
targetlat: number,
|
||||
targetlon: number,
|
||||
type: "lines" | "pois" | "polygons",
|
||||
type: "lines" | "pois" | "polygons"
|
||||
): Promise<P4CPicture[]> {
|
||||
const { x, y, z } = Tiles.embedded_tile(targetlat, targetlon, 14)
|
||||
|
||||
|
|
@ -253,7 +251,7 @@ class ImagesFromCacheServerFetcher implements ImageFetcher {
|
|||
}),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
z
|
||||
)
|
||||
await src.updateAsync()
|
||||
return src.features.data
|
||||
|
|
@ -427,7 +425,7 @@ export class CombinedFetcher {
|
|||
lat: number,
|
||||
lon: number,
|
||||
state: UIEventSource<Record<string, "loading" | "done" | "error">>,
|
||||
sink: UIEventSource<P4CPicture[]>,
|
||||
sink: UIEventSource<P4CPicture[]>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const pics = await source.fetchImages(lat, lon)
|
||||
|
|
@ -460,7 +458,7 @@ export class CombinedFetcher {
|
|||
|
||||
public getImagesAround(
|
||||
lon: number,
|
||||
lat: number,
|
||||
lat: number
|
||||
): {
|
||||
images: Store<P4CPicture[]>
|
||||
state: Store<Record<string, "loading" | "done" | "error">>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { WBK} from "wikibase-sdk"
|
||||
import { WBK } from "wikibase-sdk"
|
||||
|
||||
export class WikidataResponse {
|
||||
public readonly id: string
|
||||
|
|
@ -128,10 +128,9 @@ interface SparqlResult {
|
|||
* Utility functions around wikidata
|
||||
*/
|
||||
export default class Wikidata {
|
||||
|
||||
public static wds = WBK({
|
||||
instance: "https://wikidata.org",
|
||||
sparqlEndpoint: "https://query.wikidata.org/bigdata/namespace/wdq/sparql"
|
||||
sparqlEndpoint: "https://query.wikidata.org/bigdata/namespace/wdq/sparql",
|
||||
})
|
||||
|
||||
public static readonly neededUrls = [
|
||||
|
|
@ -211,7 +210,7 @@ export default class Wikidata {
|
|||
${instanceOf}
|
||||
${minusPhrases.join("\n ")}
|
||||
} ORDER BY ASC(?num) LIMIT ${options?.maxCount ?? 20}`
|
||||
const url = Wikidata. wds.sparqlQuery(sparql)
|
||||
const url = Wikidata.wds.sparqlQuery(sparql)
|
||||
|
||||
const result = await Utils.downloadJson<SparqlResult>(url)
|
||||
/*The full uri of the wikidata-item*/
|
||||
|
|
@ -252,7 +251,7 @@ export default class Wikidata {
|
|||
lang +
|
||||
"&type=item&origin=*" +
|
||||
"&props=" // props= removes some unused values in the result
|
||||
const response = await Utils.downloadJsonCached<{search: any[]}>(url, 10000)
|
||||
const response = await Utils.downloadJsonCached<{ search: any[] }>(url, 10000)
|
||||
|
||||
const result = response.search
|
||||
|
||||
|
|
@ -401,7 +400,7 @@ export default class Wikidata {
|
|||
"}"
|
||||
const url = Wikidata.wds.sparqlQuery(query)
|
||||
const result = await Utils.downloadJsonCached<SparqlResult>(url, 24 * 60 * 60 * 1000)
|
||||
return <any> result.results.bindings
|
||||
return <any>result.results.bindings
|
||||
}
|
||||
|
||||
private static _cache = new Map<string, Promise<WikidataResponse>>()
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export default class Constants {
|
|||
"favourite",
|
||||
"summary",
|
||||
"search",
|
||||
"geocoded_image"
|
||||
"geocoded_image",
|
||||
] as const
|
||||
/**
|
||||
* Special layers which are not included in a theme by default
|
||||
|
|
@ -50,7 +50,7 @@ export default class Constants {
|
|||
...Constants.no_include,
|
||||
] as const
|
||||
|
||||
public static panoramax: { url: string, token: string } = packagefile.config.panoramax
|
||||
public static panoramax: { url: string; token: string } = packagefile.config.panoramax
|
||||
|
||||
// The user journey states thresholds when a new feature gets unlocked
|
||||
public static userJourney = {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default class FilteredLayer {
|
|||
constructor(
|
||||
layer: LayerConfig,
|
||||
appliedFilters?: ReadonlyMap<string, UIEventSource<undefined | number | string>>,
|
||||
isDisplayed?: UIEventSource<boolean>,
|
||||
isDisplayed?: UIEventSource<boolean>
|
||||
) {
|
||||
this.layerDef = layer
|
||||
this.isDisplayed = isDisplayed ?? new UIEventSource(true)
|
||||
|
|
@ -82,25 +82,25 @@ export default class FilteredLayer {
|
|||
layer: LayerConfig,
|
||||
context: string,
|
||||
osmConnection: OsmConnection,
|
||||
enabledByDefault?: Store<boolean>,
|
||||
enabledByDefault?: Store<boolean>
|
||||
) {
|
||||
let isDisplayed: UIEventSource<boolean>
|
||||
if (layer.syncSelection === "local") {
|
||||
isDisplayed = LocalStorageSource.getParsed(
|
||||
context + "-layer-" + layer.id + "-enabled",
|
||||
layer.shownByDefault,
|
||||
layer.shownByDefault
|
||||
)
|
||||
} else if (layer.syncSelection === "theme-only") {
|
||||
isDisplayed = FilteredLayer.getPref(
|
||||
osmConnection,
|
||||
context + "-layer-" + layer.id + "-enabled",
|
||||
layer,
|
||||
layer
|
||||
)
|
||||
} else if (layer.syncSelection === "global") {
|
||||
isDisplayed = FilteredLayer.getPref(
|
||||
osmConnection,
|
||||
"layer-" + layer.id + "-enabled",
|
||||
layer,
|
||||
layer
|
||||
)
|
||||
} else {
|
||||
let isShown = layer.shownByDefault
|
||||
|
|
@ -110,7 +110,7 @@ export default class FilteredLayer {
|
|||
isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||
FilteredLayer.queryParameterKey(layer),
|
||||
isShown,
|
||||
"Whether or not layer " + layer.id + " is shown",
|
||||
"Whether or not layer " + layer.id + " is shown"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,7 @@ export default class FilteredLayer {
|
|||
*/
|
||||
private static fieldsToTags(
|
||||
option: FilterConfigOption,
|
||||
fieldstate: string | Record<string, string>,
|
||||
fieldstate: string | Record<string, string>
|
||||
): TagsFilter | undefined {
|
||||
let properties: Record<string, string>
|
||||
if (typeof fieldstate === "string") {
|
||||
|
|
@ -181,7 +181,7 @@ export default class FilteredLayer {
|
|||
private static getPref(
|
||||
osmConnection: OsmConnection,
|
||||
key: string,
|
||||
layer: LayerConfig,
|
||||
layer: LayerConfig
|
||||
): UIEventSource<boolean> {
|
||||
return osmConnection.GetPreference(key, layer.shownByDefault + "").sync(
|
||||
(v) => {
|
||||
|
|
@ -196,7 +196,7 @@ export default class FilteredLayer {
|
|||
return undefined
|
||||
}
|
||||
return "" + b
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export interface MapProperties {
|
|||
onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void
|
||||
|
||||
flyTo(lon: number, lat: number, zoom: number): void
|
||||
|
||||
}
|
||||
|
||||
export interface ExportableMap {
|
||||
|
|
|
|||
|
|
@ -23,25 +23,25 @@ export class AvailableRasterLayers {
|
|||
}
|
||||
console.debug("Downloading ELI")
|
||||
const eli = await Utils.downloadJson<{ features: EditorLayerIndex }>(
|
||||
"./assets/data/editor-layer-index.json",
|
||||
"./assets/data/editor-layer-index.json"
|
||||
)
|
||||
this._editorLayerIndex = eli.features?.filter((l) => l.properties.id !== "Bing") ?? []
|
||||
this._editorLayerIndexStore.set(this._editorLayerIndex)
|
||||
return this._editorLayerIndex
|
||||
}
|
||||
|
||||
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> = AvailableRasterLayers.initGlobalLayers()
|
||||
public static readonly globalLayers: ReadonlyArray<RasterLayerPolygon> =
|
||||
AvailableRasterLayers.initGlobalLayers()
|
||||
|
||||
private static initGlobalLayers(): RasterLayerPolygon[] {
|
||||
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers ).layers
|
||||
.filter(
|
||||
(properties) =>
|
||||
properties.id !== "osm.carto" && properties.id !== "Bing", /*Added separately*/
|
||||
)
|
||||
const gl: RasterLayerProperties[] = (globallayers["default"] ?? globallayers).layers.filter(
|
||||
(properties) =>
|
||||
properties.id !== "osm.carto" && properties.id !== "Bing" /*Added separately*/
|
||||
)
|
||||
const glEli: RasterLayerProperties[] = globallayersEli["default"] ?? globallayersEli
|
||||
const joined = gl.concat(glEli)
|
||||
if (joined.some(j => !j.id)) {
|
||||
console.log("Invalid layers:", JSON.stringify(joined .filter(l => !l.id)))
|
||||
if (joined.some((j) => !j.id)) {
|
||||
console.log("Invalid layers:", JSON.stringify(joined.filter((l) => !l.id)))
|
||||
throw "Detected invalid global layer with invalid id"
|
||||
}
|
||||
return joined.map(
|
||||
|
|
@ -50,7 +50,7 @@ export class AvailableRasterLayers {
|
|||
type: "Feature",
|
||||
properties,
|
||||
geometry: BBox.global.asGeometry(),
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -85,18 +85,18 @@ export class AvailableRasterLayers {
|
|||
|
||||
public static layersAvailableAt(
|
||||
location: Store<{ lon: number; lat: number }>,
|
||||
enableBing?: Store<boolean>,
|
||||
enableBing?: Store<boolean>
|
||||
): { store: Store<RasterLayerPolygon[]> } {
|
||||
const store = { store: undefined }
|
||||
Utils.AddLazyProperty(store, "store", () =>
|
||||
AvailableRasterLayers._layersAvailableAt(location, enableBing),
|
||||
AvailableRasterLayers._layersAvailableAt(location, enableBing)
|
||||
)
|
||||
return store
|
||||
}
|
||||
|
||||
private static _layersAvailableAt(
|
||||
location: Store<{ lon: number; lat: number }>,
|
||||
enableBing?: Store<boolean>,
|
||||
enableBing?: Store<boolean>
|
||||
): Store<RasterLayerPolygon[]> {
|
||||
this.editorLayerIndex() // start the download
|
||||
const availableLayersBboxes = Stores.ListStabilized(
|
||||
|
|
@ -109,8 +109,8 @@ export class AvailableRasterLayers {
|
|||
const lonlat: [number, number] = [loc.lon, loc.lat]
|
||||
return eli.filter((eliPolygon) => BBox.get(eliPolygon).contains(lonlat))
|
||||
},
|
||||
[AvailableRasterLayers._editorLayerIndexStore],
|
||||
),
|
||||
[AvailableRasterLayers._editorLayerIndexStore]
|
||||
)
|
||||
)
|
||||
return Stores.ListStabilized(
|
||||
availableLayersBboxes.map(
|
||||
|
|
@ -132,15 +132,15 @@ export class AvailableRasterLayers {
|
|||
if (
|
||||
!matching.some(
|
||||
(l) =>
|
||||
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id,
|
||||
l.id === AvailableRasterLayers.defaultBackgroundLayer.properties.id
|
||||
)
|
||||
) {
|
||||
matching.push(AvailableRasterLayers.defaultBackgroundLayer)
|
||||
}
|
||||
return matching
|
||||
},
|
||||
[enableBing],
|
||||
),
|
||||
[enableBing]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -159,7 +159,7 @@ export class RasterLayerUtils {
|
|||
available: RasterLayerPolygon[],
|
||||
preferredCategory: string,
|
||||
ignoreLayer?: RasterLayerPolygon,
|
||||
skipLayers: number = 0,
|
||||
skipLayers: number = 0
|
||||
): RasterLayerPolygon {
|
||||
const inCategory = available.filter((l) => l.properties.category === preferredCategory)
|
||||
const best: RasterLayerPolygon[] = inCategory.filter((l) => l.properties.best)
|
||||
|
|
@ -167,7 +167,7 @@ export class RasterLayerUtils {
|
|||
let all = best.concat(others)
|
||||
console.log(
|
||||
"Selected layers are:",
|
||||
all.map((l) => l.properties.id),
|
||||
all.map((l) => l.properties.id)
|
||||
)
|
||||
if (others.length > skipLayers) {
|
||||
all = all.slice(skipLayers)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import {
|
|||
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"
|
||||
|
|
@ -31,23 +34,26 @@ import { ConversionContext } from "./ConversionContext"
|
|||
import { ExpandRewrite } from "./ExpandRewrite"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
|
||||
|
||||
class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super("Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus 'ExpandFilter' should be run afterwards. Can be disabled with \"#filter\":\"no-auto\"", ["filter"], "AddFiltersFromTagRenderings")
|
||||
super(
|
||||
'Inspects all the tagRenderings. If some tagRenderings have the `filter` attribute set, introduce those filters. This step might introduce shorthand filter names, thus \'ExpandFilter\' should be run afterwards. Can be disabled with "#filter":"no-auto"',
|
||||
["filter"],
|
||||
"AddFiltersFromTagRenderings"
|
||||
)
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
const noAutoFilters = json["#filter"] === "no-auto"
|
||||
if(noAutoFilters){
|
||||
if (noAutoFilters) {
|
||||
return json
|
||||
}
|
||||
|
||||
if(json.filter?.["sameAs"]){
|
||||
if (json.filter?.["sameAs"]) {
|
||||
return json
|
||||
}
|
||||
|
||||
const filters: (FilterConfigJson | string)[] = [...<any>json.filter ?? []]
|
||||
const filters: (FilterConfigJson | string)[] = [...(<any>json.filter ?? [])]
|
||||
|
||||
function filterExists(filterName: string): boolean {
|
||||
return filters.some((existing) => {
|
||||
|
|
@ -59,8 +65,6 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
for (let i = 0; i < json.tagRenderings?.length; i++) {
|
||||
const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i]
|
||||
if (!tagRendering?.filter) {
|
||||
|
|
@ -70,7 +74,12 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
|||
if (filterExists(tagRendering["id"])) {
|
||||
continue
|
||||
}
|
||||
filters.push(ExpandFilter.buildFilterFromTagRendering(tagRendering, context.enters("tagRenderings", i, "filter")))
|
||||
filters.push(
|
||||
ExpandFilter.buildFilterFromTagRendering(
|
||||
tagRendering,
|
||||
context.enters("tagRenderings", i, "filter")
|
||||
)
|
||||
)
|
||||
continue
|
||||
}
|
||||
for (const filterName of tagRendering.filter ?? []) {
|
||||
|
|
@ -89,7 +98,7 @@ class AddFiltersFromTagRenderings extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
if(filters.length === 0){
|
||||
if (filters.length === 0) {
|
||||
return json
|
||||
}
|
||||
|
||||
|
|
@ -102,10 +111,12 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
["Expands filters: replaces a shorthand by the value found in 'filters.json'.",
|
||||
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead."].join(" "),
|
||||
[
|
||||
"Expands filters: replaces a shorthand by the value found in 'filters.json'.",
|
||||
"If the string is formatted 'layername.filtername, it will be looked up into that layer instead.",
|
||||
].join(" "),
|
||||
["filter"],
|
||||
"ExpandFilter",
|
||||
"ExpandFilter"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
|
@ -118,10 +129,13 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
return filters
|
||||
}
|
||||
|
||||
public static buildFilterFromTagRendering(tr: TagRenderingConfigJson, context: ConversionContext): FilterConfigJson {
|
||||
public static buildFilterFromTagRendering(
|
||||
tr: TagRenderingConfigJson,
|
||||
context: ConversionContext
|
||||
): FilterConfigJson {
|
||||
if (!(tr.mappings?.length >= 1)) {
|
||||
context.err(
|
||||
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
|
||||
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings"
|
||||
)
|
||||
}
|
||||
const options = (<QuestionableTagRenderingConfigJson>tr).mappings.map((mapping) => {
|
||||
|
|
@ -131,12 +145,13 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
emoji = icon
|
||||
icon = undefined
|
||||
}
|
||||
return (<FilterConfigOptionJson>{
|
||||
return <FilterConfigOptionJson>{
|
||||
question: mapping.then,
|
||||
osmTags: mapping.if,
|
||||
searchTerms: mapping.searchTerms,
|
||||
icon, emoji,
|
||||
})
|
||||
icon,
|
||||
emoji,
|
||||
}
|
||||
})
|
||||
// Add default option
|
||||
options.unshift({
|
||||
|
|
@ -144,10 +159,10 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
osmTags: undefined,
|
||||
searchTerms: undefined,
|
||||
})
|
||||
return ({
|
||||
return {
|
||||
id: tr["id"],
|
||||
options,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: ConversionContext): LayerConfigJson {
|
||||
|
|
@ -159,11 +174,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
return json // Nothing to change here
|
||||
}
|
||||
|
||||
|
||||
const newFilters: FilterConfigJson[] = []
|
||||
const filters = <(FilterConfigJson | string)[]>json.filter
|
||||
|
||||
|
||||
/**
|
||||
* Create filters based on builtin filters or create them based on the tagRendering
|
||||
*/
|
||||
|
|
@ -181,7 +194,10 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings.find((tr) => !!tr && tr["id"] === filter)
|
||||
)
|
||||
if (matchingTr) {
|
||||
const filter = ExpandFilter.buildFilterFromTagRendering(matchingTr, context.enters("filter", i))
|
||||
const filter = ExpandFilter.buildFilterFromTagRendering(
|
||||
matchingTr,
|
||||
context.enters("filter", i)
|
||||
)
|
||||
newFilters.push(filter)
|
||||
continue
|
||||
}
|
||||
|
|
@ -194,7 +210,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const split = filter.split(".")
|
||||
if (split.length > 2) {
|
||||
context.err(
|
||||
"invalid filter name: " + filter + ", expected `layername.filterid`",
|
||||
"invalid filter name: " + filter + ", expected `layername.filterid`"
|
||||
)
|
||||
}
|
||||
const layer = this._state.sharedLayers.get(split[0])
|
||||
|
|
@ -203,7 +219,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
const expectedId = split[1]
|
||||
const expandedFilter = (<(FilterConfigJson | string)[]>layer.filter).find(
|
||||
(f) => typeof f !== "string" && f.id === expectedId,
|
||||
(f) => typeof f !== "string" && f.id === expectedId
|
||||
)
|
||||
if (expandedFilter === undefined) {
|
||||
context.err("Did not find filter with name " + filter)
|
||||
|
|
@ -218,15 +234,15 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const suggestions = Utils.sortedByLevenshteinDistance(
|
||||
filter,
|
||||
Array.from(ExpandFilter.predefinedFilters.keys()),
|
||||
(t) => t,
|
||||
(t) => t
|
||||
)
|
||||
context
|
||||
.enter(filter)
|
||||
.err(
|
||||
"While searching for predefined filter " +
|
||||
filter +
|
||||
": this filter is not found. Perhaps you meant one of: " +
|
||||
suggestions,
|
||||
filter +
|
||||
": this filter is not found. Perhaps you meant one of: " +
|
||||
suggestions
|
||||
)
|
||||
}
|
||||
newFilters.push(found)
|
||||
|
|
@ -239,9 +255,9 @@ class ExpandTagRendering extends Conversion<
|
|||
| string
|
||||
| TagRenderingConfigJson
|
||||
| {
|
||||
builtin: string | string[]
|
||||
override: any
|
||||
},
|
||||
builtin: string | string[]
|
||||
override: any
|
||||
},
|
||||
TagRenderingConfigJson[]
|
||||
> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
|
@ -263,12 +279,12 @@ class ExpandTagRendering extends Conversion<
|
|||
noHardcodedStrings?: false | boolean
|
||||
// If set, a question will be added to the 'sharedTagRenderings'. Should only be used for 'questions.json'
|
||||
addToContext?: false | boolean
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(
|
||||
"Converts a tagRenderingSpec into the full tagRendering, e.g. by substituting the tagRendering by the shared-question and reusing the builtins",
|
||||
[],
|
||||
"ExpandTagRendering",
|
||||
"ExpandTagRendering"
|
||||
)
|
||||
this._state = state
|
||||
this._self = self
|
||||
|
|
@ -288,7 +304,7 @@ class ExpandTagRendering extends Conversion<
|
|||
|
||||
public convert(
|
||||
spec: string | any,
|
||||
ctx: ConversionContext,
|
||||
ctx: ConversionContext
|
||||
): QuestionableTagRenderingConfigJson[] {
|
||||
const trs = this.convertOnce(spec, ctx)
|
||||
|
||||
|
|
@ -401,8 +417,8 @@ class ExpandTagRendering extends Conversion<
|
|||
found,
|
||||
ConversionContext.construct(
|
||||
[layer.id, "tagRenderings", found["id"]],
|
||||
["AddContextToTranslations"],
|
||||
),
|
||||
["AddContextToTranslations"]
|
||||
)
|
||||
)
|
||||
matchingTrs[i] = found
|
||||
}
|
||||
|
|
@ -430,17 +446,17 @@ class ExpandTagRendering extends Conversion<
|
|||
ctx.warn(
|
||||
`A literal rendering was detected: ${tr}
|
||||
Did you perhaps forgot to add a layer name as 'layername.${tr}'? ` +
|
||||
Array.from(state.sharedLayers.keys()).join(", "),
|
||||
Array.from(state.sharedLayers.keys()).join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
if (this._options?.noHardcodedStrings && this._state?.sharedLayers?.size > 0) {
|
||||
ctx.err(
|
||||
"Detected an invocation to a builtin tagRendering, but this tagrendering was not found: " +
|
||||
tr +
|
||||
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
|
||||
tr +
|
||||
"`? ",
|
||||
tr +
|
||||
" \n Did you perhaps forget to add the layer as prefix, such as `icons." +
|
||||
tr +
|
||||
"`? "
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -475,9 +491,9 @@ class ExpandTagRendering extends Conversion<
|
|||
}
|
||||
ctx.err(
|
||||
"An object calling a builtin can only have keys `builtin` or `override`, but a key with name `" +
|
||||
key +
|
||||
"` was found. This won't be picked up! The full object is: " +
|
||||
JSON.stringify(tr),
|
||||
key +
|
||||
"` was found. This won't be picked up! The full object is: " +
|
||||
JSON.stringify(tr)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -496,39 +512,39 @@ class ExpandTagRendering extends Conversion<
|
|||
const candidates = Utils.sortedByLevenshteinDistance(
|
||||
layerName,
|
||||
Array.from(state.sharedLayers.keys()),
|
||||
(s) => s,
|
||||
(s) => s
|
||||
)
|
||||
if (state.sharedLayers.size === 0) {
|
||||
ctx.warn(
|
||||
"BOOTSTRAPPING. Rerun generate layeroverview. While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found for now, but ignoring as this is a bootstrapping run. ",
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found for now, but ignoring as this is a bootstrapping run. "
|
||||
)
|
||||
} else {
|
||||
ctx.err(
|
||||
": While reusing tagrendering: " +
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant one of " +
|
||||
candidates.slice(0, 3).join(", "),
|
||||
name +
|
||||
": layer " +
|
||||
layerName +
|
||||
" not found. Maybe you meant one of " +
|
||||
candidates.slice(0, 3).join(", ")
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
candidates = Utils.NoNull(layer.tagRenderings.map((tr) => tr["id"])).map(
|
||||
(id) => layerName + "." + id,
|
||||
(id) => layerName + "." + id
|
||||
)
|
||||
}
|
||||
candidates = Utils.sortedByLevenshteinDistance(name, candidates, (i) => i)
|
||||
ctx.err(
|
||||
"The tagRendering with identifier " +
|
||||
name +
|
||||
" was not found.\n\tDid you mean one of " +
|
||||
candidates.join(", ") +
|
||||
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first",
|
||||
name +
|
||||
" was not found.\n\tDid you mean one of " +
|
||||
candidates.join(", ") +
|
||||
"?\n(Hint: did you add a new label and are you trying to use this label at the same time? Run 'reset:layeroverview' first"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -553,13 +569,13 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
super(
|
||||
"If no 'inline' is set on the freeform key, it will be automatically added. If no special renderings are used, it'll be set to true",
|
||||
["freeform.inline"],
|
||||
"DetectInline",
|
||||
"DetectInline"
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: QuestionableTagRenderingConfigJson,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): QuestionableTagRenderingConfigJson {
|
||||
if (json.freeform === undefined) {
|
||||
return json
|
||||
|
|
@ -582,7 +598,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
if (json.freeform.inline === true) {
|
||||
context.err(
|
||||
"'inline' is set, but the rendering contains a special visualisation...\n " +
|
||||
spec[key],
|
||||
spec[key]
|
||||
)
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
|
|
@ -596,17 +612,18 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
return json
|
||||
}
|
||||
|
||||
if(json.render === undefined){
|
||||
if (json.render === undefined) {
|
||||
context.err("No 'render' defined")
|
||||
return json
|
||||
}
|
||||
|
||||
if(!Object.values(json?.render)?.some(render => render !== "{"+json.freeform.key+"}")){
|
||||
if (
|
||||
!Object.values(json?.render)?.some((render) => render !== "{" + json.freeform.key + "}")
|
||||
) {
|
||||
// We only render the current value, without anything more. Not worth inlining
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
json.freeform.inline ??= true
|
||||
return json
|
||||
}
|
||||
|
|
@ -617,7 +634,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds a 'questions'-object if no question element is added yet",
|
||||
["tagRenderings"],
|
||||
"AddQuestionBox",
|
||||
"AddQuestionBox"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -641,18 +658,18 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings = [...json.tagRenderings]
|
||||
const allSpecials: Exclude<RenderingSpecification, string>[] = <any>(
|
||||
ValidationUtils.getAllSpecialVisualisations(
|
||||
<QuestionableTagRenderingConfigJson[]>json.tagRenderings,
|
||||
<QuestionableTagRenderingConfigJson[]>json.tagRenderings
|
||||
).filter((spec) => typeof spec !== "string")
|
||||
)
|
||||
|
||||
const questionSpecials = allSpecials.filter((sp) => sp.func.funcName === "questions")
|
||||
const noLabels = questionSpecials.filter(
|
||||
(sp) => sp.args.length === 0 || sp.args[0].trim() === "",
|
||||
(sp) => sp.args.length === 0 || sp.args[0].trim() === ""
|
||||
)
|
||||
|
||||
if (noLabels.length > 1) {
|
||||
context.err(
|
||||
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this",
|
||||
"Multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -660,9 +677,9 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
const allLabels = new Set(
|
||||
[].concat(
|
||||
...json.tagRenderings.map(
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? [],
|
||||
),
|
||||
),
|
||||
(tr) => (<QuestionableTagRenderingConfigJson>tr).labels ?? []
|
||||
)
|
||||
)
|
||||
)
|
||||
const seen: Set<string> = new Set()
|
||||
for (const questionSpecial of questionSpecials) {
|
||||
|
|
@ -680,20 +697,20 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
if (blacklisted?.length > 0 && used?.length > 0) {
|
||||
context.err(
|
||||
"The {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", "),
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", ")
|
||||
)
|
||||
}
|
||||
for (const usedLabel of used) {
|
||||
if (!allLabels.has(usedLabel)) {
|
||||
context.err(
|
||||
"This layers specifies a special question element for label `" +
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", "),
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", ")
|
||||
)
|
||||
}
|
||||
seen.add(usedLabel)
|
||||
|
|
@ -726,7 +743,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements",
|
||||
[],
|
||||
"AddEditingElements",
|
||||
"AddEditingElements"
|
||||
)
|
||||
this._desugaring = desugaring
|
||||
this.builtinQuestions = Array.from(this._desugaring.tagRenderings?.values() ?? [])
|
||||
|
|
@ -756,13 +773,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings = [...(json.tagRenderings ?? [])]
|
||||
const allIds = new Set<string>(json.tagRenderings.map((tr) => tr["id"]))
|
||||
const specialVisualisations = ValidationUtils.getAllSpecialVisualisations(
|
||||
<any>json.tagRenderings,
|
||||
<any>json.tagRenderings
|
||||
)
|
||||
|
||||
const usedSpecialFunctions = new Set(
|
||||
specialVisualisations.map((sv) =>
|
||||
typeof sv === "string" ? undefined : sv.func.funcName,
|
||||
),
|
||||
typeof sv === "string" ? undefined : sv.func.funcName
|
||||
)
|
||||
)
|
||||
|
||||
/***** ADD TO TOP ****/
|
||||
|
|
@ -830,7 +847,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
super(
|
||||
"Converts a 'special' translation into a regular translation which uses parameters",
|
||||
["special"],
|
||||
"RewriteSpecial",
|
||||
"RewriteSpecial"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -921,12 +938,12 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
private static convertIfNeeded(
|
||||
input:
|
||||
| (object & {
|
||||
special: {
|
||||
type: string
|
||||
}
|
||||
})
|
||||
special: {
|
||||
type: string
|
||||
}
|
||||
})
|
||||
| any,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): any {
|
||||
const special = input["special"]
|
||||
if (special === undefined) {
|
||||
|
|
@ -936,7 +953,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const type = special["type"]
|
||||
if (type === undefined) {
|
||||
context.err(
|
||||
"A 'special'-block should define 'type' to indicate which visualisation should be used",
|
||||
"A 'special'-block should define 'type' to indicate which visualisation should be used"
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -946,10 +963,10 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const options = Utils.sortedByLevenshteinDistance(
|
||||
type,
|
||||
SpecialVisualizations.specialVisualizations,
|
||||
(sp) => sp.funcName,
|
||||
(sp) => sp.funcName
|
||||
)
|
||||
context.err(
|
||||
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`,
|
||||
`Special visualisation '${type}' not found. Did you perhaps mean ${options[0].funcName}, ${options[1].funcName} or ${options[2].funcName}?\n\tFor all known special visualisations, please see https://github.com/pietervdvn/MapComplete/blob/develop/Docs/SpecialRenderings.md`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -970,7 +987,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
const byDistance = Utils.sortedByLevenshteinDistance(
|
||||
wrongArg,
|
||||
argNamesList,
|
||||
(x) => x,
|
||||
(x) => x
|
||||
)
|
||||
return `Unexpected argument in special block at ${context} with name '${wrongArg}'. Did you mean ${
|
||||
byDistance[0]
|
||||
|
|
@ -989,8 +1006,8 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
`Obligated parameter '${arg.name}' in special rendering of type ${
|
||||
vis.funcName
|
||||
} not found.\n The full special rendering specification is: '${JSON.stringify(
|
||||
input,
|
||||
)}'\n ${arg.name}: ${arg.doc}`,
|
||||
input
|
||||
)}'\n ${arg.name}: ${arg.doc}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1092,7 +1109,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
|
|||
continue
|
||||
}
|
||||
Utils.WalkPath(path.path, json, (leaf, travelled) =>
|
||||
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled)),
|
||||
RewriteSpecial.convertIfNeeded(leaf, context.enter(travelled))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1126,7 +1143,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
|
|||
} = badgesJson[i]
|
||||
const expanded = this._expand.convert(
|
||||
<QuestionableTagRenderingConfigJson>iconBadge.then,
|
||||
context.enters("iconBadges", i),
|
||||
context.enters("iconBadges", i)
|
||||
)
|
||||
if (expanded === undefined) {
|
||||
iconBadges.push(iconBadge)
|
||||
|
|
@ -1137,7 +1154,7 @@ class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson> {
|
|||
...expanded.map((resolved) => ({
|
||||
if: iconBadge.if,
|
||||
then: <MinimalTagRenderingConfigJson>resolved,
|
||||
})),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1154,11 +1171,11 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson> {
|
|||
new Each(
|
||||
new On(
|
||||
"icon",
|
||||
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false })),
|
||||
),
|
||||
),
|
||||
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
|
||||
)
|
||||
)
|
||||
),
|
||||
new ExpandIconBadges(state, layer),
|
||||
new ExpandIconBadges(state, layer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1168,7 +1185,7 @@ class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"sets the fullNodeDatabase-bit if needed",
|
||||
["fullNodeDatabase"],
|
||||
"SetFullNodeDatabase",
|
||||
"SetFullNodeDatabase"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1197,7 +1214,7 @@ class ExpandMarkerRenderings extends DesugaringStep<IconConfigJson> {
|
|||
super(
|
||||
"Expands tagRenderings in the icons, if needed",
|
||||
["icon", "color"],
|
||||
"ExpandMarkerRenderings",
|
||||
"ExpandMarkerRenderings"
|
||||
)
|
||||
this._layer = layer
|
||||
this._state = state
|
||||
|
|
@ -1229,7 +1246,7 @@ class AddFavouriteBadges extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds the favourite heart to the title and the rendering badges",
|
||||
[],
|
||||
"AddFavouriteBadges",
|
||||
"AddFavouriteBadges"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1254,7 +1271,7 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"Adds the 'rating'-element if a reviews-element is used in the tagRenderings",
|
||||
["titleIcons"],
|
||||
"AddRatingBadge",
|
||||
"AddRatingBadge"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1273,8 +1290,8 @@ export class AddRatingBadge extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
const specialVis: Exclude<RenderingSpecification, string>[] = <
|
||||
Exclude<RenderingSpecification, string>[]
|
||||
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
|
||||
(rs) => typeof rs !== "string",
|
||||
>ValidationUtils.getAllSpecialVisualisations(<any>json.tagRenderings).filter(
|
||||
(rs) => typeof rs !== "string"
|
||||
)
|
||||
const funcs = new Set<string>(specialVis.map((rs) => rs.func.funcName))
|
||||
|
||||
|
|
@ -1290,12 +1307,12 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"The auto-icon creates a (non-clickable) title icon based on a tagRendering which has icons",
|
||||
["titleIcons"],
|
||||
"AutoTitleIcon",
|
||||
"AutoTitleIcon"
|
||||
)
|
||||
}
|
||||
|
||||
private createTitleIconsBasedOn(
|
||||
tr: QuestionableTagRenderingConfigJson,
|
||||
tr: QuestionableTagRenderingConfigJson
|
||||
): TagRenderingConfigJson | undefined {
|
||||
const mappings: { if: TagConfigJson; then: string }[] = tr.mappings
|
||||
?.filter((m) => m.icon !== undefined)
|
||||
|
|
@ -1325,7 +1342,7 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
return undefined
|
||||
}
|
||||
return this.createTitleIconsBasedOn(<any>tr)
|
||||
}),
|
||||
})
|
||||
)
|
||||
json.titleIcons.splice(allAutoIndex, 1, ...generated)
|
||||
return json
|
||||
|
|
@ -1354,8 +1371,8 @@ export class AutoTitleIcon extends DesugaringStep<LayerConfigJson> {
|
|||
.enters("titleIcons", i)
|
||||
.warn(
|
||||
"TagRendering with id " +
|
||||
trId +
|
||||
" does not have any icons, not generating an icon for this",
|
||||
trId +
|
||||
" does not have any icons, not generating an icon for this"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -1370,7 +1387,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
super(
|
||||
"If no source is given, automatically derives the osmTags by 'or'-ing all the preset tags",
|
||||
["source"],
|
||||
"DeriveSource",
|
||||
"DeriveSource"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1380,7 +1397,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
if (!json.presets) {
|
||||
context.err(
|
||||
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given",
|
||||
"No source tags given. Trying to derive the source-tags based on the presets, but no presets are given"
|
||||
)
|
||||
return json
|
||||
}
|
||||
|
|
@ -1406,7 +1423,7 @@ class DeriveSource extends DesugaringStep<LayerConfigJson> {
|
|||
export class PrepareLayer extends Fuse<LayerConfigJson> {
|
||||
constructor(
|
||||
state: DesugaringContext,
|
||||
options?: { addTagRenderingsToContext?: false | boolean },
|
||||
options?: { addTagRenderingsToContext?: false | boolean }
|
||||
) {
|
||||
super(
|
||||
"Fully prepares and expands a layer for the LayerConfig.",
|
||||
|
|
@ -1419,8 +1436,8 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new Concat(
|
||||
new ExpandTagRendering(state, layer, {
|
||||
addToContext: options?.addTagRenderingsToContext ?? false,
|
||||
}),
|
||||
),
|
||||
})
|
||||
)
|
||||
),
|
||||
new On("tagRenderings", new Each(new DetectInline())),
|
||||
new AddQuestionBox(),
|
||||
|
|
@ -1433,11 +1450,11 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On<PointRenderingConfigJson[], LayerConfigJson>(
|
||||
"pointRendering",
|
||||
(layer) =>
|
||||
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer)))),
|
||||
new Each(new On("marker", new Each(new ExpandMarkerRenderings(state, layer))))
|
||||
),
|
||||
new On<PointRenderingConfigJson[], LayerConfigJson>(
|
||||
"pointRendering",
|
||||
(layer) => new Each(new PreparePointRendering(state, layer)),
|
||||
(layer) => new Each(new PreparePointRendering(state, layer))
|
||||
),
|
||||
new SetDefault("titleIcons", ["icons.defaults"]),
|
||||
new AddRatingBadge(),
|
||||
|
|
@ -1446,10 +1463,10 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new On(
|
||||
"titleIcons",
|
||||
(layer) =>
|
||||
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true })),
|
||||
new Concat(new ExpandTagRendering(state, layer, { noHardcodedStrings: true }))
|
||||
),
|
||||
new AddFiltersFromTagRenderings(),
|
||||
new ExpandFilter(state),
|
||||
new ExpandFilter(state)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
import { Concat, Conversion, DesugaringContext, DesugaringStep, Each, Fuse, On, Pass, SetDefault } from "./Conversion"
|
||||
import {
|
||||
Concat,
|
||||
Conversion,
|
||||
DesugaringContext,
|
||||
DesugaringStep,
|
||||
Each,
|
||||
Fuse,
|
||||
On,
|
||||
Pass,
|
||||
SetDefault,
|
||||
} from "./Conversion"
|
||||
import { ThemeConfigJson } from "../Json/ThemeConfigJson"
|
||||
import { PrepareLayer } from "./PrepareLayer"
|
||||
import { LayerConfigJson } from "../Json/LayerConfigJson"
|
||||
|
|
@ -18,7 +28,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
super(
|
||||
"Converts the identifier of a builtin layer into the actual layer, or converts a 'builtin' syntax with override in the fully expanded form. Note that 'tagRenderings+' will be inserted before 'leftover-questions'",
|
||||
[],
|
||||
"SubstituteLayer",
|
||||
"SubstituteLayer"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
|
@ -28,7 +38,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
|
||||
function reportNotFound(name: string) {
|
||||
const knownLayers = Array.from(state.sharedLayers.keys())
|
||||
const withDistance:[string,number][] = knownLayers.map((lname) => [
|
||||
const withDistance: [string, number][] = knownLayers.map((lname) => [
|
||||
lname,
|
||||
Utils.levenshteinDistance(name, lname),
|
||||
])
|
||||
|
|
@ -74,14 +84,14 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
(found["tagRenderings"] ?? []).length > 0
|
||||
) {
|
||||
context.err(
|
||||
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`,
|
||||
`When overriding a layer, an override is not allowed to override into tagRenderings. Use "+tagRenderings" or "tagRenderings+" instead to prepend or append some questions.`
|
||||
)
|
||||
}
|
||||
try {
|
||||
const trPlus = json["override"]["tagRenderings+"]
|
||||
if (trPlus) {
|
||||
let index = found.tagRenderings.findIndex(
|
||||
(tr) => tr["id"] === "leftover-questions",
|
||||
(tr) => tr["id"] === "leftover-questions"
|
||||
)
|
||||
if (index < 0) {
|
||||
index = found.tagRenderings.length
|
||||
|
|
@ -95,8 +105,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
} catch (e) {
|
||||
context.err(
|
||||
`Could not apply an override due to: ${e}.\nThe override is: ${JSON.stringify(
|
||||
json["override"],
|
||||
)}`,
|
||||
json["override"]
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -120,9 +130,9 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
usedLabels.add(labels[forbiddenLabel])
|
||||
context.info(
|
||||
"Dropping tagRendering " +
|
||||
tr["id"] +
|
||||
" as it has a forbidden label: " +
|
||||
labels[forbiddenLabel],
|
||||
tr["id"] +
|
||||
" as it has a forbidden label: " +
|
||||
labels[forbiddenLabel]
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -131,7 +141,7 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
if (hideLabels.has(tr["id"])) {
|
||||
usedLabels.add(tr["id"])
|
||||
context.info(
|
||||
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label",
|
||||
"Dropping tagRendering " + tr["id"] + " as its id is a forbidden label"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -140,10 +150,10 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
usedLabels.add(tr["group"])
|
||||
context.info(
|
||||
"Dropping tagRendering " +
|
||||
tr["id"] +
|
||||
" as its group `" +
|
||||
tr["group"] +
|
||||
"` is a forbidden label",
|
||||
tr["id"] +
|
||||
" as its group `" +
|
||||
tr["group"] +
|
||||
"` is a forbidden label"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -154,8 +164,8 @@ class SubstituteLayer extends Conversion<string | LayerConfigJson, LayerConfigJs
|
|||
if (unused.length > 0) {
|
||||
context.err(
|
||||
"This theme specifies that certain tagrenderings have to be removed based on forbidden layers. One or more of these layers did not match any tagRenderings and caused no deletions: " +
|
||||
unused.join(", ") +
|
||||
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore",
|
||||
unused.join(", ") +
|
||||
"\n This means that this label can be removed or that the original tagRendering that should be deleted does not have this label anymore"
|
||||
)
|
||||
}
|
||||
found.tagRenderings = filtered
|
||||
|
|
@ -172,7 +182,7 @@ class AddDefaultLayers extends DesugaringStep<ThemeConfigJson> {
|
|||
super(
|
||||
"Adds the default layers, namely: " + Constants.added_by_default.join(", "),
|
||||
["layers"],
|
||||
"AddDefaultLayers",
|
||||
"AddDefaultLayers"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
|
@ -195,10 +205,10 @@ class AddDefaultLayers extends DesugaringStep<ThemeConfigJson> {
|
|||
if (alreadyLoaded.has(v.id)) {
|
||||
context.warn(
|
||||
"Layout " +
|
||||
context +
|
||||
" already has a layer with name " +
|
||||
v.id +
|
||||
"; skipping inclusion of this builtin layer",
|
||||
context +
|
||||
" already has a layer with name " +
|
||||
v.id +
|
||||
"; skipping inclusion of this builtin layer"
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
|
@ -214,7 +224,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<ThemeConfigJson> {
|
|||
super(
|
||||
"Adds context to translations, including the prefix 'themes:json.id'; this is to make sure terms in an 'overrides' or inline layer are linkable too",
|
||||
["_context"],
|
||||
"AddContextToTranlationsInLayout",
|
||||
"AddContextToTranlationsInLayout"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +233,7 @@ class AddContextToTranslationsInLayout extends DesugaringStep<ThemeConfigJson> {
|
|||
// The context is used to generate the 'context' in the translation .It _must_ be `json.id` to correctly link into weblate
|
||||
return conversion.convert(
|
||||
json,
|
||||
ConversionContext.construct([json.id], ["AddContextToTranslation"]),
|
||||
ConversionContext.construct([json.id], ["AddContextToTranslation"])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -233,7 +243,7 @@ class ApplyOverrideAll extends DesugaringStep<ThemeConfigJson> {
|
|||
super(
|
||||
"Applies 'overrideAll' onto every 'layer'. The 'overrideAll'-field is removed afterwards",
|
||||
["overrideAll", "layers"],
|
||||
"ApplyOverrideAll",
|
||||
"ApplyOverrideAll"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -262,7 +272,7 @@ class ApplyOverrideAll extends DesugaringStep<ThemeConfigJson> {
|
|||
layer.tagRenderings = tagRenderingsPlus
|
||||
} else {
|
||||
let index = layer.tagRenderings.findIndex(
|
||||
(tr) => tr["id"] === "leftover-questions",
|
||||
(tr) => tr["id"] === "leftover-questions"
|
||||
)
|
||||
if (index < 0) {
|
||||
index = layer.tagRenderings.length - 1
|
||||
|
|
@ -285,7 +295,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
super(
|
||||
`If a layer has a dependency on another layer, these layers are added automatically on the theme. (For example: defibrillator depends on 'walls_and_buildings' to snap onto. This layer is added automatically)`,
|
||||
["layers"],
|
||||
"AddDependencyLayersToTheme",
|
||||
"AddDependencyLayersToTheme"
|
||||
)
|
||||
this._state = state
|
||||
}
|
||||
|
|
@ -294,7 +304,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
alreadyLoaded: LayerConfigJson[],
|
||||
allKnownLayers: Map<string, LayerConfigJson>,
|
||||
themeId: string,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): { config: LayerConfigJson; reason: string }[] {
|
||||
const dependenciesToAdd: { config: LayerConfigJson; reason: string }[] = []
|
||||
const loadedLayerIds: Set<string> = new Set<string>(alreadyLoaded.map((l) => l?.id))
|
||||
|
|
@ -318,7 +328,7 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
for (const layerConfig of alreadyLoaded) {
|
||||
try {
|
||||
const layerDeps = DependencyCalculator.getLayerDependencies(
|
||||
new LayerConfig(layerConfig, themeId + "(dependencies)"),
|
||||
new LayerConfig(layerConfig, themeId + "(dependencies)")
|
||||
)
|
||||
dependencies.push(...layerDeps)
|
||||
} catch (e) {
|
||||
|
|
@ -337,8 +347,16 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
// We mark the needed layer as 'mustLoad'
|
||||
const loadedLayer = alreadyLoaded.find((l) => l.id === dependency.neededLayer)
|
||||
loadedLayer.forceLoad = true
|
||||
if(dependency.checkHasSnapName && !loadedLayer.snapName){
|
||||
context.enters("layer dependency").err("Layer "+dependency.neededLayer+" is loaded because "+dependency.reason+"; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`")
|
||||
if (dependency.checkHasSnapName && !loadedLayer.snapName) {
|
||||
context
|
||||
.enters("layer dependency")
|
||||
.err(
|
||||
"Layer " +
|
||||
dependency.neededLayer +
|
||||
" is loaded because " +
|
||||
dependency.reason +
|
||||
"; so it must specify a `snapName`. This is used in the sentence `move this point to snap it to {snapName}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -362,10 +380,10 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
if (dep === undefined) {
|
||||
const message = [
|
||||
"Loading a dependency failed: layer " +
|
||||
unmetDependency.neededLayer +
|
||||
" is not found, neither as layer of " +
|
||||
themeId +
|
||||
" nor as builtin layer.",
|
||||
unmetDependency.neededLayer +
|
||||
" is not found, neither as layer of " +
|
||||
themeId +
|
||||
" nor as builtin layer.",
|
||||
reason,
|
||||
"Loaded layers are: " + alreadyLoaded.map((l) => l.id).join(","),
|
||||
]
|
||||
|
|
@ -381,12 +399,11 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
})
|
||||
loadedLayerIds.add(dep.id)
|
||||
unmetDependencies = unmetDependencies.filter(
|
||||
(d) => d.neededLayer !== unmetDependency.neededLayer,
|
||||
(d) => d.neededLayer !== unmetDependency.neededLayer
|
||||
)
|
||||
}
|
||||
} while (unmetDependencies.length > 0)
|
||||
|
||||
|
||||
return dependenciesToAdd
|
||||
}
|
||||
|
||||
|
|
@ -404,12 +421,12 @@ class AddDependencyLayersToTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
layers,
|
||||
allKnownLayers,
|
||||
theme.id,
|
||||
context,
|
||||
context
|
||||
)
|
||||
if (dependencies.length > 0) {
|
||||
for (const dependency of dependencies) {
|
||||
context.info(
|
||||
"Added " + dependency.config.id + " to the theme. " + dependency.reason,
|
||||
"Added " + dependency.config.id + " to the theme. " + dependency.reason
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -457,7 +474,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
|
|||
super(
|
||||
"Generates a warning if a theme uses an unsubstituted layer",
|
||||
["layers"],
|
||||
"WarnForUnsubstitutedLayersInTheme",
|
||||
"WarnForUnsubstitutedLayersInTheme"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +486,7 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
|
|||
context
|
||||
.enter("layers")
|
||||
.err(
|
||||
"No layers are defined. You must define at least one layer to have a valid theme",
|
||||
"No layers are defined. You must define at least one layer to have a valid theme"
|
||||
)
|
||||
return json
|
||||
}
|
||||
|
|
@ -493,10 +510,10 @@ class WarnForUnsubstitutedLayersInTheme extends DesugaringStep<ThemeConfigJson>
|
|||
|
||||
context.warn(
|
||||
"The theme " +
|
||||
json.id +
|
||||
" has an inline layer: " +
|
||||
layer["id"] +
|
||||
". This is discouraged.",
|
||||
json.id +
|
||||
" has an inline layer: " +
|
||||
layer["id"] +
|
||||
". This is discouraged."
|
||||
)
|
||||
}
|
||||
return json
|
||||
|
|
@ -523,13 +540,13 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
}
|
||||
const sameBasedOn = <LayerConfigJson[]>(
|
||||
json.layers.filter(
|
||||
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id,
|
||||
(l) => l["_basedOn"] === layer["_basedOn"] && l["id"] !== layer.id
|
||||
)
|
||||
)
|
||||
const minZoomAll = Math.min(...sameBasedOn.map((sbo) => sbo.minzoom))
|
||||
|
||||
const sameNameDetected = sameBasedOn.some(
|
||||
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"]),
|
||||
(same) => JSON.stringify(layer["name"]) === JSON.stringify(same["name"])
|
||||
)
|
||||
if (!sameNameDetected) {
|
||||
// The name is unique, so it'll won't be confusing
|
||||
|
|
@ -538,12 +555,12 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
if (minZoomAll < layer.minzoom) {
|
||||
context.err(
|
||||
"There are multiple layers based on " +
|
||||
basedOn +
|
||||
". The layer with id " +
|
||||
layer.id +
|
||||
" has a minzoom of " +
|
||||
layer.minzoom +
|
||||
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer.",
|
||||
basedOn +
|
||||
". The layer with id " +
|
||||
layer.id +
|
||||
" has a minzoom of " +
|
||||
layer.minzoom +
|
||||
", and has a name set. Another similar layer has a lower minzoom. As such, the layer selection might show 'zoom in to see features' even though some of the features are already visible. Set `\"name\": null` for this layer and eventually remove the 'name':null for the other layer."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -563,17 +580,17 @@ class PostvalidateTheme extends DesugaringStep<ThemeConfigJson> {
|
|||
const closeLayers = Utils.sortedByLevenshteinDistance(
|
||||
sameAs,
|
||||
json.layers,
|
||||
(l) => l["id"],
|
||||
(l) => l["id"]
|
||||
).map((l) => l["id"])
|
||||
context
|
||||
.enters("layers", config.id, "filter", "sameAs")
|
||||
.err(
|
||||
"The layer " +
|
||||
config.id +
|
||||
" follows the filter state of layer " +
|
||||
sameAs +
|
||||
", but no layer with this name was found.\n\tDid you perhaps mean one of: " +
|
||||
closeLayers.slice(0, 3).join(", "),
|
||||
config.id +
|
||||
" follows the filter state of layer " +
|
||||
sameAs +
|
||||
", but no layer with this name was found.\n\tDid you perhaps mean one of: " +
|
||||
closeLayers.slice(0, 3).join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -589,7 +606,7 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
|
|||
state: DesugaringContext,
|
||||
options?: {
|
||||
skipDefaultLayers: false | boolean
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(
|
||||
"Fully prepares and expands a theme",
|
||||
|
|
@ -610,8 +627,8 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
|
|||
? new Pass("AddDefaultLayers is disabled due to the set flag")
|
||||
: new AddDefaultLayers(state),
|
||||
new AddDependencyLayersToTheme(state),
|
||||
// new AddImportLayers(),
|
||||
new PostvalidateTheme(state),
|
||||
// new AddImportLayers(),
|
||||
new PostvalidateTheme(state)
|
||||
)
|
||||
this.state = state
|
||||
}
|
||||
|
|
@ -626,13 +643,13 @@ export class PrepareTheme extends Fuse<ThemeConfigJson> {
|
|||
const needsNodeDatabase = result.layers?.some((l: LayerConfigJson) =>
|
||||
l.tagRenderings?.some((tr) =>
|
||||
ValidationUtils.getSpecialVisualisations(<any>tr)?.some(
|
||||
(special) => special.needsNodeDatabase,
|
||||
),
|
||||
),
|
||||
(special) => special.needsNodeDatabase
|
||||
)
|
||||
)
|
||||
)
|
||||
if (needsNodeDatabase) {
|
||||
context.info(
|
||||
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes",
|
||||
"Setting 'enableNodeDatabase' as this theme uses a special visualisation which needs to keep track of _all_ nodes"
|
||||
)
|
||||
result.enableNodeDatabase = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
if(json["doCount"] !== undefined){
|
||||
if (json["doCount"] !== undefined) {
|
||||
context.err("Detected 'doCount'. did you mean: isCounted ?")
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,11 @@ export class PrevalidateLayer extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
if(this._isBuiltin && json.allowMove === undefined && json.source["geoJson"] === undefined) {
|
||||
if (
|
||||
this._isBuiltin &&
|
||||
json.allowMove === undefined &&
|
||||
json.source["geoJson"] === undefined
|
||||
) {
|
||||
if (!Constants.priviliged_layers.find((x) => x == json.id)) {
|
||||
context.err("Layer " + json.id + " does not have an explicit 'allowMove'")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
|
|||
super(
|
||||
"Checks that the given object is fully translated in the specified languages",
|
||||
[],
|
||||
"ValidateLanguageCompleteness",
|
||||
"ValidateLanguageCompleteness"
|
||||
)
|
||||
this._languages = languages ?? ["en"]
|
||||
}
|
||||
|
|
@ -44,18 +44,18 @@ export class ValidateLanguageCompleteness extends DesugaringStep<ThemeConfig> {
|
|||
.filter(
|
||||
(t) =>
|
||||
t.tr.translations[neededLanguage] === undefined &&
|
||||
t.tr.translations["*"] === undefined,
|
||||
t.tr.translations["*"] === undefined
|
||||
)
|
||||
.forEach((missing) => {
|
||||
context
|
||||
.enter(missing.context.split("."))
|
||||
.err(
|
||||
`The theme ${obj.id} should be translation-complete for ` +
|
||||
neededLanguage +
|
||||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en"),
|
||||
neededLanguage +
|
||||
", but it lacks a translation for " +
|
||||
missing.context +
|
||||
".\n\tThe known translation is " +
|
||||
missing.tr.textFor("en")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
constructor(
|
||||
knownImagePaths: Set<string>,
|
||||
checkExistsSync: (path: string) => boolean = undefined,
|
||||
ignore?: Set<string>,
|
||||
ignore?: Set<string>
|
||||
) {
|
||||
super("Checks if an image exists", [], "DoesImageExist")
|
||||
this._ignore = ignore
|
||||
|
|
@ -112,15 +112,15 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
if (!this._knownImagePaths.has(image)) {
|
||||
if (this.doesPathExist === undefined) {
|
||||
context.err(
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`,
|
||||
`Image with path ${image} not found or not attributed; it is used in ${context}`
|
||||
)
|
||||
} else if (!this.doesPathExist(image)) {
|
||||
context.err(
|
||||
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`,
|
||||
`Image with path ${image} does not exist.\n Check for typo's and missing directories in the path.`
|
||||
)
|
||||
} else {
|
||||
context.err(
|
||||
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`,
|
||||
`Image with path ${image} is not attributed (but it exists); execute 'npm run query:licenses' to add the license information and/or run 'npm run generate:licenses' to compile all the license info`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,7 @@ class OverrideShadowingCheck extends DesugaringStep<ThemeConfigJson> {
|
|||
super(
|
||||
"Checks that an 'overrideAll' does not override a single override",
|
||||
[],
|
||||
"OverrideShadowingCheck",
|
||||
"OverrideShadowingCheck"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ class MiscThemeChecks extends DesugaringStep<ThemeConfigJson> {
|
|||
context
|
||||
.enter("layers")
|
||||
.err(
|
||||
"The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?",
|
||||
"The 'layers'-field should be an array, but it is not. Did you pase a layer identifier and forget to add the '[' and ']'?"
|
||||
)
|
||||
}
|
||||
if (json.socialImage === "") {
|
||||
|
|
@ -217,23 +217,37 @@ class MiscThemeChecks extends DesugaringStep<ThemeConfigJson> {
|
|||
context
|
||||
.enter("overideAll")
|
||||
.err(
|
||||
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them.",
|
||||
"'overrideAll' is spelled with _two_ `r`s. You only wrote a single one of them."
|
||||
)
|
||||
}
|
||||
|
||||
if (json.defaultBackgroundId
|
||||
&& ![AvailableRasterLayers.osmCartoProperties.id, ...eliCategory ]
|
||||
.find(l => l === json.defaultBackgroundId) ) {
|
||||
if (
|
||||
json.defaultBackgroundId &&
|
||||
![AvailableRasterLayers.osmCartoProperties.id, ...eliCategory].find(
|
||||
(l) => l === json.defaultBackgroundId
|
||||
)
|
||||
) {
|
||||
const background = json.defaultBackgroundId
|
||||
const match = AvailableRasterLayers.globalLayers.find(l => l.properties.id === background)
|
||||
const match = AvailableRasterLayers.globalLayers.find(
|
||||
(l) => l.properties.id === background
|
||||
)
|
||||
if (!match) {
|
||||
const suggestions = Utils.sortedByLevenshteinDistance(background,
|
||||
AvailableRasterLayers.globalLayers, l => l.properties.id)
|
||||
context.enter("defaultBackgroundId")
|
||||
.warn("The default background layer with id", background, "does not exist or is not a global layer. Perhaps you meant one of:",
|
||||
suggestions.slice(0, 5).map(l => l.properties.id).join(", "),
|
||||
"If you want to use a certain category of background image, use", AvailableRasterLayers.globalLayers.join(", ")
|
||||
)
|
||||
const suggestions = Utils.sortedByLevenshteinDistance(
|
||||
background,
|
||||
AvailableRasterLayers.globalLayers,
|
||||
(l) => l.properties.id
|
||||
)
|
||||
context.enter("defaultBackgroundId").warn(
|
||||
"The default background layer with id",
|
||||
background,
|
||||
"does not exist or is not a global layer. Perhaps you meant one of:",
|
||||
suggestions
|
||||
.slice(0, 5)
|
||||
.map((l) => l.properties.id)
|
||||
.join(", "),
|
||||
"If you want to use a certain category of background image, use",
|
||||
AvailableRasterLayers.globalLayers.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
return json
|
||||
|
|
@ -245,7 +259,7 @@ export class PrevalidateTheme extends Fuse<ThemeConfigJson> {
|
|||
super(
|
||||
"Various consistency checks on the raw JSON",
|
||||
new MiscThemeChecks(),
|
||||
new OverrideShadowingCheck(),
|
||||
new OverrideShadowingCheck()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -255,7 +269,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
super(
|
||||
"The `if`-part in a mapping might set some keys. Those keys are not allowed to be set in the `addExtraTags`, as this might result in conflicting values",
|
||||
[],
|
||||
"DetectConflictingAddExtraTags",
|
||||
"DetectConflictingAddExtraTags"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +296,7 @@ export class DetectConflictingAddExtraTags extends DesugaringStep<TagRenderingCo
|
|||
.enters("mappings", i)
|
||||
.err(
|
||||
"AddExtraTags overrides a key that is set in the `if`-clause of this mapping. Selecting this answer might thus first set one value (needed to match as answer) and then override it with a different value, resulting in an unsaveable question. The offending `addExtraTags` is " +
|
||||
duplicateKeys.join(", "),
|
||||
duplicateKeys.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -300,13 +314,13 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
super(
|
||||
"A tagRendering might set a freeform key (e.g. `name` and have an option that _should_ erase this name, e.g. `noname=yes`). Under normal circumstances, every mapping/freeform should affect all touched keys",
|
||||
[],
|
||||
"DetectNonErasedKeysInMappings",
|
||||
"DetectNonErasedKeysInMappings"
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: QuestionableTagRenderingConfigJson,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): QuestionableTagRenderingConfigJson {
|
||||
if (json.multiAnswer) {
|
||||
// No need to check this here, this has its own validation
|
||||
|
|
@ -360,8 +374,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
.enters("freeform")
|
||||
.warn(
|
||||
"The freeform block does not modify the key `" +
|
||||
neededKey +
|
||||
"` which is set in a mapping. Use `addExtraTags` to overwrite it",
|
||||
neededKey +
|
||||
"` which is set in a mapping. Use `addExtraTags` to overwrite it"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -379,8 +393,8 @@ export class DetectNonErasedKeysInMappings extends DesugaringStep<QuestionableTa
|
|||
.enters("mappings", i)
|
||||
.warn(
|
||||
"This mapping does not modify the key `" +
|
||||
neededKey +
|
||||
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it",
|
||||
neededKey +
|
||||
"` which is set in a mapping or by the freeform block. Use `addExtraTags` to overwrite it"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +411,7 @@ export class DetectMappingsShadowedByCondition extends DesugaringStep<TagRenderi
|
|||
super(
|
||||
"Checks that, if the tagrendering has a condition, that a mapping is not contradictory to it, i.e. that there are no dead mappings",
|
||||
[],
|
||||
"DetectMappingsShadowedByCondition",
|
||||
"DetectMappingsShadowedByCondition"
|
||||
)
|
||||
this._forceError = forceError
|
||||
}
|
||||
|
|
@ -469,7 +483,7 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
* DetectShadowedMappings.extractCalculatedTagNames({calculatedTags: ["_abc=js()"]}) // => ["_abc"]
|
||||
*/
|
||||
private static extractCalculatedTagNames(
|
||||
layerConfig?: LayerConfigJson | { calculatedTags: string[] },
|
||||
layerConfig?: LayerConfigJson | { calculatedTags: string[] }
|
||||
) {
|
||||
return (
|
||||
layerConfig?.calculatedTags?.map((ct) => {
|
||||
|
|
@ -555,16 +569,16 @@ export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJso
|
|||
json.mappings[i]["hideInAnswer"] !== true
|
||||
) {
|
||||
context.warn(
|
||||
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`,
|
||||
`Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
|
||||
)
|
||||
} else if (doesMatch) {
|
||||
// The current mapping is shadowed!
|
||||
context.err(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown:
|
||||
The mapping ${parsedConditions[i].asHumanString(
|
||||
false,
|
||||
false,
|
||||
{},
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
false,
|
||||
false,
|
||||
{}
|
||||
)} is fully matched by a previous mapping (namely ${j}), which matches:
|
||||
${parsedConditions[j].asHumanString(false, false, {})}.
|
||||
|
||||
To fix this problem, you can try to:
|
||||
|
|
@ -589,7 +603,7 @@ export class ValidatePossibleLinks extends DesugaringStep<string | Record<string
|
|||
super(
|
||||
"Given a possible set of translations, validates that <a href=... target='_blank'> does have `rel='noopener'` set",
|
||||
[],
|
||||
"ValidatePossibleLinks",
|
||||
"ValidatePossibleLinks"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -619,21 +633,21 @@ export class ValidatePossibleLinks extends DesugaringStep<string | Record<string
|
|||
|
||||
convert(
|
||||
json: string | Record<string, string>,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): string | Record<string, string> {
|
||||
if (typeof json === "string") {
|
||||
if (this.isTabnabbingProne(json)) {
|
||||
context.err(
|
||||
"The string " +
|
||||
json +
|
||||
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping",
|
||||
json +
|
||||
" has a link targeting `_blank`, but it doesn't have `rel='noopener'` set. This gives rise to reverse tabnapping"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for (const k in json) {
|
||||
if (this.isTabnabbingProne(json[k])) {
|
||||
context.err(
|
||||
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`,
|
||||
`The translation for ${k} '${json[k]}' has a link targeting \`_blank\`, but it doesn't have \`rel='noopener'\` set. This gives rise to reverse tabnapping`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -651,7 +665,7 @@ export class CheckTranslation extends DesugaringStep<Translatable> {
|
|||
super(
|
||||
"Checks that a translation is valid and internally consistent",
|
||||
["*"],
|
||||
"CheckTranslation",
|
||||
"CheckTranslation"
|
||||
)
|
||||
this._allowUndefined = allowUndefined
|
||||
}
|
||||
|
|
@ -698,7 +712,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
|||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Thin wrapper around 'ValidateLayer", [], "ValidateLayerConfig")
|
||||
this.validator = new ValidateLayer(
|
||||
|
|
@ -706,7 +720,7 @@ export class ValidateLayerConfig extends DesugaringStep<LayerConfigJson> {
|
|||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations,
|
||||
skipDefaultLayers,
|
||||
skipDefaultLayers
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -734,7 +748,7 @@ export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJ
|
|||
context
|
||||
.enter("markers")
|
||||
.err(
|
||||
`Detected a field 'markerS' in pointRendering. It is written as a singular case`,
|
||||
`Detected a field 'markerS' in pointRendering. It is written as a singular case`
|
||||
)
|
||||
}
|
||||
if (json.marker && !Array.isArray(json.marker)) {
|
||||
|
|
@ -744,7 +758,7 @@ export class ValidatePointRendering extends DesugaringStep<PointRenderingConfigJ
|
|||
context
|
||||
.enter("location")
|
||||
.err(
|
||||
"A pointRendering should have at least one 'location' to defined where it should be rendered. ",
|
||||
"A pointRendering should have at least one 'location' to defined where it should be rendered. "
|
||||
)
|
||||
}
|
||||
return json
|
||||
|
|
@ -763,26 +777,26 @@ export class ValidateLayer extends Conversion<
|
|||
isBuiltin: boolean,
|
||||
doesImageExist: DoesImageExist,
|
||||
studioValidations: boolean = false,
|
||||
skipDefaultLayers: boolean = false,
|
||||
skipDefaultLayers: boolean = false
|
||||
) {
|
||||
super("Doesn't change anything, but emits warnings and errors", [], "ValidateLayer")
|
||||
this._prevalidation = new PrevalidateLayer(
|
||||
path,
|
||||
isBuiltin,
|
||||
doesImageExist,
|
||||
studioValidations,
|
||||
studioValidations
|
||||
)
|
||||
this._skipDefaultLayers = skipDefaultLayers
|
||||
}
|
||||
|
||||
convert(
|
||||
json: LayerConfigJson,
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): { parsed: LayerConfig; raw: LayerConfigJson } {
|
||||
context = context.inOperation(this.name)
|
||||
if (typeof json === "string") {
|
||||
context.err(
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`,
|
||||
`Not a valid layer: the layerConfig is a string. 'npm run generate:layeroverview' might be needed`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
|
@ -814,7 +828,7 @@ export class ValidateLayer extends Conversion<
|
|||
context
|
||||
.enters("calculatedTags", i)
|
||||
.err(
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`,
|
||||
`Invalid function definition: the custom javascript is invalid:${e}. The offending javascript code is:\n ${code}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -862,7 +876,7 @@ export class ValidateLayer extends Conversion<
|
|||
context
|
||||
.enters("allowMove", "enableAccuracy")
|
||||
.err(
|
||||
"`enableAccuracy` is written with two C in the first occurrence and only one in the last",
|
||||
"`enableAccuracy` is written with two C in the first occurrence and only one in the last"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -893,8 +907,8 @@ export class ValidateFilter extends DesugaringStep<FilterConfigJson> {
|
|||
.enters("fields", i)
|
||||
.err(
|
||||
`Invalid filter: ${type} is not a valid textfield type.\n\tTry one of ${Array.from(
|
||||
Validators.availableTypes,
|
||||
).join(",")}`,
|
||||
Validators.availableTypes
|
||||
).join(",")}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -911,13 +925,13 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
super(
|
||||
"Tries to detect layers where a shared filter can be used (or where similar filters occur)",
|
||||
[],
|
||||
"DetectDuplicateFilters",
|
||||
"DetectDuplicateFilters"
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: { layers: LayerConfigJson[]; themes: ThemeConfigJson[] },
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): { layers: LayerConfigJson[]; themes: ThemeConfigJson[] } {
|
||||
const { layers, themes } = json
|
||||
const perOsmTag = new Map<
|
||||
|
|
@ -981,7 +995,7 @@ export class DetectDuplicateFilters extends DesugaringStep<{
|
|||
filter: FilterConfigJson
|
||||
}[]
|
||||
>,
|
||||
theme?: ThemeConfigJson | undefined,
|
||||
theme?: ThemeConfigJson | undefined
|
||||
): void {
|
||||
if (layer.filter === undefined || layer.filter === null) {
|
||||
return
|
||||
|
|
@ -1021,7 +1035,7 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
|
|||
super(
|
||||
"Detects mappings which have identical (english) names or identical mappings.",
|
||||
["presets"],
|
||||
"DetectDuplicatePresets",
|
||||
"DetectDuplicatePresets"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1032,13 +1046,13 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
|
|||
if (new Set(enNames).size != enNames.length) {
|
||||
const dups = Utils.Duplicates(enNames)
|
||||
const layersWithDup = json.layers.filter((l) =>
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0),
|
||||
l.presets.some((p) => dups.indexOf(p.title.textFor("en")) >= 0)
|
||||
)
|
||||
const layerIds = layersWithDup.map((l) => l.id)
|
||||
context.err(
|
||||
`This theme has multiple presets which are named:${dups}, namely layers ${layerIds.join(
|
||||
", ",
|
||||
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`,
|
||||
", "
|
||||
)} this is confusing for contributors and is probably the result of reusing the same layer multiple times. Use \`{"override": {"=presets": []}}\` to remove some presets`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1053,17 +1067,17 @@ export class DetectDuplicatePresets extends DesugaringStep<ThemeConfig> {
|
|||
Utils.SameObject(presetATags, presetBTags) &&
|
||||
Utils.sameList(
|
||||
presetA.preciseInput.snapToLayers,
|
||||
presetB.preciseInput.snapToLayers,
|
||||
presetB.preciseInput.snapToLayers
|
||||
)
|
||||
) {
|
||||
context.err(
|
||||
`This theme has multiple presets with the same tags: ${presetATags.asHumanString(
|
||||
false,
|
||||
false,
|
||||
{},
|
||||
{}
|
||||
)}, namely the preset '${presets[i].title.textFor("en")}' and '${presets[
|
||||
j
|
||||
].title.textFor("en")}'`,
|
||||
].title.textFor("en")}'`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1088,13 +1102,13 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
super(
|
||||
"Validates that all themes together are logical, i.e. no duplicate ids exists within (overriden) themes",
|
||||
[],
|
||||
"ValidateThemeEnsemble",
|
||||
"ValidateThemeEnsemble"
|
||||
)
|
||||
}
|
||||
|
||||
convert(
|
||||
json: ThemeConfig[],
|
||||
context: ConversionContext,
|
||||
context: ConversionContext
|
||||
): Map<
|
||||
string,
|
||||
{
|
||||
|
|
@ -1145,11 +1159,11 @@ export class ValidateThemeEnsemble extends Conversion<
|
|||
context.err(
|
||||
[
|
||||
"The layer with id '" +
|
||||
id +
|
||||
"' is found in multiple themes with different tag definitions:",
|
||||
id +
|
||||
"' is found in multiple themes with different tag definitions:",
|
||||
"\t In theme " + oldTheme + ":\t" + oldTags.asHumanString(false, false, {}),
|
||||
"\tIn theme " + theme.id + ":\t" + tags.asHumanString(false, false, {}),
|
||||
].join("\n"),
|
||||
].join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,9 +33,20 @@ export default class DependencyCalculator {
|
|||
*/
|
||||
public static getLayerDependencies(
|
||||
layer: LayerConfig
|
||||
): { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] {
|
||||
const deps: { neededLayer: string; reason: string; context?: string; neededBy: string, checkHasSnapName: boolean }[] =
|
||||
[]
|
||||
): {
|
||||
neededLayer: string
|
||||
reason: string
|
||||
context?: string
|
||||
neededBy: string
|
||||
checkHasSnapName: boolean
|
||||
}[] {
|
||||
const deps: {
|
||||
neededLayer: string
|
||||
reason: string
|
||||
context?: string
|
||||
neededBy: string
|
||||
checkHasSnapName: boolean
|
||||
}[] = []
|
||||
|
||||
for (let i = 0; layer.presets !== undefined && i < layer.presets.length; i++) {
|
||||
const preset = layer.presets[i]
|
||||
|
|
@ -51,7 +62,7 @@ export default class DependencyCalculator {
|
|||
reason: `preset \`${preset.title.textFor("en")}\` snaps to this layer`,
|
||||
context: `${layer.id}.presets[${i}]`,
|
||||
neededBy: layer.id,
|
||||
checkHasSnapName: true
|
||||
checkHasSnapName: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -63,7 +74,7 @@ export default class DependencyCalculator {
|
|||
reason: "a tagrendering needs this layer",
|
||||
context: tr.id,
|
||||
neededBy: layer.id,
|
||||
checkHasSnapName: false
|
||||
checkHasSnapName: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +110,7 @@ export default class DependencyCalculator {
|
|||
"] which calculates the value for " +
|
||||
currentKey,
|
||||
neededBy: layer.id,
|
||||
checkHasSnapName: false
|
||||
checkHasSnapName: false,
|
||||
})
|
||||
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -59,20 +59,30 @@ export default class FilterConfig {
|
|||
throw `Invalid filter: no question given at ${ctx}`
|
||||
}
|
||||
|
||||
const fields: { name: string; type: ValidatorType }[] = (option.fields ?? []).map((f, i) => {
|
||||
const type = <ValidatorType> f.type ?? "regex"
|
||||
if(Validators.availableTypes.indexOf(type) < 0){
|
||||
throw `Invalid filter: type is not a valid validator. Did you mean one of ${Utils.sortedByLevenshteinDistance(type, <ReadonlyArray<string>>Validators.availableTypes, x => x).slice(0, 3)}`
|
||||
const fields: { name: string; type: ValidatorType }[] = (option.fields ?? []).map(
|
||||
(f, i) => {
|
||||
const type = <ValidatorType>f.type ?? "regex"
|
||||
if (Validators.availableTypes.indexOf(type) < 0) {
|
||||
throw `Invalid filter: type is not a valid validator. Did you mean one of ${Utils.sortedByLevenshteinDistance(
|
||||
type,
|
||||
<ReadonlyArray<string>>Validators.availableTypes,
|
||||
(x) => x
|
||||
).slice(0, 3)}`
|
||||
}
|
||||
// Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig
|
||||
if (
|
||||
f.name === undefined ||
|
||||
f.name === "" ||
|
||||
f.name.match(/[a-z0-9_-]+/) == null
|
||||
) {
|
||||
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
|
||||
}
|
||||
return {
|
||||
name: f.name,
|
||||
type,
|
||||
}
|
||||
}
|
||||
// Type is validated against 'ValidatedTextField' in Validation.ts, in ValidateFilterConfig
|
||||
if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) {
|
||||
throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]`
|
||||
}
|
||||
return {
|
||||
name: f.name,
|
||||
type
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
for (const field of fields) {
|
||||
for (const ln in question.translations) {
|
||||
|
|
@ -226,7 +236,7 @@ export default class FilterConfig {
|
|||
opt.osmTags?.asHumanString() ?? "",
|
||||
opt.fields?.length > 0
|
||||
? opt.fields.map((f) => f.name + " (" + f.type + ")").join(" ")
|
||||
: undefined
|
||||
: undefined,
|
||||
])
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,12 +85,12 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
|
||||
this.syncSelection = json.syncSelection ?? "no"
|
||||
if(!json.source) {
|
||||
if(json.presets === undefined){
|
||||
throw "Error while parsing "+json.id+" in "+context+"; no source given"
|
||||
if (!json.source) {
|
||||
if (json.presets === undefined) {
|
||||
throw "Error while parsing " + json.id + " in " + context + "; no source given"
|
||||
}
|
||||
this.source = new SourceConfig({
|
||||
osmTags: TagUtils.Tag({or: json.presets.map(pr => ({and:pr.tags}))}),
|
||||
osmTags: TagUtils.Tag({ or: json.presets.map((pr) => ({ and: pr.tags })) }),
|
||||
})
|
||||
} else if (typeof json.source !== "string") {
|
||||
this.maxAgeOfCache = json.source["maxCacheAge"] ?? 24 * 60 * 60 * 30
|
||||
|
|
@ -104,7 +104,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
mercatorCrs: json.source["mercatorCrs"],
|
||||
idKey: json.source["idKey"],
|
||||
},
|
||||
json.id,
|
||||
json.id
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (json.calculatedTags !== undefined) {
|
||||
if (!official) {
|
||||
console.warn(
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`,
|
||||
`Unofficial theme ${this.id} with custom javascript! This is a security risk`
|
||||
)
|
||||
}
|
||||
this.calculatedTags = []
|
||||
|
|
@ -194,7 +194,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
tags: pr.tags.map((t) => TagUtils.SimpleTag(t)),
|
||||
description: Translations.T(
|
||||
pr.description,
|
||||
`${translationContext}.presets.${i}.description`,
|
||||
`${translationContext}.presets.${i}.description`
|
||||
),
|
||||
preciseInput: preciseInput,
|
||||
exampleImages: pr.exampleImages,
|
||||
|
|
@ -208,7 +208,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
if (json.lineRendering) {
|
||||
this.lineRendering = Utils.NoNull(json.lineRendering).map(
|
||||
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`),
|
||||
(r, i) => new LineRenderingConfig(r, `${context}[${i}]`)
|
||||
)
|
||||
} else {
|
||||
this.lineRendering = []
|
||||
|
|
@ -216,7 +216,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
if (json.pointRendering) {
|
||||
this.mapRendering = Utils.NoNull(json.pointRendering).map(
|
||||
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`),
|
||||
(r, i) => new PointRenderingConfig(r, `${context}[${i}](${this.id})`)
|
||||
)
|
||||
} else {
|
||||
this.mapRendering = []
|
||||
|
|
@ -228,7 +228,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
r.location.has("centroid") ||
|
||||
r.location.has("projected_centerpoint") ||
|
||||
r.location.has("start") ||
|
||||
r.location.has("end"),
|
||||
r.location.has("end")
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
@ -250,7 +250,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
Constants.priviliged_layers.indexOf(<any>this.id) < 0 &&
|
||||
this.source !== null /*library layer*/ &&
|
||||
!this.source?.geojsonSource?.startsWith(
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json",
|
||||
"https://api.openstreetmap.org/api/0.6/notes.json"
|
||||
)
|
||||
) {
|
||||
throw (
|
||||
|
|
@ -269,7 +269,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
typeof tr !== "string" &&
|
||||
tr["builtin"] === undefined &&
|
||||
tr["id"] === undefined &&
|
||||
tr["rewrite"] === undefined,
|
||||
tr["rewrite"] === undefined
|
||||
) ?? []
|
||||
if (missingIds?.length > 0 && official) {
|
||||
console.error("Some tagRenderings of", this.id, "are missing an id:", missingIds)
|
||||
|
|
@ -280,8 +280,8 @@ export default class LayerConfig extends WithContextLoader {
|
|||
(tr, i) =>
|
||||
new TagRenderingConfig(
|
||||
<QuestionableTagRenderingConfigJson>tr,
|
||||
this.id + ".tagRenderings[" + i + "]",
|
||||
),
|
||||
this.id + ".tagRenderings[" + i + "]"
|
||||
)
|
||||
)
|
||||
if (json.units !== undefined && !Array.isArray(json.units)) {
|
||||
throw (
|
||||
|
|
@ -291,7 +291,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
)
|
||||
}
|
||||
this.units = (json.units ?? []).flatMap((unitJson, i) =>
|
||||
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`),
|
||||
Unit.fromJson(unitJson, this.tagRenderings, `${context}.unit[${i}]`)
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
@ -362,14 +362,18 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (mapRenderings.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return new Combine(mapRenderings.map(
|
||||
mr => mr.GetBaseIcon(properties ?? this.GetBaseTags()).SetClass("absolute left-0 top-0 w-full h-full"))
|
||||
return new Combine(
|
||||
mapRenderings.map((mr) =>
|
||||
mr
|
||||
.GetBaseIcon(properties ?? this.GetBaseTags())
|
||||
.SetClass("absolute left-0 top-0 w-full h-full")
|
||||
)
|
||||
).SetClass("relative block w-full h-full")
|
||||
}
|
||||
|
||||
public GetBaseTags(): Record<string, string> {
|
||||
return TagUtils.changeAsProperties(
|
||||
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }],
|
||||
this.source?.osmTags?.asChange({ id: "node/-1" }) ?? [{ k: "id", v: "node/-1" }]
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +386,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
neededLayer: string
|
||||
}[] = [],
|
||||
addedByDefault = false,
|
||||
canBeIncluded = true,
|
||||
canBeIncluded = true
|
||||
): string {
|
||||
const extraProps: string[] = []
|
||||
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
|
||||
|
|
@ -390,32 +394,32 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (canBeIncluded) {
|
||||
if (addedByDefault) {
|
||||
extraProps.push(
|
||||
"**This layer is included automatically in every theme. This layer might contain no points**",
|
||||
"**This layer is included automatically in every theme. This layer might contain no points**"
|
||||
)
|
||||
}
|
||||
if (this.shownByDefault === false) {
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and must be enabled in the filter by the user. ",
|
||||
"This layer is not visible by default and must be enabled in the filter by the user. "
|
||||
)
|
||||
}
|
||||
if (this.title === undefined) {
|
||||
extraProps.push(
|
||||
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable.",
|
||||
"Elements don't have a title set and cannot be toggled nor will they show up in the dashboard. If you import this layer in your theme, override `title` to make this toggleable."
|
||||
)
|
||||
}
|
||||
if (this.name === undefined && this.shownByDefault === false) {
|
||||
extraProps.push(
|
||||
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true",
|
||||
"This layer is not visible by default and the visibility cannot be toggled, effectively resulting in a fully hidden layer. This can be useful, e.g. to calculate some metatags. If you want to render this layer (e.g. for debugging), enable it by setting the URL-parameter layer-<id>=true"
|
||||
)
|
||||
}
|
||||
if (this.name === undefined) {
|
||||
extraProps.push(
|
||||
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`",
|
||||
"Not visible in the layer selection by default. If you want to make this layer toggable, override `name`"
|
||||
)
|
||||
}
|
||||
if (this.mapRendering.length === 0) {
|
||||
extraProps.push(
|
||||
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`",
|
||||
"Not rendered on the map by default. If you want to rendering this on the map, override `mapRenderings`"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -425,12 +429,12 @@ export default class LayerConfig extends WithContextLoader {
|
|||
"<img src='../warning.svg' height='1rem'/>",
|
||||
"This layer is loaded from an external source, namely ",
|
||||
"`" + this.source.geojsonSource + "`",
|
||||
].join("\n\n"),
|
||||
].join("\n\n")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
extraProps.push(
|
||||
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data.",
|
||||
"This layer can **not** be included in a theme. It is solely used by [special renderings](SpecialRenderings.md) showing a minimap with custom data."
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -440,7 +444,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
usingLayer = [
|
||||
"## Themes using this layer",
|
||||
MarkdownUtils.list(
|
||||
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`),
|
||||
(usedInThemes ?? []).map((id) => `[${id}](https://mapcomplete.org/${id})`)
|
||||
),
|
||||
]
|
||||
} else if (this.source !== null) {
|
||||
|
|
@ -456,31 +460,43 @@ export default class LayerConfig extends WithContextLoader {
|
|||
" into the layout as it depends on it: ",
|
||||
dep.reason,
|
||||
"(" + dep.context + ")",
|
||||
].join(" "),
|
||||
].join(" ")
|
||||
)
|
||||
}
|
||||
|
||||
let presets: string[] = []
|
||||
if (this.presets.length > 0) {
|
||||
|
||||
presets = [
|
||||
"## Presets",
|
||||
"The following options to create new points are included:",
|
||||
MarkdownUtils.list(this.presets.map(preset => {
|
||||
let snaps = ""
|
||||
if (preset.preciseInput?.snapToLayers) {
|
||||
snaps = " (snaps to layers " + preset.preciseInput.snapToLayers.map(id => `\`${id}\``).join(", ") + ")"
|
||||
}
|
||||
return "**" + preset.title.txt + "** which has the following tags:" + new And(preset.tags).asHumanString(true) + snaps
|
||||
})),
|
||||
MarkdownUtils.list(
|
||||
this.presets.map((preset) => {
|
||||
let snaps = ""
|
||||
if (preset.preciseInput?.snapToLayers) {
|
||||
snaps =
|
||||
" (snaps to layers " +
|
||||
preset.preciseInput.snapToLayers
|
||||
.map((id) => `\`${id}\``)
|
||||
.join(", ") +
|
||||
")"
|
||||
}
|
||||
return (
|
||||
"**" +
|
||||
preset.title.txt +
|
||||
"** which has the following tags:" +
|
||||
new And(preset.tags).asHumanString(true) +
|
||||
snaps
|
||||
)
|
||||
})
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
for (const revDep of Utils.Dedup(layerIsNeededBy?.get(this.id) ?? [])) {
|
||||
extraProps.push(
|
||||
["This layer is needed as dependency for layer", `[${revDep}](#${revDep})`].join(
|
||||
" ",
|
||||
),
|
||||
" "
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -491,10 +507,10 @@ export default class LayerConfig extends WithContextLoader {
|
|||
.filter((values) => values.key !== "id")
|
||||
.map((values) => {
|
||||
const embedded: string[] = values.values?.map((v) =>
|
||||
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown(),
|
||||
Link.OsmWiki(values.key, v, true).SetClass("mr-2").AsMarkdown()
|
||||
) ?? ["_no preset options defined, or no values in them_"]
|
||||
const statistics = `https://taghistory.raifer.tech/?#***/${encodeURIComponent(
|
||||
values.key,
|
||||
values.key
|
||||
)}/`
|
||||
const tagInfo = `https://taginfo.openstreetmap.org/keys/${values.key}#values`
|
||||
return [
|
||||
|
|
@ -509,7 +525,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
: `[${values.type}](../SpecialInputElements.md#${values.type})`,
|
||||
embedded.join(" "),
|
||||
]
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
let quickOverview: string[] = []
|
||||
|
|
@ -519,7 +535,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
"this quick overview is incomplete",
|
||||
MarkdownUtils.table(
|
||||
["attribute", "type", "values which are supported by this layer"],
|
||||
tableRows,
|
||||
tableRows
|
||||
),
|
||||
]
|
||||
}
|
||||
|
|
@ -553,19 +569,19 @@ export default class LayerConfig extends WithContextLoader {
|
|||
const parts = neededTags["and"]
|
||||
tagsDescription.push(
|
||||
"Elements must match **all** of the following expressions:",
|
||||
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n"),
|
||||
parts.map((p, i) => i + ". " + p.asHumanString(true, false, {})).join("\n")
|
||||
)
|
||||
} else if (neededTags["or"]) {
|
||||
const parts = neededTags["or"]
|
||||
tagsDescription.push(
|
||||
"Elements must match **any** of the following expressions:",
|
||||
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n"),
|
||||
parts.map((p) => " - " + p.asHumanString(true, false, {})).join("\n")
|
||||
)
|
||||
} else {
|
||||
tagsDescription.push(
|
||||
"Elements must match the expression **" +
|
||||
neededTags.asHumanString(true, false, {}) +
|
||||
"**",
|
||||
neededTags.asHumanString(true, false, {}) +
|
||||
"**"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -616,8 +632,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (!presets) {
|
||||
return undefined
|
||||
}
|
||||
const matchingPresets = presets
|
||||
.filter((pr) => new And(pr.tags).matchesProperties(tags))
|
||||
const matchingPresets = presets.filter((pr) => new And(pr.tags).matchesProperties(tags))
|
||||
let mostShadowed = matchingPresets[0]
|
||||
let mostShadowedTags = new And(mostShadowed.tags)
|
||||
for (let i = 1; i < matchingPresets.length; i++) {
|
||||
|
|
@ -646,18 +661,18 @@ export default class LayerConfig extends WithContextLoader {
|
|||
* Indicates if this is a normal layer, meaning that it can be toggled by the user in normal circumstances
|
||||
* Thus: name is set, not a note import layer, not synced with another filter, ...
|
||||
*/
|
||||
public isNormal(){
|
||||
if(this.id.startsWith("note_import")){
|
||||
public isNormal() {
|
||||
if (this.id.startsWith("note_import")) {
|
||||
return false
|
||||
}
|
||||
|
||||
if(Constants.added_by_default.indexOf(<any> this.id) >=0){
|
||||
if (Constants.added_by_default.indexOf(<any>this.id) >= 0) {
|
||||
return false
|
||||
}
|
||||
if(this.filterIsSameAs !== undefined){
|
||||
if (this.filterIsSameAs !== undefined) {
|
||||
return false
|
||||
}
|
||||
if(!this.name ){
|
||||
if (!this.name) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default class TagRenderingConfig {
|
|||
| string
|
||||
| TagRenderingConfigJson
|
||||
| (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
|
||||
context?: string,
|
||||
context?: string
|
||||
) {
|
||||
let json = <string | QuestionableTagRenderingConfigJson>config
|
||||
if (json === undefined) {
|
||||
|
|
@ -144,7 +144,7 @@ export default class TagRenderingConfig {
|
|||
this.description = Translations.T(json.description, translationKey + ".description")
|
||||
this.editButtonAriaLabel = Translations.T(
|
||||
json.editButtonAriaLabel,
|
||||
translationKey + ".editButtonAriaLabel",
|
||||
translationKey + ".editButtonAriaLabel"
|
||||
)
|
||||
|
||||
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
|
||||
|
|
@ -160,7 +160,7 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
this.metacondition = TagUtils.Tag(
|
||||
json.metacondition ?? { and: [] },
|
||||
`${context}.metacondition`,
|
||||
`${context}.metacondition`
|
||||
)
|
||||
if (json.freeform) {
|
||||
if (
|
||||
|
|
@ -178,7 +178,7 @@ export default class TagRenderingConfig {
|
|||
}, perhaps you meant ${Utils.sortedByLevenshteinDistance(
|
||||
json.freeform.key,
|
||||
<any>Validators.availableTypes,
|
||||
(s) => <any>s,
|
||||
(s) => <any>s
|
||||
)}`
|
||||
}
|
||||
const type: ValidatorType = <any>json.freeform.type ?? "string"
|
||||
|
|
@ -200,7 +200,7 @@ export default class TagRenderingConfig {
|
|||
placeholder,
|
||||
addExtraTags:
|
||||
json.freeform.addExtraTags?.map((tg, i) =>
|
||||
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`),
|
||||
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
|
||||
) ?? [],
|
||||
inline: json.freeform.inline ?? false,
|
||||
default: json.freeform.default,
|
||||
|
|
@ -266,8 +266,8 @@ export default class TagRenderingConfig {
|
|||
context,
|
||||
this.multiAnswer,
|
||||
this.question !== undefined,
|
||||
commonIconSize,
|
||||
),
|
||||
commonIconSize
|
||||
)
|
||||
)
|
||||
} else {
|
||||
this.mappings = []
|
||||
|
|
@ -293,7 +293,7 @@ export default class TagRenderingConfig {
|
|||
for (const expectedKey of keys) {
|
||||
if (usedKeys.indexOf(expectedKey) < 0) {
|
||||
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
|
||||
", ",
|
||||
", "
|
||||
)}, but it should also give a value for ${expectedKey}`
|
||||
this.configuration_warnings.push(msg)
|
||||
}
|
||||
|
|
@ -340,7 +340,7 @@ export default class TagRenderingConfig {
|
|||
context: string,
|
||||
multiAnswer?: boolean,
|
||||
isQuestionable?: boolean,
|
||||
commonSize: string = "small",
|
||||
commonSize: string = "small"
|
||||
): Mapping {
|
||||
const ctx = `${translationKey}.mappings.${i}`
|
||||
if (mapping.if === undefined) {
|
||||
|
|
@ -349,7 +349,7 @@ export default class TagRenderingConfig {
|
|||
if (mapping.then === undefined) {
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
|
||||
mapping,
|
||||
mapping
|
||||
)}`
|
||||
}
|
||||
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
|
||||
|
|
@ -360,7 +360,7 @@ export default class TagRenderingConfig {
|
|||
|
||||
if (mapping["render"] !== undefined) {
|
||||
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
|
||||
mapping,
|
||||
mapping
|
||||
)}`
|
||||
}
|
||||
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
|
||||
|
|
@ -383,11 +383,11 @@ export default class TagRenderingConfig {
|
|||
} else if (mapping.hideInAnswer !== undefined) {
|
||||
hideInAnswer = TagUtils.Tag(
|
||||
mapping.hideInAnswer,
|
||||
`${context}.mapping[${i}].hideInAnswer`,
|
||||
`${context}.mapping[${i}].hideInAnswer`
|
||||
)
|
||||
}
|
||||
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
|
||||
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`),
|
||||
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
|
||||
)
|
||||
if (hideInAnswer === true && addExtraTags.length > 0) {
|
||||
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
|
||||
|
|
@ -483,7 +483,7 @@ export default class TagRenderingConfig {
|
|||
* @constructor
|
||||
*/
|
||||
public GetRenderValues(
|
||||
tags: Record<string, string>,
|
||||
tags: Record<string, string>
|
||||
): { then: Translation; icon?: string; iconClass?: string }[] {
|
||||
if (!this.multiAnswer) {
|
||||
return [this.GetRenderValueWithImage(tags)]
|
||||
|
|
@ -506,7 +506,7 @@ export default class TagRenderingConfig {
|
|||
return mapping
|
||||
}
|
||||
return undefined
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
|
||||
|
|
@ -514,7 +514,7 @@ export default class TagRenderingConfig {
|
|||
applicableMappings
|
||||
?.flatMap((m) => m.if?.usedTags() ?? [])
|
||||
?.filter((kv) => kv.key === this.freeform.key)
|
||||
?.map((kv) => kv.value),
|
||||
?.map((kv) => kv.value)
|
||||
)
|
||||
|
||||
const freeformValues = tags[this.freeform.key].split(";")
|
||||
|
|
@ -523,7 +523,7 @@ export default class TagRenderingConfig {
|
|||
applicableMappings.push({
|
||||
then: new TypedTranslation<object>(
|
||||
this.render.replace("{" + this.freeform.key + "}", leftover).translations,
|
||||
this.render.context,
|
||||
this.render.context
|
||||
),
|
||||
})
|
||||
}
|
||||
|
|
@ -541,7 +541,7 @@ export default class TagRenderingConfig {
|
|||
* @constructor
|
||||
*/
|
||||
public GetRenderValueWithImage(
|
||||
tags: Record<string, string>,
|
||||
tags: Record<string, string>
|
||||
): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined {
|
||||
if (this.condition !== undefined) {
|
||||
if (!this.condition.matchesProperties(tags)) {
|
||||
|
|
@ -610,7 +610,7 @@ export default class TagRenderingConfig {
|
|||
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
|
||||
if (key === undefined) {
|
||||
const values: { k: string; v: string }[][] = Utils.NoNull(
|
||||
answerMappings?.map((m) => m.if.asChange({})) ?? [],
|
||||
answerMappings?.map((m) => m.if.asChange({})) ?? []
|
||||
)
|
||||
if (values.length === 0) {
|
||||
return
|
||||
|
|
@ -628,15 +628,15 @@ export default class TagRenderingConfig {
|
|||
return {
|
||||
key: commonKey,
|
||||
values: Utils.NoNull(
|
||||
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v),
|
||||
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let values = Utils.NoNull(
|
||||
answerMappings?.map(
|
||||
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v,
|
||||
) ?? [],
|
||||
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
|
||||
) ?? []
|
||||
)
|
||||
if (values.length === undefined) {
|
||||
values = undefined
|
||||
|
|
@ -700,7 +700,7 @@ export default class TagRenderingConfig {
|
|||
freeformValue: string | undefined,
|
||||
singleSelectedMapping: number,
|
||||
multiSelectedMapping: boolean[] | undefined,
|
||||
currentProperties: Record<string, string>,
|
||||
currentProperties: Record<string, string>
|
||||
): UploadableTag {
|
||||
if (typeof freeformValue === "string") {
|
||||
freeformValue = freeformValue?.trim()
|
||||
|
|
@ -775,7 +775,7 @@ export default class TagRenderingConfig {
|
|||
new And([
|
||||
new Tag(this.freeform.key, freeformValue),
|
||||
...(this.freeform.addExtraTags ?? []),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
|
||||
|
|
@ -845,11 +845,11 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
const msgs: string[] = [
|
||||
icon +
|
||||
" " +
|
||||
"*" +
|
||||
m.then.textFor(lang) +
|
||||
"* is shown if with " +
|
||||
m.if.asHumanString(true, false, {}),
|
||||
" " +
|
||||
"*" +
|
||||
m.then.textFor(lang) +
|
||||
"* is shown if with " +
|
||||
m.if.asHumanString(true, false, {}),
|
||||
]
|
||||
|
||||
if (m.hideInAnswer === true) {
|
||||
|
|
@ -858,11 +858,11 @@ export default class TagRenderingConfig {
|
|||
if (m.ifnot !== undefined) {
|
||||
msgs.push(
|
||||
"Unselecting this answer will add " +
|
||||
m.ifnot.asHumanString(true, false, {}),
|
||||
m.ifnot.asHumanString(true, false, {})
|
||||
)
|
||||
}
|
||||
return msgs.join(". ")
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -871,7 +871,7 @@ export default class TagRenderingConfig {
|
|||
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(
|
||||
true,
|
||||
false,
|
||||
{},
|
||||
{}
|
||||
)
|
||||
condition =
|
||||
"This tagrendering is only visible in the popup if the following condition is met: " +
|
||||
|
|
@ -905,7 +905,7 @@ export default class TagRenderingConfig {
|
|||
this.metacondition,
|
||||
this.condition,
|
||||
this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
|
||||
this.invalidValues,
|
||||
this.invalidValues
|
||||
)
|
||||
for (const m of this.mappings ?? []) {
|
||||
tags.push(m.if)
|
||||
|
|
@ -925,21 +925,26 @@ export default class TagRenderingConfig {
|
|||
* The keys that should be erased if one has to revert to 'unknown'.
|
||||
* Might give undefined if setting to unknown is not possible
|
||||
*/
|
||||
public removeToSetUnknown(partOfLayer: LayerConfig, currentTags: Record<string, string>): string[] | undefined {
|
||||
public removeToSetUnknown(
|
||||
partOfLayer: LayerConfig,
|
||||
currentTags: Record<string, string>
|
||||
): string[] | undefined {
|
||||
if (!partOfLayer?.source || !currentTags) {
|
||||
return
|
||||
}
|
||||
const toDelete = new Set<string>()
|
||||
if (this.freeform) {
|
||||
toDelete.add(this.freeform.key)
|
||||
const extraTags = new And(this.freeform.addExtraTags ?? []).usedKeys().filter(k => k !== "fixme")
|
||||
const extraTags = new And(this.freeform.addExtraTags ?? [])
|
||||
.usedKeys()
|
||||
.filter((k) => k !== "fixme")
|
||||
if (extraTags.length > 0) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
if (this.mappings?.length > 0) {
|
||||
const mainkey = this.mappings[0].if.usedKeys()
|
||||
mainkey.forEach(k => toDelete.add(k))
|
||||
mainkey.forEach((k) => toDelete.add(k))
|
||||
for (const mapping of this.mappings) {
|
||||
if (mapping.addExtraTags?.length > 0) {
|
||||
return undefined
|
||||
|
|
@ -953,7 +958,6 @@ export default class TagRenderingConfig {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
currentTags = { ...currentTags }
|
||||
for (const key of toDelete) {
|
||||
delete currentTags[key]
|
||||
|
|
@ -971,7 +975,7 @@ export class TagRenderingConfigUtils {
|
|||
public static withNameSuggestionIndex(
|
||||
config: TagRenderingConfig,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
feature?: Feature,
|
||||
feature?: Feature
|
||||
): Store<TagRenderingConfig> {
|
||||
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0
|
||||
if (!isNSI) {
|
||||
|
|
@ -989,8 +993,8 @@ export class TagRenderingConfigUtils {
|
|||
tags,
|
||||
country.split(";"),
|
||||
center,
|
||||
{ sortByFrequency: true },
|
||||
),
|
||||
{ sortByFrequency: true }
|
||||
)
|
||||
)
|
||||
})
|
||||
return extraMappings.map((extraMappings) => {
|
||||
|
|
@ -1000,19 +1004,17 @@ export class TagRenderingConfigUtils {
|
|||
const clone: TagRenderingConfig = Object.create(config)
|
||||
// The original mappings get "priorityIf" set
|
||||
const oldMappingsCloned =
|
||||
clone.mappings?.map(
|
||||
(m) => {
|
||||
const mapping = {
|
||||
...m,
|
||||
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
|
||||
}
|
||||
if (m.if.usedKeys().indexOf("nobrand") < 0) {
|
||||
// Erase 'nobrand=yes', unless this option explicitly sets it
|
||||
mapping["addExtraTags"] = [new Tag("nobrand", "")]
|
||||
}
|
||||
return <Mapping>mapping
|
||||
},
|
||||
) ?? []
|
||||
clone.mappings?.map((m) => {
|
||||
const mapping = {
|
||||
...m,
|
||||
priorityIf: m.priorityIf ?? TagUtils.Tag("id~*"),
|
||||
}
|
||||
if (m.if.usedKeys().indexOf("nobrand") < 0) {
|
||||
// Erase 'nobrand=yes', unless this option explicitly sets it
|
||||
mapping["addExtraTags"] = [new Tag("nobrand", "")]
|
||||
}
|
||||
return <Mapping>mapping
|
||||
}) ?? []
|
||||
clone.mappings = [...oldMappingsCloned, ...extraMappings]
|
||||
return clone
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ export class ThemeInformation {
|
|||
keywords?: (Translatable | Translation)[]
|
||||
}
|
||||
|
||||
|
||||
export default class ThemeConfig implements ThemeInformation {
|
||||
public static readonly defaultSocialImage = "assets/SocialImage.png"
|
||||
public readonly id: string
|
||||
|
|
@ -193,7 +192,7 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
icon: "./assets/svg/pop-out.svg",
|
||||
href: "https://{basepath}/{theme}.html?lat={lat}&lon={lon}&z={zoom}&language={language}",
|
||||
newTab: true,
|
||||
requirements: ["iframe", "no-welcome-message"]
|
||||
requirements: ["iframe", "no-welcome-message"],
|
||||
},
|
||||
context + ".extraLink"
|
||||
)
|
||||
|
|
@ -292,9 +291,7 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
if (!untranslated.has(ln)) {
|
||||
untranslated.set(ln, [])
|
||||
}
|
||||
untranslated
|
||||
.get(ln)
|
||||
.push(translation.context)
|
||||
untranslated.get(ln).push(translation.context)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
@ -330,7 +327,12 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
}
|
||||
}
|
||||
}
|
||||
console.trace("Fallthrough: could not find the appropriate layer for an object with tags", tags, "within layout", this)
|
||||
console.trace(
|
||||
"Fallthrough: could not find the appropriate layer for an object with tags",
|
||||
tags,
|
||||
"within layout",
|
||||
this
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +344,7 @@ export default class ThemeConfig implements ThemeInformation {
|
|||
// The 'favourite'-layer contains pretty much all images as it bundles all layers, so we exclude it
|
||||
const jsonNoFavourites = {
|
||||
...json,
|
||||
layers: json.layers.filter((l) => l["id"] !== "favourite")
|
||||
layers: json.layers.filter((l) => l["id"] !== "favourite"),
|
||||
}
|
||||
const usedImages = jsonNoFavourites._usedImages
|
||||
usedImages.sort()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import ThemeConfig from "./ThemeConfig/ThemeConfig"
|
|||
import { SpecialVisualizationState } from "../UI/SpecialVisualization"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { FeatureSource, IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||
import {
|
||||
FeatureSource,
|
||||
IndexedFeatureSource,
|
||||
WritableFeatureSource,
|
||||
} from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { ExportableMap, MapProperties } from "./MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
|
|
@ -46,7 +50,9 @@ import BackgroundLayerResetter from "../Logic/Actors/BackgroundLayerResetter"
|
|||
import SaveFeatureSourceToLocalStorage from "../Logic/FeatureSource/Actors/SaveFeatureSourceToLocalStorage"
|
||||
import BBoxFeatureSource from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
|
||||
import NoElementsInViewDetector, { FeatureViewState } from "../Logic/Actors/NoElementsInViewDetector"
|
||||
import NoElementsInViewDetector, {
|
||||
FeatureViewState,
|
||||
} from "../Logic/Actors/NoElementsInViewDetector"
|
||||
import FilteredLayer from "./FilteredLayer"
|
||||
import { PreferredRasterLayerSelector } from "../Logic/Actors/PreferredRasterLayerSelector"
|
||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
||||
|
|
@ -166,7 +172,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.featureSwitches = new FeatureSwitchState(layout)
|
||||
this.guistate = new MenuState(
|
||||
this.featureSwitches.featureSwitchWelcomeMessage.data,
|
||||
layout.id,
|
||||
layout.id
|
||||
)
|
||||
this.map = new UIEventSource<MlMap>(undefined)
|
||||
const geolocationState = new GeoLocationState()
|
||||
|
|
@ -182,14 +188,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
oauth_token: QueryParameters.GetQueryParameter(
|
||||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login",
|
||||
"Used to complete the login"
|
||||
),
|
||||
})
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
layout,
|
||||
this.featureSwitches,
|
||||
this.mapProperties,
|
||||
this.mapProperties
|
||||
)
|
||||
this.userRelatedState.fixateNorth.addCallbackAndRunD((fixated) => {
|
||||
this.mapProperties.allowRotating.setData(fixated !== "yes")
|
||||
|
|
@ -200,20 +206,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
geolocationState,
|
||||
this.selectedElement,
|
||||
this.mapProperties,
|
||||
this.userRelatedState.gpsLocationHistoryRetentionTime,
|
||||
this.userRelatedState.gpsLocationHistoryRetentionTime
|
||||
)
|
||||
this.geolocationControl = new GeolocationControlState(this.geolocation, this.mapProperties)
|
||||
|
||||
this.availableLayers = AvailableRasterLayers.layersAvailableAt(
|
||||
this.mapProperties.location,
|
||||
this.osmConnection.isLoggedIn,
|
||||
this.osmConnection.isLoggedIn
|
||||
)
|
||||
|
||||
this.layerState = new LayerState(
|
||||
this.osmConnection,
|
||||
layout.layers,
|
||||
layout.id,
|
||||
this.featureSwitches.featureSwitchLayerDefault,
|
||||
this.featureSwitches.featureSwitchLayerDefault
|
||||
)
|
||||
|
||||
{
|
||||
|
|
@ -222,7 +228,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
const isDisplayed = QueryParameters.GetBooleanQueryParameter(
|
||||
"overlay-" + rasterInfo.id,
|
||||
rasterInfo.defaultState ?? true,
|
||||
"Whether or not overlay layer " + rasterInfo.id + " is shown",
|
||||
"Whether or not overlay layer " + rasterInfo.id + " is shown"
|
||||
)
|
||||
const state = { isDisplayed }
|
||||
overlayLayerStates.set(rasterInfo.id, state)
|
||||
|
|
@ -247,7 +253,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.osmConnection.Backend(),
|
||||
(id) => this.layerState.filteredLayers.get(id).isDisplayed,
|
||||
mvtAvailableLayers,
|
||||
this.fullNodeDatabase,
|
||||
this.fullNodeDatabase
|
||||
)
|
||||
|
||||
let currentViewIndex = 0
|
||||
|
|
@ -265,7 +271,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
id: "current_view_" + currentViewIndex,
|
||||
}),
|
||||
]
|
||||
}),
|
||||
})
|
||||
)
|
||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||
|
||||
|
|
@ -276,19 +282,19 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.changes = new Changes(
|
||||
this,
|
||||
layout?.isLeftRightSensitive() ?? false,
|
||||
(e, extraMsg) => this.reportError(e, extraMsg),
|
||||
(e, extraMsg) => this.reportError(e, extraMsg)
|
||||
)
|
||||
this.historicalUserLocations = this.geolocation.historicalUserLocations
|
||||
this.newFeatures = new NewGeometryFromChangesFeatureSource(
|
||||
this.changes,
|
||||
layoutSource,
|
||||
this.featureProperties,
|
||||
this.featureProperties
|
||||
)
|
||||
layoutSource.addSource(this.newFeatures)
|
||||
|
||||
const perLayer = new PerLayerFeatureSourceSplitter(
|
||||
Array.from(this.layerState.filteredLayers.values()).filter(
|
||||
(l) => l.layerDef?.source !== null,
|
||||
(l) => l.layerDef?.source !== null
|
||||
),
|
||||
new ChangeGeometryApplicator(this.indexedFeatures, this.changes),
|
||||
{
|
||||
|
|
@ -299,10 +305,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
"Got ",
|
||||
features.length,
|
||||
"leftover features, such as",
|
||||
features[0].properties,
|
||||
features[0].properties
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
this.perLayer = perLayer.perLayer
|
||||
}
|
||||
|
|
@ -342,12 +348,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.lastClickObject = new LastClickFeatureSource(
|
||||
this.theme,
|
||||
this.mapProperties.lastClickLocation,
|
||||
this.userRelatedState.addNewFeatureMode,
|
||||
this.userRelatedState.addNewFeatureMode
|
||||
)
|
||||
|
||||
this.osmObjectDownloader = new OsmObjectDownloader(
|
||||
this.osmConnection.Backend(),
|
||||
this.changes,
|
||||
this.changes
|
||||
)
|
||||
|
||||
this.perLayerFiltered = this.showNormalDataOn(this.map)
|
||||
|
|
@ -357,8 +363,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
{
|
||||
currentZoom: this.mapProperties.zoom,
|
||||
layerState: this.layerState,
|
||||
bounds: this.visualFeedbackViewportBounds.map(bounds => bounds ?? this.mapProperties.bounds?.data, [this.mapProperties.bounds]),
|
||||
},
|
||||
bounds: this.visualFeedbackViewportBounds.map(
|
||||
(bounds) => bounds ?? this.mapProperties.bounds?.data,
|
||||
[this.mapProperties.bounds]
|
||||
),
|
||||
}
|
||||
)
|
||||
this.featureSummary = this.setupSummaryLayer()
|
||||
this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView
|
||||
|
|
@ -370,7 +379,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.changes,
|
||||
this.geolocation.geolocationState.currentGPSLocation,
|
||||
this.indexedFeatures,
|
||||
this.reportError,
|
||||
this.reportError
|
||||
)
|
||||
this.favourites = new FavouritesFeatureSource(this)
|
||||
const longAgo = new Date()
|
||||
|
|
@ -417,7 +426,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
ThemeSource.fromCacheZoomLevel,
|
||||
fs,
|
||||
this.featureProperties,
|
||||
fs.layer.layerDef.maxAgeOfCache,
|
||||
fs.layer.layerDef.maxAgeOfCache
|
||||
)
|
||||
toLocalStorage.set(layerId, storage)
|
||||
})
|
||||
|
|
@ -430,7 +439,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
const doShowLayer = this.mapProperties.zoom.map(
|
||||
(z) =>
|
||||
(fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0),
|
||||
[fs.layer.isDisplayed],
|
||||
[fs.layer.isDisplayed]
|
||||
)
|
||||
|
||||
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
|
||||
|
|
@ -447,7 +456,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
fs.layer,
|
||||
fs,
|
||||
(id) => this.featureProperties.getStore(id),
|
||||
this.layerState.globalFilters,
|
||||
this.layerState.globalFilters
|
||||
)
|
||||
filteringFeatureSource.set(layerName, filtered)
|
||||
|
||||
|
|
@ -579,7 +588,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.guistate.pageStates.favourites.set(true)
|
||||
})
|
||||
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
nomod: " ",
|
||||
|
|
@ -593,11 +601,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) {
|
||||
return
|
||||
}
|
||||
if (document.activeElement.tagName === "button" || document.activeElement.tagName === "input") {
|
||||
if (
|
||||
document.activeElement.tagName === "button" ||
|
||||
document.activeElement.tagName === "input"
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.selectClosestAtCenter(0)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for (let i = 1; i < 9; i++) {
|
||||
|
|
@ -615,15 +626,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
onUp: true,
|
||||
},
|
||||
doc,
|
||||
() => this.selectClosestAtCenter(i - 1),
|
||||
() => this.selectClosestAtCenter(i - 1)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
|
||||
this.searchState.feedback.set(undefined)
|
||||
this.searchState.searchIsFocused.set(true)
|
||||
})
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ ctrl: "F" },
|
||||
Translations.t.hotkeyDocumentation.selectSearch,
|
||||
() => {
|
||||
this.searchState.feedback.set(undefined)
|
||||
this.searchState.searchIsFocused.set(true)
|
||||
}
|
||||
)
|
||||
|
||||
this.featureSwitches.featureSwitchBackgroundSelection.addCallbackAndRun((enable) => {
|
||||
if (!enable) {
|
||||
|
|
@ -638,7 +652,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
if (this.featureSwitches.featureSwitchBackgroundSelection.data) {
|
||||
this.guistate.pageStates.background.setData(true)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{
|
||||
|
|
@ -649,7 +663,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
if (this.featureSwitches.featureSwitchFilter.data) {
|
||||
this.guistate.openFilterView()
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
const setLayerCategory = (category: EliCategory, skipLayers: number = 0) => {
|
||||
const timeOfCall = new Date()
|
||||
|
|
@ -664,7 +678,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
available,
|
||||
category,
|
||||
current.data,
|
||||
skipLayers,
|
||||
skipLayers
|
||||
)
|
||||
if (!best) {
|
||||
return
|
||||
|
|
@ -677,43 +691,43 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "O" },
|
||||
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap"),
|
||||
() => setLayerCategory("osmbasedmap")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "M" },
|
||||
Translations.t.hotkeyDocumentation.selectMap,
|
||||
() => setLayerCategory("map"),
|
||||
() => setLayerCategory("map")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "P" },
|
||||
Translations.t.hotkeyDocumentation.selectAerial,
|
||||
() => setLayerCategory("photo"),
|
||||
() => setLayerCategory("photo")
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "O" },
|
||||
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap", 2),
|
||||
() => setLayerCategory("osmbasedmap", 2)
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "M" },
|
||||
Translations.t.hotkeyDocumentation.selectMap,
|
||||
() => setLayerCategory("map", 2),
|
||||
() => setLayerCategory("map", 2)
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "P" },
|
||||
Translations.t.hotkeyDocumentation.selectAerial,
|
||||
() => setLayerCategory("photo", 2),
|
||||
() => setLayerCategory("photo", 2)
|
||||
)
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "L" },
|
||||
Translations.t.hotkeyDocumentation.geolocate,
|
||||
() => {
|
||||
this.geolocationControl.handleClick()
|
||||
},
|
||||
}
|
||||
)
|
||||
return true
|
||||
})
|
||||
|
|
@ -730,7 +744,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
} else {
|
||||
tm.setData("false")
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -738,7 +752,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
/**
|
||||
* MaxZoom for the summary layer
|
||||
*/
|
||||
const normalLayers = this.theme.layers.filter(l => l.isNormal())
|
||||
const normalLayers = this.theme.layers.filter((l) => l.isNormal())
|
||||
|
||||
const maxzoom = Math.min(...normalLayers.map((l) => l.minzoom))
|
||||
|
||||
|
|
@ -746,7 +760,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
(l) =>
|
||||
Constants.priviliged_layers.indexOf(<any>l.id) < 0 &&
|
||||
l.source.geojsonSource === undefined &&
|
||||
l.doCount,
|
||||
l.doCount
|
||||
)
|
||||
if (!Constants.SummaryServer || layers.length === 0) {
|
||||
return undefined
|
||||
|
|
@ -758,7 +772,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.mapProperties,
|
||||
{
|
||||
isActive: this.mapProperties.zoom.map((z) => z < maxzoom),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return new SummaryTileSourceRewriter(summaryTileSource, this.layerState.filteredLayers)
|
||||
|
|
@ -780,12 +794,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
gps_track: this.geolocation.historicalUserLocationsTrack,
|
||||
geocoded_image: new StaticFeatureSource(this.geocodedImages),
|
||||
selected_element: new StaticFeatureSource(
|
||||
this.selectedElement.map((f) => (f === undefined ? empty : [f])),
|
||||
this.selectedElement.map((f) => (f === undefined ? empty : [f]))
|
||||
),
|
||||
range: new StaticFeatureSource(
|
||||
this.mapProperties.maxbounds.map((bbox) =>
|
||||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })],
|
||||
),
|
||||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
|
||||
)
|
||||
),
|
||||
current_view: this.currentView,
|
||||
favourite: this.favourites,
|
||||
|
|
@ -794,7 +808,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
search: this.searchState.locationResults,
|
||||
}
|
||||
|
||||
|
||||
this.closestFeatures.registerSource(specialLayers.favourite, "favourite")
|
||||
if (this.theme?.lockLocation) {
|
||||
const bbox = new BBox(<any>this.theme.lockLocation)
|
||||
|
|
@ -802,7 +815,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
ShowDataLayer.showRange(
|
||||
this.map,
|
||||
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
|
||||
this.featureSwitches.featureSwitchIsTesting,
|
||||
this.featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
}
|
||||
const currentViewLayer = this.theme.layers.find((l) => l.id === "current_view")
|
||||
|
|
@ -816,7 +829,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
currentViewLayer,
|
||||
this.theme,
|
||||
this.osmObjectDownloader,
|
||||
this.featureProperties,
|
||||
this.featureProperties
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -848,7 +861,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
layer: flayer.layerDef,
|
||||
metaTags: this.userRelatedState.preferencesAsTags,
|
||||
selectedElement: this.selectedElement,
|
||||
|
||||
}
|
||||
if (flayer.layerDef.id === "search") {
|
||||
options.onClick = (feature) => {
|
||||
|
|
@ -863,20 +875,20 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
{
|
||||
const lastClickLayerConfig = new LayerConfig(
|
||||
<LayerConfigJson>last_click_layerconfig,
|
||||
"last_click",
|
||||
"last_click"
|
||||
)
|
||||
const lastClickFiltered =
|
||||
lastClickLayerConfig.isShown === undefined
|
||||
? specialLayers.last_click
|
||||
: specialLayers.last_click.features.mapD((fs) =>
|
||||
fs.filter((f) => {
|
||||
const matches = lastClickLayerConfig.isShown.matchesProperties(
|
||||
f.properties,
|
||||
)
|
||||
console.debug("LastClick ", f, "matches", matches)
|
||||
return matches
|
||||
}),
|
||||
)
|
||||
fs.filter((f) => {
|
||||
const matches = lastClickLayerConfig.isShown.matchesProperties(
|
||||
f.properties
|
||||
)
|
||||
console.debug("LastClick ", f, "matches", matches)
|
||||
return matches
|
||||
})
|
||||
)
|
||||
// show last click = new point/note marker
|
||||
new ShowDataLayer(this.map, {
|
||||
features: new StaticFeatureSource(lastClickFiltered),
|
||||
|
|
@ -908,7 +920,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
* Setup various services for which no reference are needed
|
||||
*/
|
||||
private initActors() {
|
||||
|
||||
if (!this.theme.official) {
|
||||
// Add custom themes to the "visited custom themes"
|
||||
const th = this.theme
|
||||
|
|
@ -916,13 +927,11 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
id: th.id,
|
||||
icon: th.icon,
|
||||
title: th.title.translations,
|
||||
shortDescription: th.shortDescription.translations ,
|
||||
layers: th.layers.filter(l => l.isNormal()).map(l => l.id)
|
||||
|
||||
shortDescription: th.shortDescription.translations,
|
||||
layers: th.layers.filter((l) => l.isNormal()).map((l) => l.id),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.selectedElement.addCallback((selected) => {
|
||||
if (selected === undefined) {
|
||||
this.focusOnMap()
|
||||
|
|
@ -942,28 +951,31 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
})
|
||||
|
||||
// Add the selected element to the recently visited history
|
||||
this.selectedElement.addCallbackD(selected => {
|
||||
this.selectedElement.addCallbackD((selected) => {
|
||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||
const layer = this.theme.getMatchingLayer(selected.properties)
|
||||
|
||||
const nameOptions = [
|
||||
selected?.properties?.name,
|
||||
selected?.properties?.alt_name, selected?.properties?.local_name,
|
||||
selected?.properties?.alt_name,
|
||||
selected?.properties?.local_name,
|
||||
layer?.title.GetRenderValue(selected?.properties ?? {}).txt,
|
||||
selected.properties.display_name,
|
||||
selected.properties.id,
|
||||
]
|
||||
const r = <GeocodeResult>{
|
||||
feature: selected,
|
||||
display_name: nameOptions.find(opt => opt !== undefined),
|
||||
osm_id, osm_type,
|
||||
lon, lat,
|
||||
display_name: nameOptions.find((opt) => opt !== undefined),
|
||||
osm_id,
|
||||
osm_type,
|
||||
lon,
|
||||
lat,
|
||||
}
|
||||
this.userRelatedState.recentlyVisitedSearch.add(r)
|
||||
})
|
||||
|
||||
this.userRelatedState.showScale.addCallbackAndRun(showScale => {
|
||||
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {
|
||||
this.mapProperties.showScale.set(showScale)
|
||||
})
|
||||
new ThemeViewStateHashActor(this)
|
||||
|
|
@ -977,7 +989,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.mapProperties.rasterLayer,
|
||||
this.availableLayers,
|
||||
this.featureSwitches.backgroundLayerId,
|
||||
this.userRelatedState.preferredBackgroundLayer,
|
||||
this.userRelatedState.preferredBackgroundLayer
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -990,7 +1002,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the theme
|
||||
*/
|
||||
public getMatchingLayer(properties: Record<string, string>) {
|
||||
|
||||
const id = properties.id
|
||||
|
||||
if (id.startsWith("summary_")) {
|
||||
|
|
@ -1024,7 +1035,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
? ">>> _Not_ reporting error to report server as testmode is on"
|
||||
: ">>> Reporting error to",
|
||||
Constants.ErrorReportServer,
|
||||
message,
|
||||
message
|
||||
)
|
||||
if (isTesting) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
"oauth_token",
|
||||
undefined,
|
||||
"Used to complete the login"
|
||||
)
|
||||
),
|
||||
})
|
||||
const state = new UserRelatedState(osmConnection)
|
||||
const t = Translations.t.index
|
||||
|
|
@ -46,41 +46,56 @@
|
|||
|
||||
let searchIsFocused = new UIEventSource(true)
|
||||
|
||||
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === false)
|
||||
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(th => th.hideFromOverview === true)
|
||||
let visitedHiddenThemes: Store<MinimalThemeInformation[]> = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection)
|
||||
.map((knownIds) => hiddenThemes.filter((theme) =>
|
||||
knownIds.indexOf(theme.id) >= 0 || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
|
||||
))
|
||||
const officialThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
|
||||
(th) => th.hideFromOverview === false
|
||||
)
|
||||
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
|
||||
(th) => th.hideFromOverview === true
|
||||
)
|
||||
let visitedHiddenThemes: Store<MinimalThemeInformation[]> =
|
||||
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map((knownIds) =>
|
||||
hiddenThemes.filter(
|
||||
(theme) =>
|
||||
knownIds.indexOf(theme.id) >= 0 ||
|
||||
state.osmConnection.userDetails.data.name === "Pieter Vander Vennet"
|
||||
)
|
||||
)
|
||||
|
||||
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(state.installedUserThemes)
|
||||
.mapD(stableIds => Utils.NoNullInplace(stableIds.map(id => state.getUnofficialTheme(id))))
|
||||
const customThemes: Store<MinimalThemeInformation[]> = Stores.ListStabilized<string>(
|
||||
state.installedUserThemes
|
||||
).mapD((stableIds) => Utils.NoNullInplace(stableIds.map((id) => state.getUnofficialTheme(id))))
|
||||
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
|
||||
return searchStable.map(search => {
|
||||
if (!search) {
|
||||
return themes.data
|
||||
}
|
||||
return searchStable.map(
|
||||
(search) => {
|
||||
if (!search) {
|
||||
return themes.data
|
||||
}
|
||||
|
||||
const start = new Date().getTime()
|
||||
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
|
||||
const end = new Date().getTime()
|
||||
console.trace("Scores for", search , "are", scores, "searching took", end - start,"ms")
|
||||
const strict = scores.filter(sc => sc.lowest < 2)
|
||||
if (strict.length > 0) {
|
||||
return strict.map(sc => sc.theme)
|
||||
}
|
||||
return scores.filter(sc => sc.lowest < 4).slice(0, 6).map(sc => sc.theme)
|
||||
}, [themes])
|
||||
const start = new Date().getTime()
|
||||
const scores = ThemeSearch.sortedByLowestScores(search, themes.data)
|
||||
const end = new Date().getTime()
|
||||
console.trace("Scores for", search, "are", scores, "searching took", end - start, "ms")
|
||||
const strict = scores.filter((sc) => sc.lowest < 2)
|
||||
if (strict.length > 0) {
|
||||
return strict.map((sc) => sc.theme)
|
||||
}
|
||||
return scores
|
||||
.filter((sc) => sc.lowest < 4)
|
||||
.slice(0, 6)
|
||||
.map((sc) => sc.theme)
|
||||
},
|
||||
[themes]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
let officialSearched : Store<MinimalThemeInformation[]>= filtered(new ImmutableStore(officialThemes))
|
||||
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
|
||||
let officialSearched: Store<MinimalThemeInformation[]> = filtered(
|
||||
new ImmutableStore(officialThemes)
|
||||
)
|
||||
let hiddenSearched: Store<MinimalThemeInformation[]> = filtered(visitedHiddenThemes)
|
||||
let customSearched: Store<MinimalThemeInformation[]> = filtered(customThemes)
|
||||
|
||||
|
||||
let searchIsFocussed = new UIEventSource(false)
|
||||
document.addEventListener("keydown", function(event) {
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.ctrlKey && event.code === "KeyF") {
|
||||
searchIsFocussed.set(true)
|
||||
event.preventDefault()
|
||||
|
|
@ -101,10 +116,7 @@
|
|||
}
|
||||
|
||||
window.location.href = ThemeSearch.createUrlFor(candidate, undefined)
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
|
@ -136,7 +148,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Searchbar value={search} placeholder={tr.searchForATheme} on:search={() => applySearch()} autofocus isFocused={searchIsFocussed} />
|
||||
<Searchbar
|
||||
value={search}
|
||||
placeholder={tr.searchForATheme}
|
||||
on:search={() => applySearch()}
|
||||
autofocus
|
||||
isFocused={searchIsFocussed}
|
||||
/>
|
||||
|
||||
<ThemesList {search} {state} themes={$officialSearched} />
|
||||
|
||||
|
|
@ -166,8 +184,11 @@
|
|||
</ThemesList>
|
||||
|
||||
{#if $customThemes.length > 0}
|
||||
<ThemesList {search} {state} themes={$customSearched}
|
||||
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
|
||||
<ThemesList
|
||||
{search}
|
||||
{state}
|
||||
themes={$customSearched}
|
||||
hasSelection={$officialSearched.length === 0 && $hiddenSearched.length === 0}
|
||||
>
|
||||
<svelte:fragment slot="title">
|
||||
<h3>
|
||||
|
|
@ -177,7 +198,6 @@
|
|||
</svelte:fragment>
|
||||
</ThemesList>
|
||||
{/if}
|
||||
|
||||
</LoginToggle>
|
||||
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -9,83 +9,82 @@
|
|||
export let open = new UIEventSource(false)
|
||||
export let dotsSize = `w-6 h-6`
|
||||
export let dotsPosition = `top-0 right-0`
|
||||
export let hideBackground= false
|
||||
export let hideBackground = false
|
||||
let menuPosition = ``
|
||||
if(dotsPosition.indexOf("left-0") >= 0){
|
||||
if (dotsPosition.indexOf("left-0") >= 0) {
|
||||
menuPosition = "left-0"
|
||||
}else{
|
||||
} else {
|
||||
menuPosition = `right-0`
|
||||
}
|
||||
|
||||
if(dotsPosition.indexOf("top-0") > 0){
|
||||
if (dotsPosition.indexOf("top-0") > 0) {
|
||||
menuPosition += " bottom-0"
|
||||
}else{
|
||||
} else {
|
||||
menuPosition += ` top-0`
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
open.set(!open.data)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="relative" style="z-index: 50">
|
||||
<div
|
||||
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled "
|
||||
class="sidebar-unit absolute {menuPosition} collapsable normal-background button-unstyled"
|
||||
class:transition-background={hideBackground}
|
||||
class:collapsed={!$open}>
|
||||
class:collapsed={!$open}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
<DotsCircleHorizontal
|
||||
class={ `absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${$open?"dots-menu-opened":""}`}
|
||||
on:click={toggle} />
|
||||
class={`absolute ${dotsPosition} ${dotsSize} dots-menu transition-colors ${
|
||||
$open ? "dots-menu-opened" : ""
|
||||
}`}
|
||||
on:click={toggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.dots-menu {
|
||||
z-index: 50;
|
||||
}
|
||||
.dots-menu {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
:global(.dots-menu > path) {
|
||||
fill: var(--interactive-background);
|
||||
transition: fill 350ms linear;
|
||||
cursor: pointer;
|
||||
:global(.dots-menu > path) {
|
||||
fill: var(--interactive-background);
|
||||
transition: fill 350ms linear;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
|
||||
fill: var(--interactive-foreground);
|
||||
}
|
||||
|
||||
:global(.dots-menu:hover > path, .dots-menu-opened > path) {
|
||||
fill: var(--interactive-foreground)
|
||||
}
|
||||
.collapsable {
|
||||
max-width: 50rem;
|
||||
max-height: 10rem;
|
||||
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: none;
|
||||
width: max-content;
|
||||
box-shadow: #ccc;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--button-background);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.collapsable {
|
||||
max-width: 50rem;
|
||||
max-height: 10rem;
|
||||
transition: max-width 500ms linear, max-height 500ms linear, border 500ms linear;
|
||||
overflow: hidden;
|
||||
flex-wrap: nowrap;
|
||||
text-wrap: none;
|
||||
width: max-content;
|
||||
box-shadow: #ccc;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--button-background);
|
||||
background-color: white;
|
||||
}
|
||||
.transition-background {
|
||||
transition: background-color 150ms linear;
|
||||
}
|
||||
|
||||
.transition-background {
|
||||
transition: background-color 150ms linear;
|
||||
}
|
||||
|
||||
.transition-background.collapsed {
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
max-width: 0;
|
||||
max-height: 0;
|
||||
border: 2px solid #00000000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.transition-background.collapsed {
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
max-width: 0;
|
||||
max-height: 0;
|
||||
border: 2px solid #00000000;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@
|
|||
let transitionParams = {
|
||||
x: 640,
|
||||
duration: 200,
|
||||
easing: sineIn
|
||||
easing: sineIn,
|
||||
}
|
||||
let hidden = !shown.data
|
||||
|
||||
shown.addCallback(sh => {
|
||||
shown.addCallback((sh) => {
|
||||
hidden = !sh
|
||||
})
|
||||
|
||||
|
|
@ -23,19 +23,21 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<Drawer placement="right"
|
||||
transitionType="fly" {transitionParams}
|
||||
activateClickOutside={false}
|
||||
divClass="overflow-y-auto z-3"
|
||||
backdrop={false}
|
||||
id="drawer-right"
|
||||
width="w-full sm:w-80 md:w-96"
|
||||
rightOffset="inset-y-0 right-0"
|
||||
bind:hidden={hidden}>
|
||||
|
||||
<Drawer
|
||||
placement="right"
|
||||
transitionType="fly"
|
||||
{transitionParams}
|
||||
activateClickOutside={false}
|
||||
divClass="overflow-y-auto z-3"
|
||||
backdrop={false}
|
||||
id="drawer-right"
|
||||
width="w-full sm:w-80 md:w-96"
|
||||
rightOffset="inset-y-0 right-0"
|
||||
bind:hidden
|
||||
>
|
||||
<div class="low-interaction h-screen">
|
||||
<div class="h-full" style={`padding-top: ${height}px`}>
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<div class="flex h-full flex-col overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,10 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Popup from "./Popup.svelte"
|
||||
|
||||
|
||||
export let onlyLink: boolean = false
|
||||
export let bodyPadding = "p-4 md:p-5 "
|
||||
export let fullscreen: boolean = false
|
||||
export let shown: UIEventSource<boolean>
|
||||
|
||||
</script>
|
||||
|
||||
{#if !onlyLink}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
export let fullscreen: boolean = false
|
||||
|
||||
const shared = "in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
|
||||
const shared =
|
||||
"in-page normal-background dark:bg-gray-800 rounded-lg border-gray-200 dark:border-gray-700 border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md"
|
||||
let defaultClass = "relative flex flex-col mx-auto w-full divide-y " + shared
|
||||
if (fullscreen) {
|
||||
defaultClass = shared
|
||||
|
|
@ -27,20 +28,23 @@
|
|||
export let shown: UIEventSource<boolean>
|
||||
export let dismissable = true
|
||||
let _shown = false
|
||||
shown.addCallbackAndRun(sh => {
|
||||
shown.addCallbackAndRun((sh) => {
|
||||
_shown = sh
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<Modal open={_shown} on:close={() => shown.set(false)} outsideclose
|
||||
size="xl"
|
||||
{dismissable}
|
||||
{defaultClass} {bodyClass} {dialogClass} {headerClass}
|
||||
color="none">
|
||||
|
||||
<Modal
|
||||
open={_shown}
|
||||
on:close={() => shown.set(false)}
|
||||
outsideclose
|
||||
size="xl"
|
||||
{dismissable}
|
||||
{defaultClass}
|
||||
{bodyClass}
|
||||
{dialogClass}
|
||||
{headerClass}
|
||||
color="none"
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
{#if $$slots.header}
|
||||
<h1 class="page-header w-full">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let value: UIEventSource<string>
|
||||
let _value = value.data ?? ""
|
||||
value.addCallbackD(v => {
|
||||
value.addCallbackD((v) => {
|
||||
_value = v
|
||||
})
|
||||
$: value.set(_value)
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
export let autofocus = false
|
||||
|
||||
isFocused?.addCallback(focussed => {
|
||||
isFocused?.addCallback((focussed) => {
|
||||
if (focussed) {
|
||||
requestAnimationFrame(() => {
|
||||
if (document.activeElement !== inputElement) {
|
||||
|
|
@ -33,41 +33,44 @@
|
|||
}
|
||||
})
|
||||
|
||||
if(autofocus){
|
||||
if (autofocus) {
|
||||
isFocused.set(true)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<form
|
||||
class="w-full"
|
||||
on:submit|preventDefault={() => dispatch("search")}
|
||||
>
|
||||
<form class="w-full" on:submit|preventDefault={() => dispatch("search")}>
|
||||
<label
|
||||
class="neutral-label normal-background flex w-full items-center rounded-full border border-black box-shadow"
|
||||
class="neutral-label normal-background box-shadow flex w-full items-center rounded-full border border-black"
|
||||
>
|
||||
<SearchIcon aria-hidden="true" class="h-8 w-8 ml-2" />
|
||||
<SearchIcon aria-hidden="true" class="ml-2 h-8 w-8" />
|
||||
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
on:focus={() => {isFocused?.setData(true)}}
|
||||
on:blur={() => {isFocused?.setData(false)}}
|
||||
on:focus={() => {
|
||||
isFocused?.setData(true)
|
||||
}}
|
||||
on:blur={() => {
|
||||
isFocused?.setData(false)
|
||||
}}
|
||||
type="search"
|
||||
style=" --tw-ring-color: rgb(0 0 0 / 0) !important;"
|
||||
class="px-0 ml-1 w-full outline-none border-none"
|
||||
class="ml-1 w-full border-none px-0 outline-none"
|
||||
on:keypress={(keypr) => {
|
||||
return keypr.key === "Enter" ? dispatch("search") : undefined
|
||||
}}
|
||||
return keypr.key === "Enter" ? dispatch("search") : undefined
|
||||
}}
|
||||
bind:value={_value}
|
||||
use:set_placeholder={placeholder}
|
||||
use:ariaLabel={placeholder}
|
||||
/>
|
||||
|
||||
{#if $value.length > 0}
|
||||
<Backspace on:click={() => value.set("")} color="var(--button-background)" class="w-6 h-6 mr-3 cursor-pointer" />
|
||||
<Backspace
|
||||
on:click={() => value.set("")}
|
||||
color="var(--button-background)"
|
||||
class="mr-3 h-6 w-6 cursor-pointer"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-6 mr-3" />
|
||||
<div class="mr-3 w-6" />
|
||||
{/if}
|
||||
</label>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,55 +1,64 @@
|
|||
<div class="sidebar-unit">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.sidebar-unit) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.25rem;
|
||||
background: var(--background-color);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
:global(.sidebar-unit) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.25rem;
|
||||
background: var(--background-color);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
:global(.sidebar-unit > h3) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
:global(.sidebar-unit > h3) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
:global(.sidebar-button svg, .sidebar-button img, .sidebar-unit > button img, .sidebar-unit > button svg) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:global(
|
||||
.sidebar-button svg,
|
||||
.sidebar-button img,
|
||||
.sidebar-unit > button img,
|
||||
.sidebar-unit > button svg
|
||||
) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.sidebar-button .weblate-link > svg) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
:global(.sidebar-button .weblate-link > svg) {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem !important;
|
||||
padding: 0.4rem 0.75rem !important;
|
||||
text-decoration: none !important;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
:global(.sidebar-button, .sidebar-unit > a, .sidebar-unit > button) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem !important;
|
||||
padding: 0.4rem 0.75rem !important;
|
||||
text-decoration: none !important;
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
:global(.sidebar-button > svg , .sidebar-button > img, .sidebar-unit > a img, .sidebar-unit > a svg, .sidebar-unit > button svg, .sidebar-unit > button img) {
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
|
||||
background: var(--low-interaction-background) !important;
|
||||
}
|
||||
|
||||
:global(
|
||||
.sidebar-button > svg,
|
||||
.sidebar-button > img,
|
||||
.sidebar-unit > a img,
|
||||
.sidebar-unit > a svg,
|
||||
.sidebar-unit > button svg,
|
||||
.sidebar-unit > button img
|
||||
) {
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:global(.sidebar-button:hover, .sidebar-unit > a:hover, .sidebar-unit > button:hover) {
|
||||
background: var(--low-interaction-background) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
return state.sync(
|
||||
(f) => f === 0,
|
||||
[],
|
||||
(b) => (b ? 0 : undefined),
|
||||
(b) => (b ? 0 : undefined)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
{:else if $isDebugging}
|
||||
<div class="code">
|
||||
{layer.id} (no name)
|
||||
</div>
|
||||
<div class="code">
|
||||
{layer.id} (no name)
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -55,14 +55,18 @@
|
|||
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
</script>
|
||||
|
||||
<div class="low-interaction p-1 rounded-2xl px-3" class:interactive={$firstValue?.length > 0}>
|
||||
<div class="low-interaction rounded-2xl p-1 px-3" class:interactive={$firstValue?.length > 0}>
|
||||
{#each parts as part, i}
|
||||
{#if part["subs"]}
|
||||
<!-- This is a field! -->
|
||||
<span class="mx-1">
|
||||
<InputHelper value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}>
|
||||
<ValidatedInput slot="fallback" value={fieldValues[part["subs"]]} type={fieldTypes[part["subs"]]}
|
||||
{feedback} />
|
||||
<ValidatedInput
|
||||
slot="fallback"
|
||||
value={fieldValues[part["subs"]]}
|
||||
type={fieldTypes[part["subs"]]}
|
||||
{feedback}
|
||||
/>
|
||||
</InputHelper>
|
||||
</span>
|
||||
{:else}
|
||||
|
|
@ -70,6 +74,6 @@
|
|||
{/if}
|
||||
{/each}
|
||||
{#if $feedback}
|
||||
<Tr cls="alert" t={$feedback}/>
|
||||
<Tr cls="alert" t={$feedback} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -150,7 +150,6 @@
|
|||
</LoginToggle>
|
||||
|
||||
<LanguagePicker />
|
||||
|
||||
</SidebarUnit>
|
||||
|
||||
<!-- Theme related: documentation links, download, ... -->
|
||||
|
|
@ -218,7 +217,6 @@
|
|||
|
||||
<!-- Other links and tools for the given location: open iD/JOSM; community index, ... -->
|
||||
<SidebarUnit>
|
||||
|
||||
<h3>
|
||||
<Tr t={t.moreUtilsTitle} />
|
||||
</h3>
|
||||
|
|
@ -238,13 +236,13 @@
|
|||
<MapillaryLink large={false} mapProperties={state.mapProperties} />
|
||||
</If>
|
||||
|
||||
<a class="flex sidebar-button" href="geo:{$location.lat},{$location.lon}"><ShareIcon /><Tr t={t.openHereDifferentApp}/></a>
|
||||
|
||||
<a class="sidebar-button flex" href="geo:{$location.lat},{$location.lon}">
|
||||
<ShareIcon /><Tr t={t.openHereDifferentApp} />
|
||||
</a>
|
||||
</SidebarUnit>
|
||||
|
||||
<!-- About MC: various outward links, legal info, ... -->
|
||||
<SidebarUnit>
|
||||
|
||||
<h3>
|
||||
<Tr t={Translations.t.general.menu.aboutMapComplete} />
|
||||
</h3>
|
||||
|
|
@ -275,11 +273,11 @@
|
|||
</a>
|
||||
|
||||
<a class="flex" href="mailto:info@mapcomplete.org">
|
||||
<EnvelopeOpen class="h-6 w-6"/>
|
||||
<Tr t={Translations.t.general.attribution.emailCreators}/>
|
||||
<EnvelopeOpen class="h-6 w-6" />
|
||||
<Tr t={Translations.t.general.attribution.emailCreators} />
|
||||
</a>
|
||||
<a class="flex" href="https://hosted.weblate.org/projects/mapcomplete/" target="_blank">
|
||||
<TranslateIcon class="h-6 w-6"/>
|
||||
<TranslateIcon class="h-6 w-6" />
|
||||
<Tr t={Translations.t.translations.activateButton} />
|
||||
</a>
|
||||
|
||||
|
|
@ -322,6 +320,3 @@
|
|||
</div>
|
||||
</SidebarUnit>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,11 @@
|
|||
let featuresForLayer: FeatureSource = state.perLayer.get(targetLayer.id)
|
||||
if (featuresForLayer) {
|
||||
if (dontShow) {
|
||||
featuresForLayer = new StaticFeatureSource(featuresForLayer.features.map(feats => feats.filter(f => dontShow.indexOf(f.properties.id) < 0)))
|
||||
featuresForLayer = new StaticFeatureSource(
|
||||
featuresForLayer.features.map((feats) =>
|
||||
feats.filter((f) => dontShow.indexOf(f.properties.id) < 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
new ShowDataLayer(map, {
|
||||
layer: targetLayer,
|
||||
|
|
@ -116,7 +120,7 @@
|
|||
allowUnsnapped: true,
|
||||
snappedTo,
|
||||
snapLocation: value,
|
||||
},
|
||||
}
|
||||
)
|
||||
const withCorrectedAttributes = new StaticFeatureSource(
|
||||
snappedLocation.features.mapD((feats) =>
|
||||
|
|
@ -130,8 +134,8 @@
|
|||
...f,
|
||||
properties,
|
||||
}
|
||||
}),
|
||||
),
|
||||
})
|
||||
)
|
||||
)
|
||||
// The actual point to be created, snapped at the new location
|
||||
new ShowDataLayer(map, {
|
||||
|
|
@ -140,14 +144,13 @@
|
|||
})
|
||||
withCorrectedAttributes.features.addCallbackAndRunD((f) => console.log("Snapped point is", f))
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<LocationInput
|
||||
{map}
|
||||
on:click
|
||||
{mapProperties}
|
||||
value={ snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
|
||||
value={snapToLayers?.length > 0 ? new UIEventSource(undefined) : value}
|
||||
initialCoordinate={coordinate}
|
||||
{maxDistanceInMeters}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { twMerge } from "tailwind-merge"
|
||||
import { PanoramaxXYZ, Panoramax } from "panoramax-js/dist"
|
||||
import Panoramax_bw from "../../assets/svg/Panoramax_bw.svelte"
|
||||
import {default as Panoramax_svg} from "../../assets/svg/Panoramax.svelte"
|
||||
import { default as Panoramax_svg } from "../../assets/svg/Panoramax.svelte"
|
||||
|
||||
/*
|
||||
A subtleButton which opens panoramax in a new tab at the current location
|
||||
|
|
@ -19,11 +19,14 @@
|
|||
}
|
||||
let location = mapProperties.location
|
||||
let zoom = mapProperties.zoom
|
||||
let href = location.mapD(location =>
|
||||
host.createViewLink({
|
||||
location,
|
||||
zoom: zoom.data,
|
||||
}), [zoom])
|
||||
let href = location.mapD(
|
||||
(location) =>
|
||||
host.createViewLink({
|
||||
location,
|
||||
zoom: zoom.data,
|
||||
}),
|
||||
[zoom]
|
||||
)
|
||||
export let large: boolean = true
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,6 @@
|
|||
</script>
|
||||
|
||||
<div class="link-underline flex flex-col">
|
||||
|
||||
<div class="flex flex-col">
|
||||
<Tr t={tr.intro} />
|
||||
<Copyable {state} text={linkToShare} />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
|
||||
export let theme: MinimalThemeInformation & {isOfficial?: boolean}
|
||||
export let theme: MinimalThemeInformation & { isOfficial?: boolean }
|
||||
let isCustom: boolean = theme.id.startsWith("https://") || theme.id.startsWith("http://")
|
||||
export let state: { layoutToUse?: { id: string }; osmConnection: OsmConnection }
|
||||
|
||||
|
|
@ -66,12 +66,12 @@
|
|||
let href = createUrl(theme, isCustom, state)
|
||||
</script>
|
||||
|
||||
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
|
||||
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
|
||||
<a class="low-interaction my-1 flex w-full items-center text-ellipsis rounded p-1" href={$href}>
|
||||
<Marker icons={theme.icon} size="block h-8 w-8 sm:h-11 sm:w-11 m-1 sm:mx-2 md:mx-4 shrink-0" />
|
||||
|
||||
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
|
||||
<Tr cls="" t={title} />
|
||||
<Tr cls="subtle text-base" t={description} />
|
||||
<slot/>
|
||||
</span>
|
||||
</a>
|
||||
<span class="flex flex-col overflow-hidden text-ellipsis text-xl font-bold">
|
||||
<Tr cls="" t={title} />
|
||||
<Tr cls="subtle text-base" t={description} />
|
||||
<slot />
|
||||
</span>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -12,22 +12,18 @@
|
|||
export let themes: MinimalThemeInformation[]
|
||||
export let state: { osmConnection: OsmConnection }
|
||||
|
||||
export let hasSelection : boolean = true
|
||||
|
||||
export let hasSelection: boolean = true
|
||||
</script>
|
||||
|
||||
<section class="w-full">
|
||||
<slot name="title" />
|
||||
<div class="theme-list my-2 gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each themes as theme (theme.id)}
|
||||
<ThemeButton
|
||||
{theme}
|
||||
{state}
|
||||
>
|
||||
<ThemeButton {theme} {state}>
|
||||
{#if $search && hasSelection && themes?.[0] === theme}
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
<span class="thanks hidden-on-mobile" aria-hidden="true">
|
||||
<Tr t={Translations.t.general.morescreen.enterToOpen} />
|
||||
</span>
|
||||
{/if}
|
||||
</ThemeButton>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,5 @@
|
|||
osmConnection: OsmConnection
|
||||
}
|
||||
|
||||
|
||||
let customThemes
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
JSON.stringify(contents),
|
||||
"mapcomplete-favourites-" + new Date().toISOString() + ".geojson",
|
||||
{
|
||||
mimetype: "application/vnd.geo+json"
|
||||
mimetype: "application/vnd.geo+json",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
gpx,
|
||||
"mapcomplete-favourites-" + new Date().toISOString() + ".gpx",
|
||||
{
|
||||
mimetype: "{gpx=application/gpx+xml}"
|
||||
mimetype: "{gpx=application/gpx+xml}",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
export let expanded = false
|
||||
export let noBorder = false
|
||||
let defaultClass: string = undefined
|
||||
if(noBorder){
|
||||
let defaultClass: string = undefined
|
||||
if (noBorder) {
|
||||
defaultClass = "unstyled w-full flex-grow"
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -31,14 +31,18 @@
|
|||
export let canZoom = previewedImage !== undefined
|
||||
let loaded = false
|
||||
let showBigPreview = new UIEventSource(false)
|
||||
onDestroy(showBigPreview.addCallbackAndRun(shown => {
|
||||
if (!shown) {
|
||||
previewedImage.set(undefined)
|
||||
}
|
||||
}))
|
||||
onDestroy(previewedImage.addCallbackAndRun(previewedImage => {
|
||||
showBigPreview.set(previewedImage?.id === image.id)
|
||||
}))
|
||||
onDestroy(
|
||||
showBigPreview.addCallbackAndRun((shown) => {
|
||||
if (!shown) {
|
||||
previewedImage.set(undefined)
|
||||
}
|
||||
})
|
||||
)
|
||||
onDestroy(
|
||||
previewedImage.addCallbackAndRun((previewedImage) => {
|
||||
showBigPreview.set(previewedImage?.id === image.id)
|
||||
})
|
||||
)
|
||||
|
||||
function highlight(entered: boolean = true) {
|
||||
if (!entered) {
|
||||
|
|
@ -72,43 +76,49 @@
|
|||
</ImageOperations>
|
||||
</div>
|
||||
<div class="absolute top-4 right-4">
|
||||
<CloseButton class="normal-background"
|
||||
on:click={() => {console.log("Closing");previewedImage.set(undefined)}}></CloseButton>
|
||||
<CloseButton
|
||||
class="normal-background"
|
||||
on:click={() => {
|
||||
console.log("Closing")
|
||||
previewedImage.set(undefined)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
{#if image.status !== undefined && image.status !== "ready"}
|
||||
<div class="h-full flex flex-col justify-center">
|
||||
<div class="flex h-full flex-col justify-center">
|
||||
<Loading>
|
||||
<Tr t={Translations.t.image.processing}/>
|
||||
<Tr t={Translations.t.image.processing} />
|
||||
</Loading>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative shrink-0">
|
||||
<div class="relative w-fit"
|
||||
on:mouseenter={() => highlight()}
|
||||
on:mouseleave={() => highlight(false)}
|
||||
<div
|
||||
class="relative w-fit"
|
||||
on:mouseenter={() => highlight()}
|
||||
on:mouseleave={() => highlight(false)}
|
||||
>
|
||||
|
||||
<img
|
||||
bind:this={imgEl}
|
||||
on:load={() => (loaded = true)}
|
||||
class={imgClass ?? ""}
|
||||
class:cursor-zoom-in={canZoom}
|
||||
on:click={() => {
|
||||
previewedImage?.set(image)
|
||||
}}
|
||||
previewedImage?.set(image)
|
||||
}}
|
||||
on:error={() => {
|
||||
if (fallbackImage) {
|
||||
imgEl.src = fallbackImage
|
||||
}
|
||||
}}
|
||||
if (fallbackImage) {
|
||||
imgEl.src = fallbackImage
|
||||
}
|
||||
}}
|
||||
src={image.url}
|
||||
/>
|
||||
|
||||
{#if canZoom && loaded}
|
||||
<div
|
||||
class="bg-black-transparent absolute right-0 top-0 rounded-bl-full"
|
||||
on:click={() => previewedImage.set(image)}>
|
||||
on:click={() => previewedImage.set(image)}
|
||||
>
|
||||
<MagnifyingGlassPlusIcon class="h-8 w-8 cursor-zoom-in pl-3 pb-3" color="white" />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,5 @@
|
|||
</div>
|
||||
|
||||
<slot />
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
key: undefined,
|
||||
provider: AllImageProviders.byName(image.provider),
|
||||
date: new Date(image.date),
|
||||
id: Object.values(image.osmTags)[0]
|
||||
id: Object.values(image.osmTags)[0],
|
||||
}
|
||||
|
||||
async function applyLink(isLinked: boolean) {
|
||||
|
|
@ -46,7 +46,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 {
|
||||
|
|
@ -55,7 +55,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)
|
||||
}
|
||||
|
|
@ -67,9 +67,8 @@
|
|||
|
||||
let element: HTMLDivElement
|
||||
if (highlighted) {
|
||||
|
||||
onDestroy(
|
||||
highlighted.addCallbackD(highlightedUrl => {
|
||||
highlighted.addCallbackD((highlightedUrl) => {
|
||||
if (highlightedUrl === image.pictureUrl) {
|
||||
Utils.scrollIntoView(element)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
import { BBox } from "../../Logic/BBox"
|
||||
import PanoramaxLink from "../BigComponents/PanoramaxLink.svelte"
|
||||
|
||||
|
||||
export let tags: UIEventSource<OsmTags>
|
||||
export let state: SpecialVisualizationState
|
||||
export let lon: number
|
||||
|
|
@ -38,7 +37,7 @@
|
|||
let imagesProvider = state.nearbyImageSearcher
|
||||
|
||||
let loadedImages = AllImageProviders.LoadImagesFor(tags).mapD(
|
||||
(loaded) => new Set(loaded.map((img) => img.url)),
|
||||
(loaded) => new Set(loaded.map((img) => img.url))
|
||||
)
|
||||
let imageState = imagesProvider.getImagesAround(lon, lat)
|
||||
let result: Store<P4CPicture[]> = imageState.images.mapD(
|
||||
|
|
@ -47,53 +46,61 @@
|
|||
.filter(
|
||||
(p: P4CPicture) =>
|
||||
!loadedImages.data.has(p.pictureUrl) && // We don't show any image which is already linked
|
||||
!p.details.isSpherical,
|
||||
!p.details.isSpherical
|
||||
)
|
||||
.slice(0, 25),
|
||||
[loadedImages],
|
||||
[loadedImages]
|
||||
)
|
||||
|
||||
let asFeatures = result.map(p4cs => p4cs.map(p4c => (<Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
|
||||
},
|
||||
properties: {
|
||||
id: p4c.pictureUrl,
|
||||
rotation: p4c.direction,
|
||||
},
|
||||
})))
|
||||
let asFeatures = result.map((p4cs) =>
|
||||
p4cs.map(
|
||||
(p4c) =>
|
||||
<Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [p4c.coordinates.lng, p4c.coordinates.lat],
|
||||
},
|
||||
properties: {
|
||||
id: p4c.pictureUrl,
|
||||
rotation: p4c.direction,
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
let selected = new UIEventSource<P4CPicture>(undefined)
|
||||
let selectedAsFeature = selected.mapD(s => {
|
||||
return [<Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [s.coordinates.lng, s.coordinates.lat],
|
||||
let selectedAsFeature = selected.mapD((s) => {
|
||||
return [
|
||||
<Feature<Point>>{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [s.coordinates.lng, s.coordinates.lat],
|
||||
},
|
||||
properties: {
|
||||
id: s.pictureUrl,
|
||||
selected: "yes",
|
||||
rotation: s.direction,
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
id: s.pictureUrl,
|
||||
selected: "yes",
|
||||
rotation: s.direction,
|
||||
},
|
||||
}]
|
||||
]
|
||||
})
|
||||
|
||||
let someLoading = imageState.state.mapD((stateRecord) =>
|
||||
Object.values(stateRecord).some((v) => v === "loading"),
|
||||
Object.values(stateRecord).some((v) => v === "loading")
|
||||
)
|
||||
let errors = imageState.state.mapD((stateRecord) =>
|
||||
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error"),
|
||||
Object.keys(stateRecord).filter((k) => stateRecord[k] === "error")
|
||||
)
|
||||
let highlighted = new UIEventSource<string>(undefined)
|
||||
|
||||
onDestroy(highlighted.addCallbackD(hl => {
|
||||
const p4c = result.data?.find(i => i.pictureUrl === hl)
|
||||
onDestroy(
|
||||
highlighted.addCallbackD((hl) => {
|
||||
const p4c = result.data?.find((i) => i.pictureUrl === hl)
|
||||
selected.set(p4c)
|
||||
},
|
||||
))
|
||||
})
|
||||
)
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let mapProperties = new MapLibreAdaptor(map, {
|
||||
|
|
@ -104,7 +111,6 @@
|
|||
location: new UIEventSource({ lon, lat }),
|
||||
})
|
||||
|
||||
|
||||
const geocodedImageLayer = new LayerConfig(<LayerConfigJson>geocoded_image)
|
||||
new ShowDataLayer(map, {
|
||||
features: new StaticFeatureSource(asFeatures),
|
||||
|
|
@ -115,15 +121,10 @@
|
|||
},
|
||||
})
|
||||
|
||||
|
||||
ShowDataLayer.showMultipleLayers(
|
||||
map,
|
||||
new StaticFeatureSource([feature]),
|
||||
state.theme.layers,
|
||||
)
|
||||
ShowDataLayer.showMultipleLayers(map, new StaticFeatureSource([feature]), state.theme.layers)
|
||||
|
||||
onDestroy(
|
||||
asFeatures.addCallbackAndRunD(features => {
|
||||
asFeatures.addCallbackAndRunD((features) => {
|
||||
if (features.length == 0) {
|
||||
return
|
||||
}
|
||||
|
|
@ -132,7 +133,7 @@
|
|||
bbox = bbox.unionWith(BBox.get(f))
|
||||
}
|
||||
mapProperties.maxbounds.set(bbox.pad(4))
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
new ShowDataLayer(map, {
|
||||
|
|
@ -142,8 +143,6 @@
|
|||
highlighted.set(feature.properties.id)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
|
@ -158,16 +157,23 @@
|
|||
{:else}
|
||||
<div class="flex w-full space-x-4 overflow-x-auto" style="scroll-snap-type: x proximity">
|
||||
{#each $result as image (image.pictureUrl)}
|
||||
<span class="w-fit shrink-0" style="scroll-snap-align: start"
|
||||
on:mouseenter={() => {highlighted.set(image.pictureUrl)}}
|
||||
on:mouseleave={() =>{ highlighted.set(undefined); selected.set(undefined)}}
|
||||
<span
|
||||
class="w-fit shrink-0"
|
||||
style="scroll-snap-align: start"
|
||||
on:mouseenter={() => {
|
||||
highlighted.set(image.pictureUrl)
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
highlighted.set(undefined)
|
||||
selected.set(undefined)
|
||||
}}
|
||||
>
|
||||
<LinkableImage {tags} {image} {state} {feature} {layer} {linkable} {highlighted} />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="w-full flex flex-wrap justify-end gap-x-8 pt-2">
|
||||
<div class="flex w-full flex-wrap justify-end gap-x-8 pt-2">
|
||||
<PanoramaxLink
|
||||
large={false}
|
||||
mapProperties={{ zoom: new ImmutableStore(16), location: new ImmutableStore({ lon, lat }) }}
|
||||
|
|
@ -178,7 +184,6 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="my-2 flex justify-between">
|
||||
<div>
|
||||
{#if $someLoading && $result.length > 0}
|
||||
|
|
@ -193,7 +198,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="h-48">
|
||||
<MaplibreMap interactive={false} {map} {mapProperties} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,13 @@
|
|||
</script>
|
||||
|
||||
{#if enableLogin.data}
|
||||
<button on:click={() => {shown.set(!shown.data)}}><Tr t={t.seeNearby}/> </button>
|
||||
<button
|
||||
on:click={() => {
|
||||
shown.set(!shown.data)
|
||||
}}
|
||||
>
|
||||
<Tr t={t.seeNearby} />
|
||||
</button>
|
||||
<Popup {shown} bodyPadding="p-4">
|
||||
<span slot="header">
|
||||
<Tr t={t.seeNearby} />
|
||||
|
|
|
|||
|
|
@ -49,16 +49,20 @@
|
|||
}
|
||||
|
||||
if (layer?.id === "note") {
|
||||
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(tags.data.id,
|
||||
const uploadResult = await state?.imageUploadManager.uploadImageWithLicense(
|
||||
tags.data.id,
|
||||
state.osmConnection.userDetails.data?.name ?? "Anonymous",
|
||||
file, "image", noBlur)
|
||||
file,
|
||||
"image",
|
||||
noBlur
|
||||
)
|
||||
if (!uploadResult) {
|
||||
return
|
||||
}
|
||||
const url = uploadResult.absoluteUrl
|
||||
await state.osmConnection.addCommentToNote(tags.data.id, url)
|
||||
NoteCommentElement.addCommentTo(url, <UIEventSource<any>>tags, {
|
||||
osmConnection: state.osmConnection
|
||||
osmConnection: state.osmConnection,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
|
@ -88,7 +92,7 @@
|
|||
multiple={true}
|
||||
on:submit={(e) => handleFiles(e.detail)}
|
||||
>
|
||||
<div class="flex items-center text-2xl w-full justify-center">
|
||||
<div class="flex w-full items-center justify-center text-2xl">
|
||||
{#if image !== undefined}
|
||||
<img src={image} aria-hidden="true" />
|
||||
{:else}
|
||||
|
|
@ -98,19 +102,17 @@
|
|||
{labelText}
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
|
||||
<Tr t={t.addPicture} />
|
||||
{#if noBlur}
|
||||
<span class="subtle text-sm">
|
||||
<Tr t={t.upload.noBlur}/>
|
||||
</span>
|
||||
<span class="subtle text-sm">
|
||||
<Tr t={t.upload.noBlur} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</FileSelector>
|
||||
<div class="text-xs subtle italic">
|
||||
<div class="subtle text-xs italic">
|
||||
<Tr t={Translations.t.general.attribution.panoramaxLicenseCCBYSA} />
|
||||
<span class="mx-1">—</span>
|
||||
<Tr t={t.respectPrivacy} />
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
let mla = new MapLibreAdaptor(map, mapProperties)
|
||||
mla.allowMoving.setData(false)
|
||||
mla.allowZooming.setData(false)
|
||||
state?.mapProperties?.rasterLayer?.addCallbackAndRunD(l => mla.rasterLayer.set(l))
|
||||
state?.mapProperties?.rasterLayer?.addCallbackAndRunD((l) => mla.rasterLayer.set(l))
|
||||
|
||||
let directionElem: HTMLElement | undefined
|
||||
$: value.addCallbackAndRunD((degrees) => {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
export let wd: number
|
||||
export let h: number
|
||||
export let type: "full" | "half"
|
||||
let dispatch = createEventDispatcher<{ "start", "end", "move","clear" }>()
|
||||
let dispatch = createEventDispatcher<{ start; end; move; clear }>()
|
||||
let element: HTMLElement
|
||||
|
||||
function send(signal: "start" | "end" | "move", ev: Event) {
|
||||
|
|
@ -39,32 +39,30 @@
|
|||
element.onmouseenter = (ev) => send("move", ev)
|
||||
element.onmouseup = (ev) => send("end", ev)
|
||||
|
||||
element.addEventListener("touchstart", ev => dispatch("start", ev))
|
||||
element.addEventListener("touchend", ev => {
|
||||
|
||||
element.addEventListener("touchstart", (ev) => dispatch("start", ev))
|
||||
element.addEventListener("touchend", (ev) => {
|
||||
const el = elementUnderTouch(ev)
|
||||
if (el?.onmouseup) {
|
||||
el?.onmouseup(<any>ev)
|
||||
}else{
|
||||
} else {
|
||||
// We dragged outside of the table
|
||||
dispatch("clear")
|
||||
}
|
||||
|
||||
})
|
||||
element.addEventListener("touchmove", ev => {
|
||||
element.addEventListener("touchmove", (ev) => {
|
||||
const underTouch = elementUnderTouch(ev)
|
||||
if(typeof underTouch?.onmouseenter !== "function"){
|
||||
return
|
||||
}
|
||||
if (typeof underTouch?.onmouseenter !== "function") {
|
||||
return
|
||||
}
|
||||
|
||||
underTouch.onmouseenter(<any>ev)
|
||||
underTouch.onmouseenter(<any>ev)
|
||||
})
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<td bind:this={element} id={"oh-"+type+"-"+h+"-"+wd}
|
||||
class:border-black={(h + 1) % 6 === 0}
|
||||
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
|
||||
<td
|
||||
bind:this={element}
|
||||
id={"oh-" + type + "-" + h + "-" + wd}
|
||||
class:border-black={(h + 1) % 6 === 0}
|
||||
class={`oh-timecell oh-timecell-${type} oh-timecell-${wd} `}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { UIEventSource } from "../../../../Logic/UIEventSource"
|
||||
import type { OpeningHour } from "../../../OpeningHours/OpeningHours"
|
||||
import { OH as OpeningHours } from "../../../OpeningHours/OpeningHours"
|
||||
|
|
@ -27,10 +26,9 @@
|
|||
let element: HTMLTableElement
|
||||
|
||||
function range(n: number) {
|
||||
return Utils.TimesT(n, n => n)
|
||||
return Utils.TimesT(n, (n) => n)
|
||||
}
|
||||
|
||||
|
||||
function clearSelection() {
|
||||
const allCells = Array.from(document.getElementsByClassName("oh-timecell"))
|
||||
for (const timecell of allCells) {
|
||||
|
|
@ -38,27 +36,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function setSelectionNormalized(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
|
||||
function setSelectionNormalized(
|
||||
weekdayStart: number,
|
||||
weekdayEnd: number,
|
||||
hourStart: number,
|
||||
hourEnd: number
|
||||
) {
|
||||
for (let wd = weekdayStart; wd <= weekdayEnd; wd++) {
|
||||
for (let h = (hourStart); h < (hourEnd); h++) {
|
||||
for (let h = hourStart; h < hourEnd; h++) {
|
||||
h = Math.floor(h)
|
||||
if (h >= hourStart && h < hourEnd) {
|
||||
const elFull = document.getElementById("oh-full-" + h + "-" + wd)
|
||||
elFull?.classList?.add("oh-timecell-selected")
|
||||
}
|
||||
if (h + 0.5 < hourEnd) {
|
||||
|
||||
const elHalf = document.getElementById("oh-half-" + h + "-" + wd)
|
||||
elHalf?.classList?.add("oh-timecell-selected")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function setSelection(weekdayStart: number, weekdayEnd: number, hourStart: number, hourEnd: number) {
|
||||
function setSelection(
|
||||
weekdayStart: number,
|
||||
weekdayEnd: number,
|
||||
hourStart: number,
|
||||
hourEnd: number
|
||||
) {
|
||||
let hourA = hourStart
|
||||
let hourB = hourEnd
|
||||
if (hourA > hourB) {
|
||||
|
|
@ -69,8 +73,12 @@
|
|||
hourA -= 0.5
|
||||
hourB += 0.5
|
||||
}
|
||||
setSelectionNormalized(Math.min(weekdayStart, weekdayEnd), Math.max(weekdayStart, weekdayEnd),
|
||||
hourA, hourB)
|
||||
setSelectionNormalized(
|
||||
Math.min(weekdayStart, weekdayEnd),
|
||||
Math.max(weekdayStart, weekdayEnd),
|
||||
hourA,
|
||||
hourB
|
||||
)
|
||||
}
|
||||
|
||||
let selectionStart: [number, number] = undefined
|
||||
|
|
@ -100,8 +108,11 @@
|
|||
let startMinutes = Math.round((start * 60) % 60)
|
||||
let endMinutes = Math.round((end * 60) % 60)
|
||||
let newOhs = [...value.data]
|
||||
for (let wd = Math.min(selectionStart[0], weekday); wd <= Math.max(selectionStart[0], weekday); wd++) {
|
||||
|
||||
for (
|
||||
let wd = Math.min(selectionStart[0], weekday);
|
||||
wd <= Math.max(selectionStart[0], weekday);
|
||||
wd++
|
||||
) {
|
||||
const oh: OpeningHour = {
|
||||
startHour: Math.floor(start),
|
||||
endHour: Math.floor(end),
|
||||
|
|
@ -116,7 +127,6 @@
|
|||
clearSelection()
|
||||
}
|
||||
|
||||
|
||||
let lasttouched: [number, number] = undefined
|
||||
|
||||
function moved(weekday: number, hour: number) {
|
||||
|
|
@ -125,11 +135,10 @@
|
|||
clearSelection()
|
||||
setSelection(selectionStart[0], weekday, selectionStart[1], hour + 0.5)
|
||||
}
|
||||
const allRows = Array.from(element.getElementsByTagName("tr"))
|
||||
const allRows = Array.from(element.getElementsByTagName("tr"))
|
||||
for (const r of allRows) {
|
||||
r.classList.remove("hover")
|
||||
r.classList.remove("hovernext")
|
||||
|
||||
}
|
||||
const selectedRow = allRows[hour * 2 + 2]
|
||||
selectedRow?.classList?.add("hover")
|
||||
|
|
@ -158,26 +167,33 @@
|
|||
* @param oh
|
||||
*/
|
||||
function rangeStyle(oh: OpeningHour, totalHeight: number): string {
|
||||
const top = (oh.startHour + oh.startMinutes / 60) * totalHeight / 24
|
||||
const height = (oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight / 24
|
||||
const top = ((oh.startHour + oh.startMinutes / 60) * totalHeight) / 24
|
||||
const height =
|
||||
((oh.endHour - oh.startHour + (oh.endMinutes - oh.startMinutes) / 60) * totalHeight) / 24
|
||||
return `top: ${top}px; height: ${height}px; z-index: 20`
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<table
|
||||
bind:this={element}
|
||||
class="oh-table no-weblate w-full" cellspacing="0" cellpadding="0"
|
||||
class:hasselection={selectionStart !== undefined} class:hasnoselection={selectionStart === undefined}
|
||||
on:mouseleave={mouseLeft}>
|
||||
class="oh-table no-weblate w-full"
|
||||
cellspacing="0"
|
||||
cellpadding="0"
|
||||
class:hasselection={selectionStart !== undefined}
|
||||
class:hasnoselection={selectionStart === undefined}
|
||||
on:mouseleave={mouseLeft}
|
||||
>
|
||||
<tr>
|
||||
<!-- Header row -->
|
||||
<th style="width: 9%">
|
||||
<!-- Top-left cell -->
|
||||
<slot name="top-left">
|
||||
<button class="absolute top-0 left-0 p-1 rounded-full" on:click={() => value.set([])} style="z-index: 10">
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
<button
|
||||
class="absolute top-0 left-0 rounded-full p-1"
|
||||
on:click={() => value.set([])}
|
||||
style="z-index: 10"
|
||||
>
|
||||
<TrashIcon class="h-5 w-5" />
|
||||
</button>
|
||||
</slot>
|
||||
</th>
|
||||
|
|
@ -188,101 +204,116 @@
|
|||
{/each}
|
||||
</tr>
|
||||
|
||||
<tr class="h-0 nobold">
|
||||
<tr class="nobold h-0">
|
||||
<!-- Virtual row to add the ranges to-->
|
||||
<td style="width: 9%" />
|
||||
{#each range(7) as wd}
|
||||
<td style="width: 13%; position: relative;">
|
||||
|
||||
<div class="h-0 pointer-events-none" style="z-index: 10">
|
||||
{#each $value.filter(oh => oh.weekday === wd).map(oh => OpeningHours.rangeAs24Hr(oh)) as range }
|
||||
<div class="absolute pointer-events-none px-1 md:px-2 w-full "
|
||||
style={rangeStyle(range, totalHeight)}
|
||||
<div class="pointer-events-none h-0" style="z-index: 10">
|
||||
{#each $value
|
||||
.filter((oh) => oh.weekday === wd)
|
||||
.map((oh) => OpeningHours.rangeAs24Hr(oh)) as range}
|
||||
<div
|
||||
class="pointer-events-none absolute w-full px-1 md:px-2"
|
||||
style={rangeStyle(range, totalHeight)}
|
||||
>
|
||||
<div class="rounded-xl border-interactive h-full low-interaction flex flex-col justify-between">
|
||||
<div
|
||||
class="border-interactive low-interaction flex h-full flex-col justify-between rounded-xl"
|
||||
>
|
||||
<div class:hidden={range.endHour - range.startHour < 3}>
|
||||
{OpeningHours.hhmm(range.startHour, range.startMinutes)}
|
||||
</div>
|
||||
<button class="w-fit rounded-full p-1 self-center pointer-events-auto"
|
||||
on:click={() => {
|
||||
const cleaned = value.data.filter(v => !OpeningHours.isSame(v, range))
|
||||
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
|
||||
value.set(cleaned)
|
||||
}}>
|
||||
<TrashIcon class="w-6 h-6" />
|
||||
<button
|
||||
class="pointer-events-auto w-fit self-center rounded-full p-1"
|
||||
on:click={() => {
|
||||
const cleaned = value.data.filter((v) => !OpeningHours.isSame(v, range))
|
||||
console.log("Cleaned", cleaned, OpeningHours.ToString(value.data))
|
||||
value.set(cleaned)
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="h-6 w-6" />
|
||||
</button>
|
||||
<div class:hidden={range.endHour - range.startHour < 3}>
|
||||
{OpeningHours.hhmm(range.endHour, range.endMinutes)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/each}
|
||||
|
||||
</tr>
|
||||
|
||||
{#each range(24) as h}
|
||||
<tr style="height: 0.75rem; width: 9%"> <!-- even row, for the hour -->
|
||||
<td rowspan={ h > 0 ? 2: 1 }
|
||||
class="relative text-sm sm:text-base oh-left-col oh-timecell-full border-box interactive "
|
||||
style={ h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}>
|
||||
<tr style="height: 0.75rem; width: 9%">
|
||||
<!-- even row, for the hour -->
|
||||
<td
|
||||
rowspan={h > 0 ? 2 : 1}
|
||||
class="oh-left-col oh-timecell-full border-box interactive relative text-sm sm:text-base"
|
||||
style={h > 0 ? "top: -0.75rem" : "height:0; top: -0.75rem"}
|
||||
>
|
||||
{#if h > 0}
|
||||
<span class="hour-header w-full">
|
||||
{h}:00
|
||||
{h}:00
|
||||
</span>
|
||||
{/if}
|
||||
</td>
|
||||
{#each range(7) as wd}
|
||||
<OHCell type="full" {h} {wd} on:start={() => startSelection(wd, h)} on:end={() => endSelection(wd, h)}
|
||||
on:move={() => moved(wd, h)} on:clear={() => clearSelection()} />
|
||||
<OHCell
|
||||
type="full"
|
||||
{h}
|
||||
{wd}
|
||||
on:start={() => startSelection(wd, h)}
|
||||
on:end={() => endSelection(wd, h)}
|
||||
on:move={() => moved(wd, h)}
|
||||
on:clear={() => clearSelection()}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
<tr style="height: calc( 0.75rem - 1px) "> <!-- odd row, for the half hour -->
|
||||
<tr style="height: calc( 0.75rem - 1px) ">
|
||||
<!-- odd row, for the half hour -->
|
||||
{#if h === 0}
|
||||
<td/> <!-- extra cell to compensate for irregular header-->
|
||||
<td />
|
||||
<!-- extra cell to compensate for irregular header-->
|
||||
{/if}
|
||||
{#each range(7) as wd}
|
||||
<OHCell type="half" {h} {wd} on:start={() => startSelection(wd, h + 0.5)}
|
||||
on:end={() => endSelection(wd, h + 0.5)}
|
||||
on:move={() => moved(wd, h + 0.5)} on:clear={() => clearSelection()} />
|
||||
<OHCell
|
||||
type="half"
|
||||
{h}
|
||||
{wd}
|
||||
on:start={() => startSelection(wd, h + 0.5)}
|
||||
on:end={() => endSelection(wd, h + 0.5)}
|
||||
on:move={() => moved(wd, h + 0.5)}
|
||||
on:clear={() => clearSelection()}
|
||||
/>
|
||||
{/each}
|
||||
</tr>
|
||||
|
||||
{/each}
|
||||
|
||||
</table>
|
||||
|
||||
<style>
|
||||
th {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
th {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
|
||||
.hasselection tr:hover .hour-header, .hasselection tr.hover .hour-header {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
|
||||
.hasselection tr:hover + tr {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hasselection tr.hovernext {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hasnoselection tr:hover, .hasnoselection tr.hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hasselection tr:hover .hour-header,
|
||||
.hasselection tr.hover .hour-header {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
.hasselection tr:hover + tr {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hasselection tr.hovernext {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hasnoselection tr:hover,
|
||||
.hasnoselection tr.hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
let postfix = ""
|
||||
if (args) {
|
||||
try {
|
||||
|
||||
const data = JSON.stringify(args)
|
||||
if (data["prefix"]) {
|
||||
prefix = data["prefix"]
|
||||
|
|
@ -31,11 +30,15 @@
|
|||
const state = new OpeningHoursState(value, prefix, postfix)
|
||||
let expanded = new UIEventSource(false)
|
||||
</script>
|
||||
|
||||
<Popup bodyPadding="p-0" shown={expanded}>
|
||||
<OHTable value={state.normalOhs} />
|
||||
<button on:click={() => expanded.set(false)} class="absolute left-0 bottom-0 primary pointer-events-auto h-8 w-10 rounded-full">
|
||||
<Check class="shrink-0 w-6 h-6 m-0 p-0" color="white"/>
|
||||
</button>
|
||||
<button
|
||||
on:click={() => expanded.set(false)}
|
||||
class="primary pointer-events-auto absolute left-0 bottom-0 h-8 w-10 rounded-full"
|
||||
>
|
||||
<Check class="m-0 h-6 w-6 shrink-0 p-0" color="white" />
|
||||
</button>
|
||||
</Popup>
|
||||
<button on:click={() => expanded.set(true)}>Pick opening hours</button>
|
||||
<PublicHolidaySelector value={state.phSelectorValue} />
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export default class Validators {
|
|||
"velopark",
|
||||
"nsi",
|
||||
"currency",
|
||||
"regex"
|
||||
"regex",
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
|
@ -94,7 +94,7 @@ export default class Validators {
|
|||
new VeloparkValidator(),
|
||||
new NameSuggestionIndexValidator(),
|
||||
new CurrencyValidator(),
|
||||
new RegexValidator()
|
||||
new RegexValidator(),
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
|
|||
|
|
@ -7,23 +7,24 @@ export default class OpeningHoursValidator extends Validator {
|
|||
"opening_hours",
|
||||
[
|
||||
"Has extra elements to easily input when a POI is opened.",
|
||||
("### Helper arguments"),
|
||||
"### Helper arguments",
|
||||
"Only one helper argument named `options` can be provided. It is a JSON-object of type `{ prefix: string, postfix: string }`:",
|
||||
MarkdownUtils.table(
|
||||
["subarg", "doc"],
|
||||
[
|
||||
[
|
||||
"prefix",
|
||||
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse."
|
||||
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
|
||||
],
|
||||
[
|
||||
"postfix",
|
||||
"Piece of text that will always be added to the end of the generated opening hours"
|
||||
]
|
||||
]),
|
||||
("### Example usage"),
|
||||
"Piece of text that will always be added to the end of the generated opening hours",
|
||||
],
|
||||
]
|
||||
),
|
||||
"### Example usage",
|
||||
"To add a conditional (based on time) access restriction:\n\n```\n" +
|
||||
`
|
||||
`
|
||||
"freeform": {
|
||||
"key": "access:conditional",
|
||||
"type": "opening_hours",
|
||||
|
|
@ -34,7 +35,7 @@ export default class OpeningHoursValidator extends Validator {
|
|||
}
|
||||
]
|
||||
}` +
|
||||
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`"
|
||||
"\n```\n\n*Don't forget to pass the prefix and postfix in the rendering as well*: `{opening_hours_table(opening_hours,yes @ &LPARENS, &RPARENS )`",
|
||||
].join("\n")
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,15 +48,12 @@ export default class PhoneValidator extends Validator {
|
|||
}
|
||||
let countryCode: CountryCode = undefined
|
||||
if (country) {
|
||||
countryCode = <CountryCode> country()?.toUpperCase()
|
||||
countryCode = <CountryCode>country()?.toUpperCase()
|
||||
}
|
||||
if (this.isShortCode(str, countryCode)) {
|
||||
return str
|
||||
}
|
||||
return parsePhoneNumberFromString(
|
||||
str,
|
||||
countryCode
|
||||
)?.formatInternational()
|
||||
return parsePhoneNumberFromString(str, countryCode)?.formatInternational()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ import { s } from "vitest/dist/env-afee91f0"
|
|||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class RegexValidator extends StringValidator{
|
||||
export default class RegexValidator extends StringValidator {
|
||||
constructor() {
|
||||
super("regex", "Validates a regex")
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation | undefined {
|
||||
try{
|
||||
try {
|
||||
new RegExp(s)
|
||||
}catch (e) {
|
||||
return Translations.T("Not a valid Regex: "+e)
|
||||
} catch (e) {
|
||||
return Translations.T("Not a valid Regex: " + e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,12 +17,12 @@ export default class WikidataValidator extends Validator {
|
|||
[
|
||||
[
|
||||
"key",
|
||||
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search"
|
||||
"the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search",
|
||||
],
|
||||
[
|
||||
"options",
|
||||
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below"
|
||||
]
|
||||
"A JSON-object of type `{ removePrefixes: Record<string, string[]>, removePostfixes: Record<string, string[]>, ... }`. See the more detailed explanation below",
|
||||
],
|
||||
]
|
||||
),
|
||||
"#### Suboptions",
|
||||
|
|
@ -31,28 +31,26 @@ export default class WikidataValidator extends Validator {
|
|||
[
|
||||
[
|
||||
"removePrefixes",
|
||||
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes"
|
||||
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
|
||||
],
|
||||
[
|
||||
"removePostfixes",
|
||||
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes."
|
||||
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
|
||||
],
|
||||
[
|
||||
"instanceOf",
|
||||
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans"
|
||||
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
|
||||
],
|
||||
[
|
||||
"notInstanceof",
|
||||
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results"
|
||||
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
|
||||
],
|
||||
[
|
||||
"multiple",
|
||||
"If 'yes' or 'true', will allow to select multiple values at once"
|
||||
]
|
||||
["multiple", "If 'yes' or 'true', will allow to select multiple values at once"],
|
||||
]
|
||||
)
|
||||
),
|
||||
].join("\n\n")
|
||||
private static readonly docsExampleUsage: string = "### Example usage\n\n" +
|
||||
private static readonly docsExampleUsage: string =
|
||||
"### Example usage\n\n" +
|
||||
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
|
||||
|
||||
\`\`\`json
|
||||
|
|
@ -96,9 +94,13 @@ Another example is to search for species and trees:
|
|||
\`\`\`
|
||||
`
|
||||
|
||||
|
||||
constructor() {
|
||||
super("wikidata", "A wikidata identifier, e.g. Q42.\n\n" + WikidataValidator.docs + WikidataValidator.docsExampleUsage)
|
||||
super(
|
||||
"wikidata",
|
||||
"A wikidata identifier, e.g. Q42.\n\n" +
|
||||
WikidataValidator.docs +
|
||||
WikidataValidator.docsExampleUsage
|
||||
)
|
||||
}
|
||||
|
||||
public isValid(str): boolean {
|
||||
|
|
|
|||
|
|
@ -157,18 +157,18 @@
|
|||
<LockClosed class={clss} {color} />
|
||||
{:else if icon === "key"}
|
||||
<Key class={clss} {color} />
|
||||
{:else if icon==="globe_alt"}
|
||||
{:else if icon === "globe_alt"}
|
||||
<GlobeAltIcon class={clss} {color} />
|
||||
{:else if icon === "building_office_2"}
|
||||
<BuildingOffice2 class={clss} {color} />
|
||||
{:else if icon === "house"}
|
||||
<HomeIcon class={clss} {color} />
|
||||
{:else if icon === "train"}
|
||||
<Train {color} class={clss}/>
|
||||
{:else if icon === "train"}
|
||||
<Train {color} class={clss} />
|
||||
{:else if icon === "airport"}
|
||||
<Airport {color} class={clss}/>
|
||||
<Airport {color} class={clss} />
|
||||
{:else if icon === "building_storefront"}
|
||||
<BuildingStorefront {color} class={clss}/>
|
||||
<BuildingStorefront {color} class={clss} />
|
||||
{:else if icon === "snap"}
|
||||
<Snap class={clss} />
|
||||
{:else if Utils.isEmoji(icon)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
|
||||
import maplibregl, {
|
||||
Map as MLMap,
|
||||
Map as MlMap,
|
||||
ScaleControl,
|
||||
SourceSpecification,
|
||||
} from "maplibre-gl"
|
||||
import { RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { Utils } from "../../Utils"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
|
|
@ -23,13 +28,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
"dragRotate",
|
||||
"dragPan",
|
||||
"keyboard",
|
||||
"touchZoomRotate"
|
||||
"touchZoomRotate",
|
||||
]
|
||||
private static maplibre_zoom_handlers = [
|
||||
"scrollZoom",
|
||||
"boxZoom",
|
||||
"doubleClickZoom",
|
||||
"touchZoomRotate"
|
||||
"touchZoomRotate",
|
||||
]
|
||||
readonly location: UIEventSource<{ lon: number; lat: number }>
|
||||
private readonly isFlying = new UIEventSource(false)
|
||||
|
|
@ -225,7 +230,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
this.allowZooming.addCallbackAndRun((allowZooming) => self.setAllowZooming(allowZooming))
|
||||
this.bounds.addCallbackAndRunD((bounds) => self.setBounds(bounds))
|
||||
this.useTerrain?.addCallbackAndRun((useTerrain) => self.setTerrain(useTerrain))
|
||||
this.showScale?.addCallbackAndRun(showScale => self.setScale(showScale))
|
||||
this.showScale?.addCallbackAndRun((showScale) => self.setScale(showScale))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,9 +245,9 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return {
|
||||
map: mlmap,
|
||||
ui: new SvelteUIElement(MaplibreMap, {
|
||||
map: mlmap
|
||||
map: mlmap,
|
||||
}),
|
||||
mapproperties: new MapLibreAdaptor(mlmap)
|
||||
mapproperties: new MapLibreAdaptor(mlmap),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -310,7 +315,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
) {
|
||||
const event = {
|
||||
date: new Date(),
|
||||
key: key
|
||||
key: key,
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._onKeyNavigation.length; i++) {
|
||||
|
|
@ -499,7 +504,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
const bounds = map.getBounds()
|
||||
const bbox = new BBox([
|
||||
[bounds.getEast(), bounds.getNorth()],
|
||||
[bounds.getWest(), bounds.getSouth()]
|
||||
[bounds.getWest(), bounds.getSouth()],
|
||||
])
|
||||
if (this.bounds.data === undefined || !isSetup) {
|
||||
this.bounds.setData(bbox)
|
||||
|
|
@ -693,14 +698,14 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
type: "raster-dem",
|
||||
url:
|
||||
"https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=" +
|
||||
Constants.maptilerApiKey
|
||||
Constants.maptilerApiKey,
|
||||
})
|
||||
try {
|
||||
while (!map?.isStyleLoaded()) {
|
||||
await Utils.waitFor(250)
|
||||
}
|
||||
map.setTerrain({
|
||||
source: id
|
||||
source: id,
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
@ -716,17 +721,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return
|
||||
}
|
||||
if (!showScale) {
|
||||
if(this.scaleControl){
|
||||
if (this.scaleControl) {
|
||||
map.removeControl(this.scaleControl)
|
||||
this.scaleControl = undefined
|
||||
}
|
||||
return
|
||||
}
|
||||
if (this.scaleControl === undefined) {
|
||||
|
||||
this.scaleControl = new ScaleControl({
|
||||
maxWidth: 100,
|
||||
unit: "metric"
|
||||
unit: "metric",
|
||||
})
|
||||
}
|
||||
if (!map.hasControl(this.scaleControl)) {
|
||||
|
|
@ -739,7 +743,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
window.requestAnimationFrame(() => {
|
||||
this._maplibreMap.data?.flyTo({
|
||||
zoom,
|
||||
center: [lon, lat]
|
||||
center: [lon, lat],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,8 +48,11 @@ class PointRenderingLayer {
|
|||
this._onClick = onClick
|
||||
this._selectedElement = selectedElement
|
||||
const self = this
|
||||
if(!features?.features){
|
||||
throw "Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is "+layer.id
|
||||
if (!features?.features) {
|
||||
throw (
|
||||
"Could not setup a PointRenderingLayer; features?.features is undefined/null. The layer is " +
|
||||
layer.id
|
||||
)
|
||||
}
|
||||
features.features?.addCallbackAndRunD((features) => self.updateFeatures(features))
|
||||
visibility?.addCallbackAndRunD((visible) => {
|
||||
|
|
@ -163,7 +166,7 @@ class PointRenderingLayer {
|
|||
})
|
||||
|
||||
if (this._onClick) {
|
||||
el.addEventListener("click", (ev)=> {
|
||||
el.addEventListener("click", (ev) => {
|
||||
ev.preventDefault()
|
||||
this._onClick(feature)
|
||||
// Workaround to signal the MapLibreAdaptor to ignore this click
|
||||
|
|
|
|||
|
|
@ -859,8 +859,14 @@ This list will be sorted
|
|||
return ranges
|
||||
}
|
||||
|
||||
public static isSame(a: OpeningHour, b: OpeningHour){
|
||||
return a.weekday === b.weekday && a.startHour === b.startHour && a.startMinutes === b.startMinutes && a.endHour === b.endHour && a.endMinutes === b.endMinutes
|
||||
public static isSame(a: OpeningHour, b: OpeningHour) {
|
||||
return (
|
||||
a.weekday === b.weekday &&
|
||||
a.startHour === b.startHour &&
|
||||
a.startMinutes === b.startMinutes &&
|
||||
a.endHour === b.endHour &&
|
||||
a.endMinutes === b.endMinutes
|
||||
)
|
||||
}
|
||||
private static multiply(
|
||||
weekdays: number[],
|
||||
|
|
@ -930,11 +936,12 @@ This list will be sorted
|
|||
* OH.rangeAs24Hr(oh).endHour // => 24
|
||||
*/
|
||||
static rangeAs24Hr(oh: OpeningHour) {
|
||||
if(oh.endHour === 0){
|
||||
return {
|
||||
...oh, endHour : 24
|
||||
}
|
||||
}
|
||||
if (oh.endHour === 0) {
|
||||
return {
|
||||
...oh,
|
||||
endHour: 24,
|
||||
}
|
||||
}
|
||||
return oh
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue