Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-10-11 04:34:57 +02:00
commit 08ffe4b7c0
146 changed files with 4380 additions and 1435 deletions

View file

@ -0,0 +1,13 @@
export {}
window.addEventListener("load", async () => {
if (!("serviceWorker" in navigator)) {
console.log("Service workers are not supported")
return
}
try {
await navigator.serviceWorker.register("/service-worker.js")
console.log("Service worker registration successful")
} catch (err) {
console.error("Service worker registration failed", err)
}
})

View file

@ -55,8 +55,13 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
* @private
*/
private handleChange(change: ChangeDescription): boolean {
const allElementStorage = this._allElementStorage
if (change.changes === undefined) {
// The geometry is not described - not a new point or geometry change, but probably a tagchange to a newly created point
// Not something that should be handled here
return false
}
const allElementStorage = this._allElementStorage
console.log("Handling pending change", change)
if (change.id > 0) {
// This is an already existing object
@ -85,10 +90,6 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
this._featureProperties.trackFeature(feature)
this.addNewFeature(feature)
return true
} else if (change.changes === undefined) {
// The geometry is not described - not a new point or geometry change, but probably a tagchange to a newly created point
// Not something that should be handled here
return false
}
try {
@ -150,7 +151,7 @@ export class NewGeometryFromChangesFeatureSource implements WritableFeatureSourc
continue
}
somethingChanged ||= this.handleChange(change)
somethingChanged = this.handleChange(change) || somethingChanged // important: _first_ evaluate the method, to avoid shortcutting
}
if (somethingChanged) {
this.features.ping()

View file

@ -23,27 +23,27 @@ export default class AllImageProviders {
)
),
]
public static apiUrls: string[] = [].concat(
...AllImageProviders.ImageAttributionSource.map((src) => src.apiUrls())
)
public static defaultKeys = [].concat(
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
)
private static providersByName = {
imgur: Imgur.singleton,
mapillary: Mapillary.singleton,
wikidata: WikidataImageProvider.singleton,
wikimedia: WikimediaImageProvider.singleton,
}
public static byName(name: string) {
return AllImageProviders.providersByName[name.toLowerCase()]
}
public static defaultKeys = [].concat(
AllImageProviders.ImageAttributionSource.map((provider) => provider.defaultKeyPrefixes)
)
private static _cache: Map<string, UIEventSource<ProvidedImage[]>> = new Map<
string,
UIEventSource<ProvidedImage[]>
>()
public static byName(name: string) {
return AllImageProviders.providersByName[name.toLowerCase()]
}
public static LoadImagesFor(
tags: Store<Record<string, string>>,
tagKey?: string[]

View file

@ -3,6 +3,10 @@ import ImageProvider, { ProvidedImage } from "./ImageProvider"
export default class GenericImageProvider extends ImageProvider {
public defaultKeyPrefixes: string[] = ["image"]
public apiUrls(): string[] {
return []
}
private readonly _valuePrefixBlacklist: string[]
public constructor(valuePrefixBlacklist: string[]) {

View file

@ -65,4 +65,6 @@ export default abstract class ImageProvider {
public abstract ExtractUrls(key: string, value: string): Promise<Promise<ProvidedImage>[]>
public abstract DownloadAttribution(url: string): Promise<LicenseInfo>
public abstract apiUrls(): string[]
}

View file

@ -10,10 +10,16 @@ export class Imgur extends ImageProvider implements ImageUploader {
public static readonly singleton = new Imgur()
public readonly defaultKeyPrefixes: string[] = ["image"]
public readonly maxFileSizeInMegabytes = 10
public static readonly apiUrl = "https://api.imgur.com/3/image"
private constructor() {
super()
}
apiUrls(): string[] {
return [Imgur.apiUrl]
}
/**
* Uploads an image, returns the URL where to find the image
* @param title
@ -25,7 +31,7 @@ export class Imgur extends ImageProvider implements ImageUploader {
description: string,
blob: File
): Promise<{ key: string; value: string }> {
const apiUrl = "https://api.imgur.com/3/image"
const apiUrl = Imgur.apiUrl
const apiKey = Constants.ImgurApiKey
const formData = new FormData()

View file

@ -17,6 +17,10 @@ export class Mapillary extends ImageProvider {
]
defaultKeyPrefixes = ["mapillary", "image"]
apiUrls(): string[] {
return ["https://mapillary.com", "https://www.mapillary.com", "https://graph.mapillary.com"]
}
/**
* Indicates that this is the same URL
* Ignores 'stp' parameter

View file

@ -5,6 +5,9 @@ import { WikimediaImageProvider } from "./WikimediaImageProvider"
import Wikidata from "../Web/Wikidata"
export class WikidataImageProvider extends ImageProvider {
public apiUrls(): string[] {
return Wikidata.neededUrls
}
public static readonly singleton = new WikidataImageProvider()
public readonly defaultKeyPrefixes = ["wikidata"]

View file

@ -11,11 +11,11 @@ import Wikimedia from "../Web/Wikimedia"
*/
export class WikimediaImageProvider extends ImageProvider {
public static readonly singleton = new WikimediaImageProvider()
public static readonly commonsPrefixes = [
public static readonly apiUrls = [
"https://commons.wikimedia.org/wiki/",
"https://upload.wikimedia.org",
"File:",
]
public static readonly commonsPrefixes = [...WikimediaImageProvider.apiUrls, "File:"]
private readonly commons_key = "wikimedia_commons"
public readonly defaultKeyPrefixes = [this.commons_key, "image"]
@ -66,6 +66,10 @@ export class WikimediaImageProvider extends ImageProvider {
return value
}
apiUrls(): string[] {
return WikimediaImageProvider.apiUrls
}
SourceIcon(backlink: string): BaseUIElement {
const img = Svg.wikimedia_commons_white_svg().SetStyle("width:2em;height: 2em")
if (backlink === undefined) {

View file

@ -1,6 +1,8 @@
import Constants from "../Models/Constants"
export default class Maproulette {
public static readonly defaultEndpoint = "https://maproulette.org/api/v2"
public static readonly STATUS_OPEN = 0
public static readonly STATUS_FIXED = 1
public static readonly STATUS_FALSE_POSITIVE = 2
@ -20,59 +22,28 @@ export default class Maproulette {
6: "Too hard",
9: "Disabled",
}
public static singleton = new Maproulette()
/*
* The API endpoint to use
*/
endpoint: string
/**
* The API key to use for all requests
*/
private readonly apiKey: string
public static singleton = new Maproulette()
/**
* Creates a new Maproulette instance
* @param endpoint The API endpoint to use
*/
constructor(endpoint: string = "https://maproulette.org/api/v2") {
this.endpoint = endpoint
constructor(endpoint?: string) {
this.endpoint = endpoint ?? Maproulette.defaultEndpoint
if (!this.endpoint) {
throw "MapRoulette endpoint is undefined. Make sure that `Maproulette.defaultEndpoint` is defined on top of the class"
}
this.apiKey = Constants.MaprouletteApiKey
}
/**
* Close a task; might throw an error
*
* Also see:https://maproulette.org/docs/swagger-ui/index.html?url=/assets/swagger.json&docExpansion=none#/Task/setTaskStatus
* @param taskId The task to close
* @param status A number indicating the status. Use MapRoulette.STATUS_*
* @param options Additional settings to pass. Refer to the API-docs for more information
*/
async closeTask(
taskId: number,
status = Maproulette.STATUS_FIXED,
options?: {
comment?: string
tags?: string
requestReview?: boolean
completionResponses?: Record<string, string>
}
): Promise<void> {
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
apiKey: this.apiKey,
},
body: options !== undefined ? JSON.stringify(options) : undefined,
})
if (response.status !== 204) {
console.log(`Failed to close task: ${response.status}`)
throw `Failed to close task: ${response.status}`
}
}
/**
* Converts a status text into the corresponding number
*
@ -91,4 +62,37 @@ export default class Maproulette {
}
return undefined
}
/**
* Close a task; might throw an error
*
* Also see:https://maproulette.org/docs/swagger-ui/index.html?url=/assets/swagger.json&docExpansion=none#/Task/setTaskStatus
* @param taskId The task to close
* @param status A number indicating the status. Use MapRoulette.STATUS_*
* @param options Additional settings to pass. Refer to the API-docs for more information
*/
async closeTask(
taskId: number,
status = Maproulette.STATUS_FIXED,
options?: {
comment?: string
tags?: string
requestReview?: boolean
completionResponses?: Record<string, string>
}
): Promise<void> {
console.log("Maproulette: setting", `${this.endpoint}/task/${taskId}/${status}`, options)
const response = await fetch(`${this.endpoint}/task/${taskId}/${status}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
apiKey: this.apiKey,
},
body: options !== undefined ? JSON.stringify(options) : undefined,
})
if (response.status !== 204) {
console.log(`Failed to close task: ${response.status}`)
throw `Failed to close task: ${response.status}`
}
}
}

View file

@ -0,0 +1,6 @@
export interface AuthConfig {
"#"?: string // optional comment
oauth_client_id: string
oauth_secret: string
url: string
}

View file

@ -525,11 +525,13 @@ export class Changes {
pending = pending.map((ch) => ChangeDescriptionTools.rewriteIds(ch, remappings))
console.log("Result is", pending)
}
const changes: {
newObjects: OsmObject[]
modifiedObjects: OsmObject[]
deletedObjects: OsmObject[]
} = self.CreateChangesetObjects(pending, objects)
return Changes.createChangesetFor("" + csId, changes)
},
metatags,
@ -558,19 +560,11 @@ export class Changes {
const successes = await Promise.all(
Array.from(pendingPerTheme, async ([theme, pendingChanges]) => {
try {
const openChangeset = this.state.osmConnection
.GetPreference("current-open-changeset-" + theme)
.sync(
(str) => {
const n = Number(str)
if (isNaN(n)) {
return undefined
}
return n
},
[],
(n) => "" + n
const openChangeset = UIEventSource.asInt(
this.state.osmConnection.GetPreference(
"current-open-changeset-" + theme
)
)
console.log(
"Using current-open-changeset-" +
theme +

View file

@ -1,5 +1,6 @@
import { Utils } from "../../Utils"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
export interface GeoCodeResult {
display_name: string
@ -15,7 +16,7 @@ export interface GeoCodeResult {
}
export class Geocoding {
private static readonly host = "https://nominatim.openstreetmap.org/search?"
public static readonly host = Constants.nominatimEndpoint
static async Search(query: string, bbox: BBox): Promise<GeoCodeResult[]> {
const b = bbox ?? BBox.global

View file

@ -4,7 +4,8 @@ import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences"
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import * as config from "../../../package.json"
import { AuthConfig } from "./AuthConfig"
import Constants from "../../Models/Constants"
export default class UserDetails {
public loggedIn = false
@ -25,18 +26,9 @@ export default class UserDetails {
}
}
export interface AuthConfig {
"#"?: string // optional comment
oauth_client_id: string
oauth_secret: string
url: string
}
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
export class OsmConnection {
public static readonly oauth_configs: Record<string, AuthConfig> =
config.config.oauth_credentials
public auth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
@ -53,7 +45,7 @@ export class OsmConnection {
public preferencesHandler: OsmPreferences
public readonly _oauth_config: AuthConfig
private readonly _dryRun: Store<boolean>
private fakeUser: boolean
private readonly fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
private readonly _iframeMode: Boolean | boolean
private readonly _singlePage: boolean
@ -65,15 +57,12 @@ export class OsmConnection {
oauth_token?: UIEventSource<string>
// Used to keep multiple changesets open and to write to the correct changeset
singlePage?: boolean
osmConfiguration?: "osm" | "osm-test"
attemptLogin?: true | boolean
}) {
options = options ?? {}
this.fakeUser = options.fakeUser ?? false
this._singlePage = options.singlePage ?? true
this._oauth_config =
OsmConnection.oauth_configs[options.osmConfiguration ?? "osm"] ??
OsmConnection.oauth_configs.osm
options ??= {}
this.fakeUser = options?.fakeUser ?? false
this._singlePage = options?.singlePage ?? true
this._oauth_config = Constants.osmAuthConfig
console.debug("Using backend", this._oauth_config.url)
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
@ -83,11 +72,8 @@ export class OsmConnection {
import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined
) {
console.debug("Using environment variables for oauth config")
this._oauth_config = {
oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID,
oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET,
url: "https://api.openstreetmap.org",
}
this._oauth_config.oauth_client_id = import.meta.env.VITE_OSM_OAUTH_CLIENT_ID
this._oauth_config.oauth_secret = import.meta.env.VITE_OSM_OAUTH_SECRET
}
this.userDetails = new UIEventSource<UserDetails>(

View file

@ -1,9 +1,17 @@
import { UIEventSource } from "../UIEventSource"
import UserDetails, { OsmConnection } from "./OsmConnection"
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
export class OsmPreferences {
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences")
/**
* A dictionary containing all the preferences. The 'preferenceSources' will be initialized from this
* We keep a local copy of them, to init mapcomplete with the previous choices and to be able to get the open changesets right away
*/
public preferences = LocalStorageSource.GetParsed<Record<string, string>>(
"all-osm-preferences",
{}
)
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
private auth: any
private userDetails: UIEventSource<UserDetails>

View file

@ -28,15 +28,8 @@ class FeatureSwitchUtils {
export class OsmConnectionFeatureSwitches {
public readonly featureSwitchFakeUser: UIEventSource<boolean>
public readonly featureSwitchApiURL: UIEventSource<string>
constructor() {
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
"backend",
"osm",
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
)
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter(
"fake-user",
false,
@ -143,7 +136,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
let testingDefaultValue = false
if (
this.featureSwitchApiURL.data !== "osm-test" &&
!Utils.runningFromConsole &&
(location.hostname === "localhost" || location.hostname === "127.0.0.1")
) {
@ -172,7 +164,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
(urls) => urls?.join(",")
)
this.overpassTimeout = UIEventSource.asFloat(
this.overpassTimeout = UIEventSource.asInt(
QueryParameters.GetQueryParameter(
"overpassTimeout",
"" + layoutToUse?.overpassTimeout,
@ -188,7 +180,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
)
)
this.osmApiTileSize = UIEventSource.asFloat(
this.osmApiTileSize = UIEventSource.asInt(
QueryParameters.GetQueryParameter(
"osmApiTileSize",
"" + layoutToUse?.osmApiTileSize,

View file

@ -612,6 +612,48 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
return src
}
/**
*
* @param source
* UIEventSource.asInt(new UIEventSource("123")).data // => 123
* UIEventSource.asInt(new UIEventSource("123456789")).data // => 123456789
*
* const srcStr = new UIEventSource("123456789"))
* const srcInt = UIEventSource.asInt(srcStr)
* srcInt.setData(987654321)
* srcStr.data // => "987654321"
*/
public static asInt(source: UIEventSource<string>): UIEventSource<number> {
return source.sync(
(str) => {
let parsed = parseInt(str)
return isNaN(parsed) ? undefined : parsed
},
[],
(fl) => {
if (fl === undefined || isNaN(fl)) {
return undefined
}
return "" + fl
}
)
}
/**
* UIEventSource.asFloat(new UIEventSource("123")).data // => 123
* UIEventSource.asFloat(new UIEventSource("123456789")).data // => 123456789
* UIEventSource.asFloat(new UIEventSource("0.5")).data // => 0.5
* UIEventSource.asFloat(new UIEventSource("0.125")).data // => 0.125
* UIEventSource.asFloat(new UIEventSource("0.0000000001")).data // => 0.0000000001
*
*
* const srcStr = new UIEventSource("123456789"))
* const srcInt = UIEventSource.asFloat(srcStr)
* srcInt.setData(987654321)
* srcStr.data // => "987654321"
* @param source
*/
public static asFloat(source: UIEventSource<string>): UIEventSource<number> {
return source.sync(
(str) => {
@ -623,7 +665,7 @@ export class UIEventSource<T> extends Store<T> implements Writable<T> {
if (fl === undefined || isNaN(fl)) {
return undefined
}
return ("" + fl).substr(0, 8)
return "" + fl
}
)
}

View file

@ -1,9 +1,9 @@
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoOperations } from "../GeoOperations"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import { Mapillary } from "../ImageProviders/Mapillary"
import P4C from "pic4carto"
import { Utils } from "../../Utils"
export interface NearbyImageOptions {
lon: number
lat: number
@ -35,17 +35,12 @@ export interface P4CPicture {
}
/**
* Uses Pic4wCarto to fetch nearby images from various providers
* Uses Pic4Carto to fetch nearby images from various providers
*/
export default class NearbyImagesSearch {
private static readonly services = [
"mapillary",
"flickr",
"openstreetcam",
"wikicommons",
] as const
private individualStores
public static readonly services = ["mapillary", "flickr", "kartaview", "wikicommons"] as const
public static readonly apiUrls = ["https://api.flickr.com"]
private readonly individualStores: Store<{ images: P4CPicture[]; beforeFilter: number }>[]
private readonly _store: UIEventSource<P4CPicture[]> = new UIEventSource<P4CPicture[]>([])
public readonly store: Store<P4CPicture[]> = this._store
private readonly _options: NearbyImageOptions
@ -71,16 +66,16 @@ export default class NearbyImagesSearch {
this.update()
}
private static buildPictureFetcher(
private static async fetchImages(
options: NearbyImageOptions,
fetcher: "mapillary" | "flickr" | "openstreetcam" | "wikicommons"
): Store<{ images: P4CPicture[]; beforeFilter: number }> {
fetcher: P4CService
): Promise<P4CPicture[]> {
const picManager = new P4C.PicturesManager({ usefetchers: [fetcher] })
const searchRadius = options.searchRadius ?? 100
const maxAgeSeconds = (options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000
const searchRadius = options.searchRadius ?? 100
const p4cStore = Stores.FromPromise<P4CPicture[]>(
picManager.startPicsRetrievalAround(
try {
const pics: P4CPicture[] = await picManager.startPicsRetrievalAround(
new P4C.LatLng(options.lat, options.lon),
searchRadius,
{
@ -88,7 +83,21 @@ export default class NearbyImagesSearch {
towardscenter: false,
}
)
return pics
} catch (e) {
console.error("Could not fetch images from service", fetcher, e)
return []
}
}
private static buildPictureFetcher(
options: NearbyImageOptions,
fetcher: P4CService
): Store<{ images: P4CPicture[]; beforeFilter: number }> {
const p4cStore = Stores.FromPromise<P4CPicture[]>(
NearbyImagesSearch.fetchImages(options, fetcher)
)
const searchRadius = options.searchRadius ?? 100
return p4cStore.map(
(images) => {
if (images === undefined) {
@ -220,3 +229,5 @@ class ImagesInLoadedDataFetcher {
return foundImages
}
}
type P4CService = (typeof NearbyImagesSearch.services)[number]

View file

@ -1,7 +1,7 @@
import { Utils } from "../../Utils"
export default class PlantNet {
private static baseUrl =
public static baseUrl =
"https://my-api.plantnet.org/v2/identify/all?api-key=2b10AAsjzwzJvucA5Ncm5qxe"
public static query(imageUrls: string[]): Promise<PlantNetResult> {

View file

@ -123,6 +123,11 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions {
* Utility functions around wikidata
*/
export default class Wikidata {
public static readonly neededUrls = [
"https://www.wikidata.org/",
"https://wikidata.org/",
"https://query.wikidata.org",
]
private static readonly _identifierPrefixes = ["Q", "L"].map((str) => str.toLowerCase())
private static readonly _prefixesToRemove = [
"https://www.wikidata.org/wiki/Lexeme:",
@ -130,11 +135,11 @@ export default class Wikidata {
"http://www.wikidata.org/entity/",
"Lexeme:",
].map((str) => str.toLowerCase())
private static readonly _storeCache = new Map<
string,
Store<{ success: WikidataResponse } | { error: any }>
>()
/**
* Same as LoadWikidataEntry, but wrapped into a UIEventSource
* @param value
@ -388,6 +393,7 @@ export default class Wikidata {
}
private static _cache = new Map<string, Promise<WikidataResponse>>()
public static async LoadWikidataEntryAsync(value: string | number): Promise<WikidataResponse> {
const key = "" + value
const cached = Wikidata._cache.get(key)
@ -398,6 +404,7 @@ export default class Wikidata {
Wikidata._cache.set(key, uncached)
return uncached
}
/**
* Loads a wikidata page
* @returns the entity of the given value

View file

@ -34,6 +34,8 @@ export default class Wikipedia {
private static readonly idsToRemove = ["sjabloon_zie"]
public static readonly neededUrls = ["*.wikipedia.org"]
private static readonly _cache = new Map<string, Promise<string>>()
private static _fullDetailsCache = new Map<string, Store<FullWikipediaDetails>>()
public readonly backend: string

View file

@ -1,6 +1,7 @@
import * as packagefile from "../../package.json"
import * as extraconfig from "../../config.json"
import { Utils } from "../Utils"
import { AuthConfig } from "../Logic/Osm/AuthConfig"
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
@ -104,8 +105,9 @@ export default class Constants {
public static ImgurApiKey = Constants.config.api_keys.imgur
public static readonly mapillary_client_token_v4 = Constants.config.api_keys.mapillary_v4
public static defaultOverpassUrls = Constants.config.default_overpass_urls
static countryCoderEndpoint: string = Constants.config.country_coder_host
public static countryCoderEndpoint: string = Constants.config.country_coder_host
public static osmAuthConfig: AuthConfig = Constants.config.oauth_credentials
public static nominatimEndpoint: string = Constants.config.nominatimEndpoint
/**
* These are the values that are allowed to use as 'backdrop' icon for a map pin
*/

View file

@ -13,7 +13,6 @@ export interface MapProperties {
readonly allowMoving: UIEventSource<true | boolean>
readonly allowRotating: UIEventSource<true | boolean>
readonly lastClickLocation: Store<{ lon: number; lat: number }>
readonly allowZooming: UIEventSource<true | boolean>
}

View file

@ -13,7 +13,6 @@ import Combine from "../../UI/Base/Combine"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
import Marker from "../../UI/Map/Marker.svelte"
import DynamicMarker from "../../UI/Map/DynamicMarker.svelte"
export class IconConfig extends WithContextLoader {
@ -83,7 +82,7 @@ export default class PointRenderingConfig extends WithContextLoader {
})
if (json.marker === undefined && json.label === undefined) {
throw `${context}: A point rendering should define at least an icon or a marker`
throw `At ${context}: A point rendering should define at least an marker or a label`
}
if (this.location.size == 0) {

View file

@ -141,7 +141,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
undefined,
"Used to complete the login"
),
osmConfiguration: <"osm" | "osm-test">this.featureSwitches.featureSwitchApiURL.data,
})
this.userRelatedState = new UserRelatedState(
this.osmConnection,
@ -538,7 +537,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.mapProperties.maxbounds.setData(bbox)
ShowDataLayer.showRange(
this.map,
new StaticFeatureSource([bbox.asGeoJson({})]),
new StaticFeatureSource([bbox.asGeoJson({ id: "range" })]),
this.featureSwitches.featureSwitchIsTesting
)
}
@ -576,7 +575,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
}
this.featureProperties.trackFeatureSource(features)
// this.indexedFeatures.addSource(features)
new ShowDataLayer(this.map, {
features,
doShowLayer: flayer.isDisplayed,

View file

@ -23,7 +23,6 @@ export default class AllThemesGui {
undefined,
"Used to complete the login"
),
osmConfiguration: <"osm" | "osm-test">featureSwitches.featureSwitchApiURL.data,
})
const state = new UserRelatedState(osmConnection)
const intro = new Combine([

View file

@ -11,6 +11,7 @@ import { Utils } from "../../Utils"
import Constants from "../../Models/Constants"
export class OpenJosm extends Combine {
public static readonly needsUrls = ["http://127.0.0.1:8111/load_and_zoom"]
constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
const t = Translations.t.general.attribution

View file

@ -14,13 +14,6 @@
export let tags: UIEventSource<Record<string, string>>
export let highlightedRendering: UIEventSource<string> = undefined
let _tags: Record<string, string>
onDestroy(
tags.addCallbackAndRun((tags) => {
_tags = tags
})
)
let _metatags: Record<string, string>
onDestroy(
state.userRelatedState.preferencesAsTags.addCallbackAndRun((tags) => {
@ -29,7 +22,7 @@
)
</script>
{#if _tags._deleted === "yes"}
{#if $tags._deleted === "yes"}
<Tr t={Translations.t.delete.isDeleted} />
<button class="w-full" on:click={() => state.selectedElement.setData(undefined)}>
<Tr t={Translations.t.general.returnToTheMap} />
@ -37,8 +30,8 @@
{:else}
<div class="flex flex-col gap-y-2 overflow-y-auto p-1 px-2">
{#each layer.tagRenderings as config (config.id)}
{#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties( { ..._tags, ..._metatags } ))}
{#if config.IsKnown(_tags)}
{#if (config.condition?.matchesProperties($tags) ?? true) && config.metacondition?.matchesProperties({ ...$tags, ..._metatags } ?? true)}
{#if config.IsKnown($tags)}
<TagRenderingEditable
{tags}
{config}

View file

@ -417,10 +417,19 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
if (!map.getLayer(addLayerBeforeId)) {
addLayerBeforeId = undefined
}
await this.awaitStyleIsLoaded()
if (!map.getSource(background.id)) {
map.addSource(background.id, MapLibreAdaptor.prepareWmsSource(background))
}
if (!map.getLayer(background.id)) {
console.log(
"Adding background layer",
background.id,
"beforeId",
addLayerBeforeId,
"; all layers are",
map.getStyle().layers.map((l) => l.id)
)
map.addLayer(
{
id: background.id,

View file

@ -1,21 +1,21 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
import type { Map as MlMap } from "maplibre-gl";
import { GeoJSONSource, Marker } from "maplibre-gl";
import { ShowDataLayerOptions } from "./ShowDataLayerOptions";
import { GeoOperations } from "../../Logic/GeoOperations";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig";
import { OsmTags } from "../../Models/OsmFeature";
import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource";
import { BBox } from "../../Logic/BBox";
import { Feature, Point } from "geojson";
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig";
import { Utils } from "../../Utils";
import * as range_layer from "../../../assets/layers/range/range.json";
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson";
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter";
import FilteredLayer from "../../Models/FilteredLayer";
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource";
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Map as MlMap } from "maplibre-gl"
import { GeoJSONSource, Marker } from "maplibre-gl"
import { ShowDataLayerOptions } from "./ShowDataLayerOptions"
import { GeoOperations } from "../../Logic/GeoOperations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import PointRenderingConfig from "../../Models/ThemeConfig/PointRenderingConfig"
import { OsmTags } from "../../Models/OsmFeature"
import { FeatureSource, FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
import { BBox } from "../../Logic/BBox"
import { Feature, Point } from "geojson"
import LineRenderingConfig from "../../Models/ThemeConfig/LineRenderingConfig"
import { Utils } from "../../Utils"
import * as range_layer from "../../../assets/layers/range/range.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import PerLayerFeatureSourceSplitter from "../../Logic/FeatureSource/PerLayerFeatureSourceSplitter"
import FilteredLayer from "../../Models/FilteredLayer"
import SimpleFeatureSource from "../../Logic/FeatureSource/Sources/SimpleFeatureSource"
class PointRenderingLayer {
private readonly _config: PointRenderingConfig
@ -228,6 +228,8 @@ class LineRenderingLayer {
this._onClick = onClick
const self = this
features.features.addCallbackAndRunD(() => self.update(features.features))
map.on("styledata", () => self.update(features.features))
}
public destruct(): void {
@ -280,22 +282,27 @@ class LineRenderingLayer {
// As such, we only now read the features from the featureSource and compare with the previously set data
const features = featureSource.data
const src = <GeoJSONSource>map.getSource(this._layername)
if (this.currentSourceData === features) {
if (
src !== undefined &&
this.currentSourceData === features &&
src._data === <any>features
) {
// Already up to date
return
}
{// Add source to the map or update the features
{
// Add source to the map or update the feature source
if (src === undefined) {
this.currentSourceData = features;
this.currentSourceData = features
map.addSource(this._layername, {
type: "geojson",
data: {
type: "FeatureCollection",
features
features,
},
promoteId: "id"
});
const linelayer = this._layername + "_line";
promoteId: "id",
})
const linelayer = this._layername + "_line"
map.addLayer({
source: this._layername,
id: linelayer,
@ -304,14 +311,18 @@ class LineRenderingLayer {
"line-color": ["feature-state", "color"],
"line-opacity": ["feature-state", "color-opacity"],
"line-width": ["feature-state", "width"],
"line-offset": ["feature-state", "offset"]
"line-offset": ["feature-state", "offset"],
},
layout: {
"line-cap": "round"
}
});
"line-cap": "round",
},
})
for (const feature of features) {
if (!feature.properties.id) {
console.warn("Feature without id:", feature)
continue
}
map.setFeatureState(
{ source: this._layername, id: feature.properties.id },
this.calculatePropsFor(feature.properties)
@ -320,10 +331,10 @@ class LineRenderingLayer {
map.on("click", linelayer, (e) => {
// line-layer-listener
e.originalEvent["consumed"] = true;
this._onClick(e.features[0]);
});
const polylayer = this._layername + "_polygon";
e.originalEvent["consumed"] = true
this._onClick(e.features[0])
})
const polylayer = this._layername + "_polygon"
map.addLayer({
source: this._layername,
@ -333,41 +344,40 @@ class LineRenderingLayer {
layout: {},
paint: {
"fill-color": ["feature-state", "fillColor"],
"fill-opacity": ["feature-state", "fillColor-opacity"]
}
});
"fill-opacity": ["feature-state", "fillColor-opacity"],
},
})
if (this._onClick) {
map.on("click", polylayer, (e) => {
// polygon-layer-listener
if (e.originalEvent["consumed"]) {
// This is a polygon beneath a marker, we can ignore it
return;
return
}
e.originalEvent["consumed"] = true;
console.log("Got features:", e.features, e);
this._onClick(e.features[0]);
});
e.originalEvent["consumed"] = true
this._onClick(e.features[0])
})
}
this._visibility?.addCallbackAndRunD((visible) => {
try {
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none");
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none");
map.setLayoutProperty(linelayer, "visibility", visible ? "visible" : "none")
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
} catch (e) {
console.warn(
"Error while setting visibility of layers ",
linelayer,
polylayer,
e
);
)
}
});
})
} else {
this.currentSourceData = features;
this.currentSourceData = features
src.setData({
type: "FeatureCollection",
features: this.currentSourceData
});
features: this.currentSourceData,
})
}
}
for (let i = 0; i < features.length; i++) {
@ -388,9 +398,6 @@ class LineRenderingLayer {
if (this._listenerInstalledOn.has(id)) {
continue
}
if (!map.getSource(this._layername)) {
continue
}
if (this._fetchStore === undefined) {
map.setFeatureState(
{ source: this._layername, id },
@ -399,8 +406,12 @@ class LineRenderingLayer {
} else {
const tags = this._fetchStore(id)
this._listenerInstalledOn.add(id)
tags.addCallbackAndRunD((properties) => {
if(!map.getLayer(this._layername)){
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties)
)
tags.addCallbackD((properties) => {
if (!map.getLayer(this._layername)) {
return
}
map.setFeatureState(
@ -418,7 +429,6 @@ export default class ShowDataLayer {
<any>range_layer,
"ShowDataLayer.ts:range.json"
)
private readonly _map: Store<MlMap>
private readonly _options: ShowDataLayerOptions & {
layer: LayerConfig
drawMarkers?: true | boolean
@ -435,7 +445,6 @@ export default class ShowDataLayer {
drawLines?: true | boolean
}
) {
this._map = map
this._options = options
const self = this
this.onDestroy.push(map.addCallbackAndRunD((map) => self.initDrawFeatures(map)))

View file

@ -10,9 +10,11 @@ import Combine from "../Base/Combine"
import Title from "../Base/Title"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment"
needsUrls = [Constants.osmAuthConfig.url]
docs = "A textfield to add a comment to a node (with the option to close the note)."
args = [
{

View file

@ -9,7 +9,6 @@ import { Utils } from "../../Utils"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Translations from "../i18n/Translations"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
@ -209,6 +208,8 @@ class ApplyButton extends UIElement {
export default class AutoApplyButton implements SpecialVisualization {
public readonly docs: BaseUIElement
public readonly funcName: string = "auto_apply"
public readonly needsUrls = []
public readonly args: {
name: string
defaultValue?: string
@ -271,14 +272,7 @@ export default class AutoApplyButton implements SpecialVisualization {
argument: string[]
): BaseUIElement {
try {
if (
!state.layout.official &&
!(
state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
)
) {
if (!state.layout.official && !state.featureSwitchIsTesting.data) {
const t = Translations.t.general.add.import
return new Combine([
new FixedUiElement(

View file

@ -8,9 +8,11 @@ import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note"
public readonly needsUrls = [Constants.osmAuthConfig.url]
public readonly docs =
"Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text."
public readonly args = [

View file

@ -50,6 +50,9 @@
$: isHardDelete = changedProperties[DeleteConfig.deleteReasonKey] !== undefined
async function onDelete() {
if (selectedTags === undefined) {
return
}
currentState = "applying"
let actionToTake: OsmChangeAction
const changedProperties = TagUtils.changeAsProperties(selectedTags.asChange(tags?.data ?? {}))

View file

@ -13,7 +13,7 @@ export class ExportAsGpxViz implements SpecialVisualization {
funcName = "export_as_gpx"
docs = "Exports the selected feature as GPX-file"
args = []
needsUrls = []
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,

View file

@ -2,10 +2,13 @@ import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import Histogram from "../BigComponents/Histogram"
import { Feature } from "geojson"
import Constants from "../../Models/Constants"
export class HistogramViz implements SpecialVisualization {
funcName = "histogram"
docs = "Create a histogram for a list of given values, read from the properties."
needsUrls = []
example =
'`{histogram(\'some_key\')}` with properties being `{some_key: ["a","b","a","c"]} to create a histogram'
args = [

View file

@ -24,6 +24,7 @@ export interface ConflateFlowArguments extends ImportFlowArguments {
export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction {
supportsAutoAction: boolean = true
needsUrls = []
public readonly funcName: string = "conflate_button"
public readonly args: {
name: string

View file

@ -194,10 +194,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
return { error: t.hasBeenImported }
}
const usesTestUrl =
this.state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
if (!state.layout.official && !(isTesting || usesTestUrl)) {
if (!state.layout.official && !isTesting) {
// Unofficial theme - imports not allowed
return {
error: t.officialThemesOnly,

View file

@ -18,6 +18,7 @@ export class PointImportButtonViz implements SpecialVisualization {
public readonly docs: string | BaseUIElement
public readonly example?: string
public readonly args: { name: string; defaultValue?: string; doc: string }[]
public needsUrls = []
constructor() {
this.funcName = "import_button"

View file

@ -20,6 +20,7 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou
*/
export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
public readonly funcName: string = "import_way_button"
needsUrls = []
public readonly docs: string =
"This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" +
ImportFlowUtils.documentationGeneral

View file

@ -20,6 +20,7 @@ import { Feature } from "geojson"
export class LanguageElement implements SpecialVisualization {
funcName: string = "language_chooser"
needsUrls = []
docs: string | BaseUIElement =
"The language element allows to show and pick all known (modern) languages. The key can be set"

View file

@ -9,6 +9,8 @@ import MapillaryLink from "../BigComponents/MapillaryLink.svelte"
export class MapillaryLinkVis implements SpecialVisualization {
funcName = "mapillary_link"
docs = "Adds a button to open mapillary on the specified location"
needsUrls = []
args = [
{
name: "zoom",

View file

@ -13,6 +13,7 @@ import { BBox } from "../../Logic/BBox"
export class MinimapViz implements SpecialVisualization {
funcName = "minimap"
docs = "A small map showing the selected feature."
needsUrls = []
args = [
{
doc: "The (maximum) zoomlevel: the target zoomlevel after fitting the entire feature. The minimap will fit the entire feature, then zoom out to this zoom level. The higher, the more zoomed in with 1 being the entire world and 19 being really close",
@ -79,7 +80,9 @@ export class MinimapViz implements SpecialVisualization {
)
const mlmap = new UIEventSource(undefined)
const mla = new MapLibreAdaptor(mlmap)
const mla = new MapLibreAdaptor(mlmap, {
rasterLayer: state.mapProperties.rasterLayer,
})
mla.maxzoom.setData(17)
let zoom = 18

View file

@ -4,6 +4,7 @@ import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisua
export class MultiApplyViz implements SpecialVisualization {
funcName = "multi_apply"
needsUrls = []
docs =
"A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags"
args = [

View file

@ -8,9 +8,10 @@ import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import SvelteUIElement from "../Base/SvelteUIElement"
import PlantNet from "../PlantNet/PlantNet.svelte"
import { default as PlantNetCode } from "../../Logic/Web/PlantNet"
export class PlantNetDetectionViz implements SpecialVisualization {
funcName = "plantnet_detection"
needsUrls = [PlantNetCode.baseUrl]
docs =
"Sends the images linked to the current object to plantnet.org and asks it what plant species is shown on it. The user can then select the correct species; the corresponding wikidata-identifier will then be added to the object (together with `source:species:wikidata=plantnet.org AI`). "

View file

@ -1,51 +0,0 @@
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import SvelteUIElement from "../Base/SvelteUIElement"
import Questionbox from "./TagRendering/Questionbox.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
/**
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
*/
export default class QuestionViz implements SpecialVisualization {
funcName = "questions"
docs =
"The special element which shows the questions which are unkown. Added by default if not yet there"
args = [
{
name: "labels",
doc: "One or more ';'-separated labels. If these are given, only questions with these labels will be given. Use `unlabeled` for all questions that don't have an explicit label. If none given, all questions will be shown",
},
{
name: "blacklisted-labels",
doc: "One or more ';'-separated labels of questions which should _not_ be included",
},
]
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const labels = args[0]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
const blacklist = args[1]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
return new SvelteUIElement(Questionbox, {
layer,
tags,
selectedElement: feature,
state,
onlyForLabels: labels,
notForLabels: blacklist,
})
}
}

View file

@ -15,6 +15,7 @@ export class ShareLinkViz implements SpecialVisualization {
doc: "The url to share (default: current URL)",
},
]
needsUrls = []
public constr(
state: SpecialVisualizationState,

View file

@ -21,6 +21,7 @@ import Maproulette from "../../Logic/Maproulette"
export default class TagApplyButton implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply"
needsUrls = []
public readonly docs =
"Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" +
Utils.Special_visualizations_tagsToApplyHelpText
@ -110,6 +111,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
while (spec.length > 0) {
const [part] = spec.match(/((\\;)|[^;])*/)
console.log("Spec is", part, spec)
spec = spec.substring(part.length + 1) // +1 to remove the pending ';' as well
const kv = part.split("=").map((s) => s.trim().replace("\\;", ";"))
if (kv.length == 2) {

View file

@ -2,6 +2,7 @@ import UploadTraceToOsmUI from "../BigComponents/UploadTraceToOsmUI"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { GeoOperations } from "../../Logic/GeoOperations"
import Constants from "../../Models/Constants"
/**
* Wrapper around 'UploadTraceToOsmUI'
@ -11,6 +12,7 @@ export class UploadToOsmViz implements SpecialVisualization {
docs =
"Uploads the GPS-history as GPX to OpenStreetMap.org; clears the history afterwards. The actual feature is ignored."
args = []
needsUrls = [Constants.osmAuthConfig.url]
constr(
state: SpecialVisualizationState,

View file

@ -0,0 +1,31 @@
let lang = (
(navigator.languages && navigator.languages[0]) ||
navigator.language ||
navigator["userLanguage"] ||
"en"
).substr(0, 2)
function filterLangs(maindiv) {
let foundLangs = 0
for (const child of Array.from(maindiv.children)) {
if (child.attributes.getNamedItem("lang")?.value === lang) {
foundLangs++
}
}
if (foundLangs === 0) {
lang = "en"
}
for (const child of Array.from(maindiv.children)) {
const childLang = child.attributes.getNamedItem("lang")
if (childLang === undefined) {
continue
}
if (childLang.value === lang) {
continue
}
child.parentElement.removeChild(child)
}
}
filterLangs(document.getElementById("descriptions-while-loading"))
filterLangs(document.getElementById("default-title"))

View file

@ -88,6 +88,7 @@ export interface SpecialVisualization {
readonly funcName: string
readonly docs: string | BaseUIElement
readonly example?: string
readonly needsUrls: string[]
/**
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included

View file

@ -28,7 +28,6 @@ import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation"
import Translations from "./i18n/Translations"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
import { SubtleButton } from "./Base/SubtleButton"
import Svg from "../Svg"
import NoteCommentElement from "./Popup/NoteCommentElement"
@ -41,7 +40,6 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import QuestionViz from "./Popup/QuestionViz"
import { Feature, Point } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte"
@ -68,9 +66,15 @@ import SendEmail from "./Popup/SendEmail.svelte"
import NearbyImages from "./Popup/NearbyImages.svelte"
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
import UploadImage from "./Image/UploadImage.svelte"
import { Imgur } from "../Logic/ImageProviders/Imgur"
import Constants from "../Models/Constants"
import { MangroveReviews } from "mangrove-reviews-typescript"
import Wikipedia from "../Logic/Web/Wikipedia"
import NearbyImagesSearch from "../Logic/Web/NearbyImagesSearch"
import AllReviews from "./Reviews/AllReviews.svelte"
import StarsBarIcon from "./Reviews/StarsBarIcon.svelte"
import ReviewForm from "./Reviews/ReviewForm.svelte"
import Questionbox from "./Popup/TagRendering/Questionbox.svelte";
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -84,7 +88,7 @@ class NearbyImageVis implements SpecialVisualization {
docs =
"A component showing nearby images loaded from various online services such as Mapillary. In edit mode and when used on a feature, the user can select an image to add to the feature"
funcName = "nearby_images"
needsUrls = NearbyImagesSearch.apiUrls
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
@ -122,6 +126,7 @@ class StealViz implements SpecialVisualization {
required: true,
},
]
needsUrls = []
constr(state: SpecialVisualizationState, featureTags, args) {
const [featureIdKey, layerAndtagRenderingIds] = args
@ -176,6 +181,52 @@ class StealViz implements SpecialVisualization {
}
}
/**
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
*/
export class QuestionViz implements SpecialVisualization {
funcName = "questions"
needsUrls = []
docs =
"The special element which shows the questions which are unkown. Added by default if not yet there"
args = [
{
name: "labels",
doc: "One or more ';'-separated labels. If these are given, only questions with these labels will be given. Use `unlabeled` for all questions that don't have an explicit label. If none given, all questions will be shown",
},
{
name: "blacklisted-labels",
doc: "One or more ';'-separated labels of questions which should _not_ be included",
},
]
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const labels = args[0]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
const blacklist = args[1]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
return new SvelteUIElement(Questionbox, {
layer,
tags,
selectedElement: feature,
state,
onlyForLabels: labels,
notForLabels: blacklist,
})
}
}
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -382,6 +433,7 @@ export default class SpecialVisualizations {
funcName: "add_new_point",
docs: "An element which allows to add a new point on the 'last_click'-location. Only makes sense in the layer `last_click`",
args: [],
needsUrls: [],
constr(state: SpecialVisualizationState, _, __, feature): BaseUIElement {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, {
@ -393,6 +445,7 @@ export default class SpecialVisualizations {
{
funcName: "user_profile",
args: [],
needsUrls: [],
docs: "A component showing information about the currently logged in user (username, profile description, profile picture + link to edit them). Mostly meant to be used in the 'user-settings'",
constr(state: SpecialVisualizationState): BaseUIElement {
return new SvelteUIElement(UserProfile, {
@ -403,6 +456,7 @@ export default class SpecialVisualizations {
{
funcName: "language_picker",
args: [],
needsUrls: [],
docs: "A component to set the language of the user interface",
constr(state: SpecialVisualizationState): BaseUIElement {
return new LanguagePicker(
@ -414,6 +468,7 @@ export default class SpecialVisualizations {
{
funcName: "logout",
args: [],
needsUrls: [Constants.osmAuthConfig.url],
docs: "Shows a button where the user can log out",
constr(state: SpecialVisualizationState): BaseUIElement {
return new SubtleButton(Svg.logout_svg(), Translations.t.general.logout, {
@ -430,6 +485,7 @@ export default class SpecialVisualizations {
funcName: "split_button",
docs: "Adds a button which allows to split a way",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>
@ -450,6 +506,7 @@ export default class SpecialVisualizations {
funcName: "move_button",
docs: "Adds a button which allows to move the object to another location. The config will be read from the layer config",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -473,6 +530,7 @@ export default class SpecialVisualizations {
funcName: "delete_button",
docs: "Adds a button which allows to delete the object at this location. The config will be read from the layer config",
args: [],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -497,6 +555,7 @@ export default class SpecialVisualizations {
{
funcName: "open_note",
args: [],
needsUrls: [Constants.osmAuthConfig.url],
docs: "Creates a new map note on the given location. This options is placed in the 'last_click'-popup automatically if the 'notes'-layer is enabled",
constr(
state: SpecialVisualizationState,
@ -529,6 +588,7 @@ export default class SpecialVisualizations {
defaultValue: "wikidata;wikipedia",
},
],
needsUrls: [...Wikidata.neededUrls, ...Wikipedia.neededUrls],
example:
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
constr: (_, tagsSource, args) => {
@ -552,6 +612,7 @@ export default class SpecialVisualizations {
defaultValue: "wikidata",
},
],
needsUrls: Wikidata.neededUrls,
example:
"`{wikidata_label()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the label itself",
constr: (_, tagsSource, args) =>
@ -581,6 +642,7 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
needsUrls: [],
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
},
@ -594,6 +656,7 @@ export default class SpecialVisualizations {
doc: "The keys given to the images, e.g. if <span class='literal-code'>image</span> is given, the first picture URL will be added as <span class='literal-code'>image</span>, the second as <span class='literal-code'>image:0</span>, the third as <span class='literal-code'>image:1</span>, etc... Multiple values are allowed if ';'-separated ",
},
],
needsUrls: AllImageProviders.apiUrls,
constr: (state, tags, args) => {
let imagePrefixes: string[] = undefined
if (args.length > 0) {
@ -609,6 +672,7 @@ export default class SpecialVisualizations {
{
funcName: "image_upload",
docs: "Creates a button where a user can upload an image to IMGUR",
needsUrls: [Imgur.apiUrl],
args: [
{
name: "image-key",
@ -633,6 +697,7 @@ export default class SpecialVisualizations {
{
funcName: "rating",
docs: "Shows stars which represent the avarage rating on mangrove.reviews",
needsUrls: [MangroveReviews.ORIGINAL_API],
args: [
{
name: "subjectKey",
@ -658,11 +723,6 @@ export default class SpecialVisualizations {
)
return new SvelteUIElement(StarsBarIcon, {
score: reviews.average,
reviews,
state,
tags,
feature,
layer,
})
},
},
@ -670,6 +730,7 @@ export default class SpecialVisualizations {
{
funcName: "create_review",
docs: "Invites the contributor to leave a review. Somewhat small UI-element until interacted",
needsUrls: [MangroveReviews.ORIGINAL_API],
args: [
{
name: "subjectKey",
@ -699,6 +760,7 @@ export default class SpecialVisualizations {
{
funcName: "list_reviews",
docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten",
needsUrls: [MangroveReviews.ORIGINAL_API],
example:
"`{reviews()}` for a vanilla review, `{reviews(name, play_forest)}` to review a play forest. If a name is known, the name will be used as identifier, otherwise 'play_forest' is used",
args: [
@ -747,6 +809,7 @@ export default class SpecialVisualizations {
doc: "Remove this string from the end of the value before parsing. __Note: use `&RPARENs` to indicate `)` if needed__",
},
],
needsUrls: [],
example:
"A normal opening hours table can be invoked with `{opening_hours_table()}`. A table for e.g. conditional access with opening hours can be `{opening_hours_table(access:conditional, no @ &LPARENS, &RPARENS)}`",
constr: (state, tagSource: UIEventSource<any>, args) => {
@ -759,38 +822,9 @@ export default class SpecialVisualizations {
)
},
},
{
funcName: "live",
docs: "Downloads a JSON from the given URL, e.g. '{live(example.org/data.json, shorthand:x.y.z, other:a.b.c, shorthand)}' will download the given file, will create an object {shorthand: json[x][y][z], other: json[a][b][c] out of it and will return 'other' or 'json[a][b][c]. This is made to use in combination with tags, e.g. {live({url}, {url:format}, needed_value)}",
example:
"{live({url},{url:format},hour)} {live(https://data.mobility.brussels/bike/api/counts/?request=live&featureID=CB2105,hour:data.hour_cnt;day:data.day_cnt;year:data.year_cnt,hour)}",
args: [
{
name: "Url",
doc: "The URL to load",
required: true,
},
{
name: "Shorthands",
doc: "A list of shorthands, of the format 'shorthandname:path.path.path'. separated by ;",
},
{
name: "path",
doc: "The path (or shorthand) that should be returned",
},
],
constr: (_, tagSource: UIEventSource<any>, args) => {
const url = args[0]
const shorthands = args[1]
const neededValue = args[2]
const source = LiveQueryHandler.FetchLiveData(url, shorthands.split(";"))
return new VariableUiElement(
source.map((data) => data[neededValue] ?? "Loading...")
)
},
},
{
funcName: "canonical",
needsUrls: [],
docs: "Converts a short, canonical value into the long, translated text including the unit. This only works if a `unit` is defined for the corresponding value. The unit specification will be included in the text. ",
example:
"If the object has `length=42`, then `{canonical(length)}` will be shown as **42 meter** (in english), **42 metre** (in french), ...",
@ -828,6 +862,7 @@ export default class SpecialVisualizations {
funcName: "export_as_geojson",
docs: "Exports the selected feature as GeoJson-file",
args: [],
needsUrls: [],
constr: (state, tagSource, tagsSource, feature, layer) => {
const t = Translations.t.general.download
@ -857,6 +892,7 @@ export default class SpecialVisualizations {
funcName: "open_in_iD",
docs: "Opens the current view in the iD-editor",
args: [],
needsUrls: [],
constr: (state, feature) => {
return new SvelteUIElement(OpenIdEditor, {
mapProperties: state.mapProperties,
@ -868,6 +904,8 @@ export default class SpecialVisualizations {
funcName: "open_in_josm",
docs: "Opens the current view in the JOSM-editor",
args: [],
needsUrls: OpenJosm.needsUrls,
constr: (state) => {
return new OpenJosm(state.osmConnection, state.mapProperties.bounds)
},
@ -876,6 +914,7 @@ export default class SpecialVisualizations {
funcName: "clear_location_history",
docs: "A button to remove the travelled track information from the device",
args: [],
needsUrls: [],
constr: (state) => {
return new SubtleButton(
Svg.delete_icon_svg().SetStyle("height: 1.5rem"),
@ -901,6 +940,7 @@ export default class SpecialVisualizations {
defaultValue: "0",
},
],
needsUrls: [Constants.osmAuthConfig.url],
constr: (state, tags, args) =>
new VariableUiElement(
tags
@ -929,6 +969,7 @@ export default class SpecialVisualizations {
defaultValue: "id",
},
],
needsUrls: [Imgur.apiUrl],
constr: (state, tags, args) => {
const id = tags.data[args[0] ?? "id"]
tags = state.featureProperties.getStore(id)
@ -939,6 +980,7 @@ export default class SpecialVisualizations {
{
funcName: "title",
args: [],
needsUrls: [],
docs: "Shows the title of the popup. Useful for some cases, e.g. 'What is phone number of {title()}?'",
example:
"`What is the phone number of {title()}`, which might automatically become `What is the phone number of XYZ`.",
@ -959,6 +1001,7 @@ export default class SpecialVisualizations {
{
funcName: "maproulette_task",
args: [],
needsUrls: [Maproulette.defaultEndpoint],
constr(state, tagSource) {
let parentId = tagSource.data.mr_challengeId
if (parentId === undefined) {
@ -967,7 +1010,7 @@ export default class SpecialVisualizations {
}
let challenge = Stores.FromPromise(
Utils.downloadJsonCached(
`https://maproulette.org/api/v2/challenge/${parentId}`,
`${Maproulette.defaultEndpoint}/challenge/${parentId}`,
24 * 60 * 60 * 1000
)
)
@ -1002,6 +1045,7 @@ export default class SpecialVisualizations {
{
funcName: "maproulette_set_status",
docs: "Change the status of the given MapRoulette task",
needsUrls: [Maproulette.defaultEndpoint],
example:
" The following example sets the status to '2' (false positive)\n" +
"\n" +
@ -1125,6 +1169,7 @@ export default class SpecialVisualizations {
funcName: "statistics",
docs: "Show general statistics about the elements currently in view. Intended to use on the `current_view`-layer",
args: [],
needsUrls: [],
constr: (state) => {
return new Combine(
state.layout.layers
@ -1167,6 +1212,8 @@ export default class SpecialVisualizations {
required: true,
},
],
needsUrls: [],
constr(__, tags, args) {
return new SvelteUIElement(SendEmail, { args, tags })
},
@ -1194,6 +1241,7 @@ export default class SpecialVisualizations {
doc: "If set, this link will act as a download-button. The contents of `href` will be offered for download; this parameter will act as the proposed filename",
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,
@ -1215,6 +1263,7 @@ export default class SpecialVisualizations {
{
funcName: "multi",
docs: "Given an embedded tagRendering (read only) and a key, will read the keyname as a JSON-list. Every element of this list will be considered as tags and rendered with the tagRendering",
needsUrls: [],
example:
"```json\n" +
JSON.stringify(
@ -1305,6 +1354,7 @@ export default class SpecialVisualizations {
required: true,
},
],
needsUrls: [],
constr(
state: SpecialVisualizationState,
tagSource: UIEventSource<Record<string, string>>,

View file

@ -37,6 +37,7 @@ export class SubstitutedTranslation extends VariableUiElement {
constr: typeof value === "function" ? value : () => value,
docs: "Dynamically injected input element",
args: [],
needsUrls: [],
example: "",
})
})

File diff suppressed because it is too large Load diff

View file

@ -566,6 +566,7 @@ class SvgToPdfPage {
images: Record<string, HTMLImageElement> = {}
rects: Record<string, { rect: SVGRectElement; isInDef: boolean }> = {}
readonly options: SvgToPdfOptions
public readonly status: UIEventSource<string>
private readonly importedTranslations: Record<string, string> = {}
private readonly layerTranslations: Record<string, Record<string, any>> = {}
/**
@ -574,7 +575,6 @@ class SvgToPdfPage {
*/
private readonly _state: UIEventSource<string>
private _isPrepared = false
public readonly status: UIEventSource<string>
constructor(
page: string,
@ -674,7 +674,10 @@ class SvgToPdfPage {
public async PrepareLanguage(language: string) {
// Always fetch the remote data - it's cached anyway
this.layerTranslations[language] = await Utils.downloadJsonCached(
"https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" +
window.location.protocol +
"//" +
window.location.host +
"/assets/langs/layers/" +
language +
".json",
24 * 60 * 60 * 1000
@ -995,6 +998,7 @@ export interface PdfTemplateInfo {
orientation: "portrait" | "landscape"
isPublic: boolean
}
export class SvgToPdf {
public static readonly templates: Record<
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3",

View file

@ -1,11 +1,11 @@
{
"contributors": [
{
"commits": 6039,
"commits": 6085,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 408,
"commits": 417,
"contributor": "Robin van der Linde"
},
{
@ -52,6 +52,10 @@
"commits": 24,
"contributor": "Ward"
},
{
"commits": 21,
"contributor": "dependabot[bot]"
},
{
"commits": 21,
"contributor": "wjtje"
@ -60,10 +64,6 @@
"commits": 21,
"contributor": "AlexanderRebai"
},
{
"commits": 20,
"contributor": "dependabot[bot]"
},
{
"commits": 19,
"contributor": "Niels Elgaard Larsen"
@ -106,11 +106,11 @@
},
{
"commits": 11,
"contributor": "RobJN"
"contributor": "Thibault Molleman"
},
{
"commits": 10,
"contributor": "Thibault Molleman"
"commits": 11,
"contributor": "RobJN"
},
{
"commits": 10,

File diff suppressed because one or more lines are too long

View file

@ -506,12 +506,6 @@
"ur",
"en"
],
"PL": [
"pl",
"be",
"pl",
"be"
],
"PS": [
"ar"
],

View file

@ -12,7 +12,7 @@
"gl": "lingua galega",
"he": "עברית",
"hu": "magyar",
"id": "bahasa Indonesia",
"id": "Indonesia",
"it": "italiano",
"ja": "日本語",
"nb_NO": "bokmål",
@ -23,6 +23,5 @@
"ru": "русский язык",
"sl": "slovenščina",
"sv": "svenska",
"zh_Hans": "简体中文",
"zh_Hant": "繁體中文"
"zh_Hant": "簡體中文"
}

View file

@ -146,7 +146,7 @@
"gl": "Lingua adigue",
"he": "אדיגית",
"hu": "adigei",
"id": "bahasa Adyghe",
"id": "Adyghe",
"it": "adighè",
"ja": "アディゲ語",
"nb_NO": "adygeisk",
@ -603,7 +603,7 @@
"gl": "árabe",
"he": "ערבית",
"hu": "arab",
"id": "bahasa Arab",
"id": "Arab",
"it": "arabo",
"ja": "アラビア語",
"nb_NO": "arabisk",
@ -929,7 +929,7 @@
"fi": "Awadhin kieli",
"fr": "awadhi",
"gl": "Lingua awadhi",
"he": "אוודהית",
"he": "אוודית",
"id": "Bahasa Awadhi",
"it": "awadhi",
"ja": "アワディー語",
@ -1603,7 +1603,7 @@
"gl": "lingua bretoa",
"he": "ברטונית",
"hu": "breton",
"id": "Bahasa Breton",
"id": "Breton",
"it": "bretone",
"ja": "ブルトン語",
"nb_NO": "bretonsk",
@ -1772,7 +1772,7 @@
"gl": "Lingua buriata",
"he": "בוריאטית",
"hu": "burját",
"id": "bahasa Buryat",
"id": "Buryat",
"it": "buriato",
"ja": "ブリヤート語",
"nb_NO": "burjatisk",
@ -2316,7 +2316,7 @@
"gl": "Lingua tártara de Crimea",
"he": "טטרית של קרים",
"hu": "krími tatár",
"id": "Bahasa Tatar Krimea",
"id": "Tatar Krimea",
"it": "tataro di Crimea",
"ja": "クリミア・タタール語",
"nb_NO": "krimtatarisk",
@ -2442,6 +2442,7 @@
"id": "Bahasa Chittagonia",
"it": "lingua chittagonian",
"ja": "チッタゴン語",
"nb_NO": "Chittagong",
"pl": "Język chatgaya",
"pt": "Língua chittagong",
"pt_BR": "Língua chittagong",
@ -2532,7 +2533,7 @@
"gl": "lingua dinamarquesa",
"he": "דנית",
"hu": "dán",
"id": "bahasa Denmark",
"id": "Denmark",
"it": "danese",
"ja": "デンマーク語",
"nb_NO": "dansk",
@ -2595,7 +2596,7 @@
"gl": "lingua alemá",
"he": "גרמנית",
"hu": "német",
"id": "bahasa Jerman",
"id": "Jerman",
"it": "tedesco",
"ja": "ドイツ語",
"nb_NO": "tysk",
@ -2966,8 +2967,8 @@
"ru": "новогреческий язык",
"sl": "novogrščina",
"sv": "nygrekiska",
"zh_Hans": "希腊语",
"zh_Hant": "希臘語",
"zh_Hans": "现代希腊语",
"zh_Hant": "現代希臘語",
"_meta": {
"countries": [
"CY",
@ -3584,7 +3585,7 @@
"gl": "lingua feroesa",
"he": "פארואזית",
"hu": "feröeri",
"id": "bahasa Faroe",
"id": "Faroe",
"it": "faroese",
"ja": "フェロー語",
"nb_NO": "færøysk",
@ -4872,7 +4873,7 @@
"gl": "lingua indonesia",
"he": "אינדונזית",
"hu": "indonéz",
"id": "bahasa Indonesia",
"id": "Indonesia",
"it": "indonesiano",
"ja": "インドネシア語",
"nb_NO": "indonesisk",
@ -5019,7 +5020,7 @@
"gl": "lingua islandesa",
"he": "איסלנדית",
"hu": "izlandi",
"id": "bahasa Islandia",
"id": "Islandia",
"it": "islandese",
"ja": "アイスランド語",
"nb_NO": "islandsk",
@ -5055,7 +5056,7 @@
"gl": "lingua italiana",
"he": "איטלקית",
"hu": "olasz",
"id": "bahasa Italia",
"id": "Italia",
"it": "italiano",
"ja": "イタリア語",
"nb_NO": "italiensk",
@ -5127,7 +5128,7 @@
"gl": "lingua xaponesa",
"he": "יפנית",
"hu": "japán",
"id": "bahasa Jepang",
"id": "Jepang",
"it": "giapponese",
"ja": "日本語",
"nb_NO": "japansk",
@ -5210,7 +5211,7 @@
"gl": "Lingua xavanesa",
"he": "ג'אווה",
"hu": "jávai",
"id": "bahasa Jawa",
"id": "Jawa",
"it": "giavanese",
"ja": "ジャワ語",
"nb_NO": "javanesisk",
@ -5247,7 +5248,7 @@
"gl": "lingua xeorxiana",
"he": "גאורגית",
"hu": "grúz",
"id": "Bahasa Georgia",
"id": "Georgia",
"it": "georgiano",
"ja": "ジョージア語",
"nb_NO": "georgisk",
@ -5282,7 +5283,7 @@
"gl": "Lingua karakalpak",
"he": "קראקלפקית",
"hu": "karakalpak",
"id": "Bahasa Karakalpak",
"id": "Karakalpak",
"it": "karakalpako",
"ja": "カラカルパク語",
"nl": "Karakalpaks",
@ -5466,6 +5467,7 @@
"ja": "カインガング語",
"nb_NO": "Kaingang",
"nl": "Kaingang",
"pl": "Języki caingang",
"pt": "Língua caingangue",
"pt_BR": "Língua kaingáng",
"ru": "Каинганг",
@ -5636,7 +5638,7 @@
"gl": "Lingua casaca",
"he": "קזחית",
"hu": "kazak",
"id": "bahasa Kazakh",
"id": "Kazakh",
"it": "kazako",
"ja": "カザフ語",
"nb_NO": "kasakhisk",
@ -5673,7 +5675,7 @@
"gl": "Lingua grenlandesa",
"he": "גרינלנדית",
"hu": "grönlandi",
"id": "bahasa Greenland",
"id": "Greenland",
"it": "groenlandese",
"ja": "グリーンランド語",
"nb_NO": "grønlandsk",
@ -5705,7 +5707,7 @@
"gl": "Lingua khmer",
"he": "קמרית",
"hu": "khmer",
"id": "bahasa Khmer",
"id": "Khmer",
"it": "khmer",
"ja": "クメール語",
"nb_NO": "khmer",
@ -5816,6 +5818,7 @@
"pl": "język komi-permiacki",
"pt": "Língua komi-permyak",
"ru": "коми-пермяцкий язык",
"sl": "permjaščina",
"sv": "komi-permjakiska",
"zh_Hans": "彼尔姆科米语",
"zh_Hant": "彼爾姆科米語",
@ -6025,32 +6028,32 @@
}
},
"ku": {
"ca": "kurd del nord",
"cs": "kurmándží",
"da": "Kurmanji",
"de": "Kurmandschi",
"en": "Kurmanji",
"eo": "kurmanĝa lingvo",
"es": "kurmanji",
"eu": "Kurmanji",
"fi": "Kurmandži",
"fr": "kurmandji",
"ca": "kurd",
"cs": "kurdština",
"da": "kurdisk",
"de": "Kurdisch",
"en": "Kurdish",
"eo": "kurda lingvo",
"es": "kurdo",
"eu": "kurduera",
"fi": "kurdi",
"fr": "kurde",
"gl": "lingua kurda",
"he": "כורמנג'ית",
"hu": "kurmandzsi",
"id": "Kurmanji",
"it": "kurmanji",
"ja": "クルマンジー",
"he": "כורדית",
"hu": "kurd",
"id": "Bahasa Kurdi",
"it": "curdo",
"ja": "クルド語",
"nb_NO": "kurdisk",
"nl": "Kurmançi",
"pl": "język kurmandżi",
"pt": "curmânji",
"pt_BR": "Curmânji",
"ru": "курманджи",
"sl": "kurmandži",
"sv": "nordkurdiska",
"nl": "Koerdisch",
"pl": "język kurdyjski",
"pt": "língua curda",
"pt_BR": "língua curda",
"ru": "курдские языки",
"sl": "kurdščina",
"sv": "kurdiska",
"zh_Hans": "库尔德语",
"zh_Hant": "庫德語",
"zh_Hant": "庫德語",
"_meta": {
"countries": [
"IQ"
@ -6127,7 +6130,7 @@
"gl": "lingua komi",
"he": "קומי",
"hu": "komi",
"id": "Bahasa Komi",
"id": "Komi",
"it": "comi",
"ja": "コミ語",
"nb_NO": "syrjensk",
@ -6135,6 +6138,7 @@
"pl": "język komi",
"pt": "língua komi",
"ru": "коми язык",
"sl": "komijščina",
"sv": "komi",
"_meta": {
"dir": [
@ -6217,7 +6221,7 @@
"gl": "kirguiz",
"he": "קירגיזית",
"hu": "kirgiz",
"id": "bahasa Kirgiz",
"id": "Kirgiz",
"it": "kirghiso",
"ja": "キルギス語",
"nb_NO": "kirgisisk",
@ -6303,7 +6307,7 @@
"gl": "Lingua luxemburguesa",
"he": "לוקסמבורגית",
"hu": "luxemburgi",
"id": "bahasa Luksemburg",
"id": "Luksemburg",
"it": "lussemburghese",
"ja": "ルクセンブルク語",
"nb_NO": "luxembourgsk",
@ -6543,7 +6547,7 @@
"gl": "Lingua lombarda",
"he": "לומברד (שפה)",
"hu": "lombard",
"id": "bahasa Lombard",
"id": "Lombard",
"it": "lingua lombarda",
"ja": "ロンバルド語",
"nb_NO": "lombardisk",
@ -6603,7 +6607,7 @@
"gl": "Lingua laosiana",
"he": "לאית",
"hu": "lao",
"id": "bahasa Lao",
"id": "Lao",
"it": "lao",
"ja": "ラーオ語",
"nb_NO": "laotisk",
@ -6973,7 +6977,7 @@
"gl": "Lingua malgaxe",
"he": "מלגשית",
"hu": "malgas",
"id": "Bahasa Malagasi",
"id": "Malagasi",
"it": "malgascio",
"ja": "マダガスカル語",
"nb_NO": "gassisk",
@ -7159,7 +7163,7 @@
"gl": "Lingua macedonia",
"he": "מקדונית",
"hu": "macedón",
"id": "bahasa Makedonia",
"id": "Makedonia",
"it": "macedone",
"ja": "マケドニア語",
"nb_NO": "makedonsk",
@ -7227,7 +7231,7 @@
"gl": "Lingua mongol",
"he": "מונגולית",
"hu": "mongol",
"id": "bahasa Mongol",
"id": "Mongol",
"it": "mongolo",
"ja": "モンゴル語",
"nb_NO": "mongolsk",
@ -7468,7 +7472,7 @@
"gl": "lingua malaia",
"he": "מלאית",
"hu": "maláj",
"id": "bahasa Melayu",
"id": "Melayu",
"it": "malese",
"ja": "マレー語",
"nb_NO": "malayisk",
@ -7646,7 +7650,7 @@
"gl": "birmano",
"he": "בורמזית",
"hu": "burmai",
"id": "bahasa Burma",
"id": "Burma",
"it": "birmano",
"ja": "ビルマ語",
"nb_NO": "burmesisk",
@ -8108,7 +8112,7 @@
"gl": "lingua norueguesa",
"he": "נורווגית",
"hu": "norvég",
"id": "bahasa Norwegia",
"id": "Norwegia",
"it": "norvegese",
"ja": "ノルウェー語",
"nb_NO": "norsk",
@ -8435,12 +8439,12 @@
"eo": "olonec-karela lingvo",
"fi": "livvinkarjala",
"fr": "olonetsien",
"gl": "Lingua livvi",
"gl": "lingua livvi",
"it": "lingua livvi",
"ja": "リッヴィ語",
"nb_NO": "livvisk",
"nl": "Olonetsisch",
"pl": "Dialekt ołoniecki",
"pl": "dialekt ołoniecki",
"ru": "ливвиковское наречие",
"sv": "livvi",
"zh_Hant": "利維卡累利阿語",
@ -8545,7 +8549,7 @@
"gl": "Lingua oseta",
"he": "אוסטית",
"hu": "oszét",
"id": "bahasa Ossetia",
"id": "Ossetia",
"it": "osseto",
"ja": "オセット語",
"nb_NO": "ossetisk",
@ -8621,7 +8625,7 @@
"gl": "lingua punjabi (Shahmukhi)",
"he": "פנג'אבי (אלפבית שאהמוקי)",
"hu": "pandzsábi (Shahmukhi)",
"id": "Bahasa Punjab (Abjad Shahmukhi)",
"id": "Punjab (Abjad Shahmukhi)",
"it": "punjabi (Shahmukhī)",
"ja": "パンジャーブ語 (シャームキー文字)",
"nb_NO": "panjabi (Shahmukhi)",
@ -8846,6 +8850,7 @@
"pl": "Język neosalomoński",
"pt": "Língua pijin",
"ru": "Пиджин Соломоновых Островов",
"sl": "salomonski pidžin",
"sv": "pijin",
"_meta": {
"dir": [
@ -8900,9 +8905,6 @@
"zh_Hans": "波兰语",
"zh_Hant": "波蘭語",
"_meta": {
"countries": [
"PL"
],
"dir": [
"left-to-right"
]
@ -9043,7 +9045,7 @@
"gl": "lingua portuguesa",
"he": "פורטוגזית",
"hu": "portugál",
"id": "bahasa Portugis",
"id": "Portugis",
"it": "portoghese",
"ja": "ポルトガル語",
"nb_NO": "portugisisk",
@ -9253,7 +9255,7 @@
"en": "Rakhine",
"fr": "arakanais",
"gl": "Lingua arakanesa",
"id": "bahasa Rakhine",
"id": "Rakhine",
"ja": "ラカイン語",
"nl": "Arakanees",
"pl": "Język arakański",
@ -9501,7 +9503,7 @@
"gl": "Lingua arromanesa",
"he": "ארומנית",
"hu": "aromán",
"id": "Bahasa Arumania",
"id": "Arumania",
"it": "arumeno",
"ja": "アルーマニア語",
"nb_NO": "arumensk",
@ -9896,7 +9898,7 @@
"ca": "taixelhit",
"cs": "tašelhit",
"de": "Taschelhit",
"en": "Shilha",
"en": "Tachelhit",
"eo": "ŝelha lingvo",
"es": "chilha",
"fi": "Tašelhit",
@ -9996,7 +9998,7 @@
"pt": "Língua cingalesa",
"pt_BR": "Língua cingalesa",
"ru": "сингальский язык",
"sl": "sinhalščina",
"sl": "singalščina",
"sv": "singalesiska",
"zh_Hant": "僧伽羅語",
"_meta": {
@ -10456,7 +10458,7 @@
"gl": "Lingua albanesa",
"he": "אלבנית",
"hu": "albán",
"id": "Bahasa Albania",
"id": "Albania",
"it": "albanese",
"ja": "アルバニア語",
"nb_NO": "albansk",
@ -10699,7 +10701,7 @@
"gl": "lingua sueca",
"he": "שוודית",
"hu": "svéd",
"id": "bahasa Swedia",
"id": "Swedia",
"it": "svedese",
"ja": "スウェーデン語",
"nb_NO": "svensk",
@ -10798,7 +10800,7 @@
"gl": "Lingua silesiana",
"he": "שלזית",
"hu": "sziléziai",
"id": "bahasa Silesia",
"id": "Silesia",
"it": "slesiano",
"ja": "シレジア語",
"nb_NO": "schlesisk",
@ -10847,7 +10849,7 @@
"gl": "Lingua támil",
"he": "טמילית",
"hu": "tamil",
"id": "Bahasa Tamil",
"id": "Tamil",
"it": "tamil",
"ja": "タミル語",
"nb_NO": "tamilsk",
@ -11034,7 +11036,7 @@
"gl": "lingua tailandesa",
"he": "תאית",
"hu": "thai",
"id": "bahasa Thai",
"id": "Thai",
"it": "thailandese",
"ja": "タイ語",
"nb_NO": "thai",
@ -11104,7 +11106,7 @@
"gl": "Lingua turcomá",
"he": "טורקמנית",
"hu": "türkmén",
"id": "bahasa Turkmen",
"id": "Turkmen",
"it": "Turkmeno",
"ja": "トルクメン語",
"nb_NO": "turkmensk",
@ -11632,7 +11634,7 @@
"gl": "Lingua uigur",
"he": "אויגורית",
"hu": "ujgur",
"id": "bahasa Uyghur",
"id": "Uighur",
"it": "uiguro",
"ja": "ウイグル語",
"nb_NO": "uigurisk",
@ -11702,7 +11704,7 @@
"gl": "Lingua usbeka",
"he": "אוזבקית",
"hu": "üzbég",
"id": "bahasa Uzbek",
"id": "Uzbek",
"it": "uzbeco",
"ja": "ウズベク語",
"nb_NO": "usbekisk",
@ -12591,7 +12593,7 @@
"gl": "lingua chinesa",
"he": "סינית",
"hu": "kínai",
"id": "bahasa Tionghoa",
"id": "Tionghoa",
"it": "cinese",
"ja": "中国語",
"nb_NO": "kinesisk",
@ -12647,7 +12649,7 @@
]
}
},
"zh_Hans": {
"zh_Hant": {
"ca": "xinès simplificat",
"cs": "zjednodušená čínština",
"da": "forenklet kinesisk",
@ -12656,6 +12658,7 @@
"eo": "simpligita ĉina skribsistemo",
"es": "chino simplificado",
"eu": "Txinera sinplifikatua",
"fi": "perinteinen kiina",
"fr": "chinois simplifié",
"gl": "chinés simplificado",
"he": "סינית מפושטת",
@ -12678,36 +12681,6 @@
]
}
},
"zh_Hant": {
"ca": "xinès tradicional",
"cs": "čínština (tradiční)",
"da": "traditionel kinesisk",
"de": "traditionelles Chinesisch",
"en": "Traditional Chinese",
"eo": "ĉina lingvo de tradicia ortografio",
"es": "chino tradicional",
"eu": "Txinera tradizional",
"fi": "perinteinen kiina",
"fr": "chinois traditionnel",
"gl": "chinés tradicional",
"he": "סינית מסורתית",
"it": "cinese tradizionale",
"ja": "繁体字中国語",
"nb_NO": "tradisjonell kinesisk",
"nl": "traditioneel Chinees",
"pl": "język chiński tradycyjny",
"pt": "chinês tradicional",
"ru": "традиционный китайский",
"sl": "tradicionalna kitajščina",
"sv": "traditionell kinesiska",
"zh_Hans": "繁体中文",
"zh_Hant": "繁體中文",
"_meta": {
"dir": [
"left-to-right"
]
}
},
"zu": {
"ca": "zulu",
"cs": "zuluština",

View file

@ -1,15 +1,15 @@
{
"contributors": [
{
"commits": 306,
"commits": 310,
"contributor": "kjon"
},
{
"commits": 287,
"commits": 288,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 171,
"commits": 178,
"contributor": "paunofu"
},
{
@ -32,6 +32,10 @@
"commits": 36,
"contributor": "Iago"
},
{
"commits": 32,
"contributor": "Jiří Podhorecký"
},
{
"commits": 32,
"contributor": "Lucas"
@ -40,10 +44,6 @@
"commits": 32,
"contributor": "Babos Gábor"
},
{
"commits": 31,
"contributor": "Jiří Podhorecký"
},
{
"commits": 31,
"contributor": "Supaplex"
@ -124,6 +124,10 @@
"commits": 10,
"contributor": "Irina"
},
{
"commits": 9,
"contributor": "Ettore Atalan"
},
{
"commits": 9,
"contributor": "deep map"
@ -144,10 +148,6 @@
"commits": 9,
"contributor": "Jacque Fresco"
},
{
"commits": 8,
"contributor": "Ettore Atalan"
},
{
"commits": 8,
"contributor": "Vinicius"
@ -444,6 +444,14 @@
"commits": 2,
"contributor": "Leo Alcaraz"
},
{
"commits": 1,
"contributor": "Kelson Vibber"
},
{
"commits": 1,
"contributor": "Juan"
},
{
"commits": 1,
"contributor": "Traladarer"

View file

@ -3,6 +3,7 @@ import SvelteUIElement from "./src/UI/Base/SvelteUIElement"
import ThemeViewGUI from "./src/UI/ThemeViewGUI.svelte"
import LayoutConfig from "./src/Models/ThemeConfig/LayoutConfig";
import MetaTagging from "./src/Logic/MetaTagging";
import { FixedUiElement } from "./src/UI/Base/FixedUiElement";
function webgl_support() {
try {