forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
ef3e27ee8b
399 changed files with 38592 additions and 44846 deletions
|
@ -2,10 +2,15 @@ import { ImmutableStore, Store, UIEventSource } from "../UIEventSource"
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { QueryParameters } from "../Web/QueryParameters"
|
||||
import Hash from "../Web/Hash"
|
||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||
import { OsmObject } from "../Osm/OsmObject"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
/**
|
||||
* This actor is responsible to set the map location.
|
||||
* It will attempt to
|
||||
* - Set the map to the position of the selected element
|
||||
* - Set the map to the position as passed in by the query parameters (if available)
|
||||
* - Set the map to the position remembered in LocalStorage (if available)
|
||||
* - Set the map to the layout default
|
||||
|
@ -16,6 +21,7 @@ export default class InitialMapPositioning {
|
|||
public zoom: UIEventSource<number>
|
||||
public location: UIEventSource<{ lon: number; lat: number }>
|
||||
public useTerrain: Store<boolean>
|
||||
|
||||
constructor(layoutToUse: LayoutConfig) {
|
||||
function localStorageSynced(
|
||||
key: string,
|
||||
|
@ -38,6 +44,8 @@ export default class InitialMapPositioning {
|
|||
return src
|
||||
}
|
||||
|
||||
const initialHash = Hash.hash.data
|
||||
|
||||
// -- Location control initialization
|
||||
this.zoom = localStorageSynced(
|
||||
"z",
|
||||
|
@ -62,5 +70,19 @@ export default class InitialMapPositioning {
|
|||
lon.setData(loc.lon)
|
||||
})
|
||||
this.useTerrain = new ImmutableStore<boolean>(layoutToUse.enableTerrain)
|
||||
|
||||
if (initialHash?.match(/^(node|way|relation)\/[0-9]+$/)) {
|
||||
const [type, id] = initialHash.split("/")
|
||||
OsmObjectDownloader.RawDownloadObjectAsync(type, Number(id), Constants.osmAuthConfig.url + "/").then(osmObject => {
|
||||
if (osmObject === "deleted") {
|
||||
return
|
||||
}
|
||||
const targetLayer = layoutToUse.getMatchingLayer(osmObject.tags)
|
||||
this.zoom.setData(Math.max(this.zoom.data, targetLayer.minzoom))
|
||||
const [lat, lon] = osmObject.centerpoint()
|
||||
this.location.setData({ lon, lat })
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,7 +127,9 @@ export abstract class OsmObject {
|
|||
return result
|
||||
}
|
||||
|
||||
// The centerpoint of the feature, as [lat, lon]
|
||||
/** The centerpoint of the feature, as [lat, lon]
|
||||
*
|
||||
*/
|
||||
public abstract centerpoint(): [number, number]
|
||||
|
||||
public abstract asGeoJson(): any
|
||||
|
|
|
@ -62,7 +62,7 @@ export default class OsmObjectDownloader {
|
|||
if (idN < 0) {
|
||||
obj = this.constructObject(<"node" | "way" | "relation">type, idN)
|
||||
} else {
|
||||
obj = await this.RawDownloadObjectAsync(type, idN, maxCacheAgeInSecs)
|
||||
obj = await OsmObjectDownloader.RawDownloadObjectAsync(type, idN, this.backend, maxCacheAgeInSecs)
|
||||
}
|
||||
if (obj === "deleted") {
|
||||
return obj
|
||||
|
@ -211,13 +211,22 @@ export default class OsmObjectDownloader {
|
|||
}
|
||||
}
|
||||
|
||||
private async RawDownloadObjectAsync(
|
||||
/**
|
||||
* Only to be used in exceptional cases
|
||||
* @param type
|
||||
* @param idN
|
||||
* @param backend
|
||||
* @param maxCacheAgeInSecs
|
||||
* @constructor
|
||||
*/
|
||||
public static async RawDownloadObjectAsync(
|
||||
type: string,
|
||||
idN: number,
|
||||
backend: string,
|
||||
maxCacheAgeInSecs?: number
|
||||
): Promise<OsmObject | "deleted"> {
|
||||
const full = type !== "node" ? "/full" : ""
|
||||
const url = `${this.backend}api/0.6/${type}/${idN}${full}`
|
||||
const url = `${backend}api/0.6/${type}/${idN}${full}`
|
||||
const rawData = await Utils.downloadJsonCachedAdvanced(
|
||||
url,
|
||||
(maxCacheAgeInSecs ?? 10) * 1000
|
||||
|
@ -227,7 +236,7 @@ export default class OsmObjectDownloader {
|
|||
}
|
||||
// A full query might contain more then just the requested object (e.g. nodes that are part of a way, where we only want the way)
|
||||
const parsed = OsmObject.ParseObjects(rawData["content"].elements)
|
||||
// Lets fetch the object we need
|
||||
// Let us fetch the object we need
|
||||
for (const osmObject of parsed) {
|
||||
if (osmObject.type !== type) {
|
||||
continue
|
||||
|
|
|
@ -435,7 +435,7 @@ export default class SimpleMetaTaggers {
|
|||
() => feature.properties["_country"]
|
||||
)
|
||||
let canonical =
|
||||
denomination?.canonicalValue(value, defaultDenom == denomination) ??
|
||||
denomination?.canonicalValue(value, defaultDenom == denomination, unit.inverted) ??
|
||||
undefined
|
||||
if (canonical === value) {
|
||||
break
|
||||
|
|
|
@ -331,6 +331,9 @@ export default class LinkedDataLoader {
|
|||
return
|
||||
}
|
||||
output[key] = output[key].map((v) => applyF(v))
|
||||
if(!output[key].some(v => v !== undefined)){
|
||||
delete output[key]
|
||||
}
|
||||
}
|
||||
|
||||
function asBoolean(key: string, invert: boolean = false) {
|
||||
|
@ -379,6 +382,7 @@ export default class LinkedDataLoader {
|
|||
}
|
||||
return "€" + Number(p)
|
||||
})
|
||||
|
||||
if (output["charge"] && output["timeUnit"]) {
|
||||
const duration =
|
||||
Number(output["chargeEnd"] ?? "1") - Number(output["chargeStart"] ?? "0")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Translation, TypedTranslation } from "../UI/i18n/Translation"
|
||||
import { DenominationConfigJson } from "./ThemeConfig/Json/UnitConfigJson"
|
||||
import Translations from "../UI/i18n/Translations"
|
||||
import { Validator } from "../UI/InputElement/Validator"
|
||||
|
||||
/**
|
||||
* A 'denomination' is one way to write a certain quantity.
|
||||
|
@ -15,6 +16,7 @@ export class Denomination {
|
|||
public readonly alternativeDenominations: string[]
|
||||
public readonly human: TypedTranslation<{ quantity: string }>
|
||||
public readonly humanSingular?: Translation
|
||||
private readonly _validator: Validator
|
||||
|
||||
private constructor(
|
||||
canonical: string,
|
||||
|
@ -24,7 +26,8 @@ export class Denomination {
|
|||
addSpace: boolean,
|
||||
alternativeDenominations: string[],
|
||||
_human: TypedTranslation<{ quantity: string }>,
|
||||
_humanSingular?: Translation
|
||||
_humanSingular: Translation,
|
||||
validator: Validator
|
||||
) {
|
||||
this.canonical = canonical
|
||||
this._canonicalSingular = _canonicalSingular
|
||||
|
@ -34,9 +37,10 @@ export class Denomination {
|
|||
this.alternativeDenominations = alternativeDenominations
|
||||
this.human = _human
|
||||
this.humanSingular = _humanSingular
|
||||
this._validator = validator
|
||||
}
|
||||
|
||||
public static fromJson(json: DenominationConfigJson, context: string) {
|
||||
public static fromJson(json: DenominationConfigJson, validator: Validator, context: string) {
|
||||
context = `${context}.unit(${json.canonicalDenomination})`
|
||||
const canonical = json.canonicalDenomination.trim()
|
||||
if (canonical === undefined) {
|
||||
|
@ -68,7 +72,8 @@ export class Denomination {
|
|||
json.addSpace ?? false,
|
||||
json.alternativeDenomination?.map((v) => v.trim()) ?? [],
|
||||
humanTexts,
|
||||
Translations.T(json.humanSingular, context + "humanSingular")
|
||||
Translations.T(json.humanSingular, context + "humanSingular"),
|
||||
validator
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -81,7 +86,8 @@ export class Denomination {
|
|||
this.addSpace,
|
||||
this.alternativeDenominations,
|
||||
this.human,
|
||||
this.humanSingular
|
||||
this.humanSingular,
|
||||
this._validator
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -94,7 +100,8 @@ export class Denomination {
|
|||
this.addSpace,
|
||||
[this.canonical, ...this.alternativeDenominations],
|
||||
this.human,
|
||||
this.humanSingular
|
||||
this.humanSingular,
|
||||
this._validator
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -103,19 +110,21 @@ export class Denomination {
|
|||
* @param value the value from OSM
|
||||
* @param actAsDefault if set and the value can be parsed as number, will be parsed and trimmed
|
||||
*
|
||||
* import Validators from "../UI/InputElement/Validators"
|
||||
*
|
||||
* const unit = Denomination.fromJson({
|
||||
* canonicalDenomination: "m",
|
||||
* alternativeDenomination: ["meter"],
|
||||
* human: {
|
||||
* en: "{quantity} meter"
|
||||
* }
|
||||
* }, "test")
|
||||
* unit.canonicalValue("42m", true) // =>"42 m"
|
||||
* unit.canonicalValue("42", true) // =>"42 m"
|
||||
* unit.canonicalValue("42 m", true) // =>"42 m"
|
||||
* unit.canonicalValue("42 meter", true) // =>"42 m"
|
||||
* unit.canonicalValue("42m", true) // =>"42 m"
|
||||
* unit.canonicalValue("42", true) // =>"42 m"
|
||||
* }, Validators.get("float"), "test")
|
||||
* unit.canonicalValue("42m", true, false) // =>"42 m"
|
||||
* unit.canonicalValue("42", true, false) // =>"42 m"
|
||||
* unit.canonicalValue("42 m", true, false) // =>"42 m"
|
||||
* unit.canonicalValue("42 meter", true, false) // =>"42 m"
|
||||
* unit.canonicalValue("42m", true, false) // =>"42 m"
|
||||
* unit.canonicalValue("42", true, false) // =>"42 m"
|
||||
*
|
||||
* // Should be trimmed if canonical is empty
|
||||
* const unit = Denomination.fromJson({
|
||||
|
@ -124,22 +133,26 @@ export class Denomination {
|
|||
* human: {
|
||||
* en: "{quantity} meter"
|
||||
* }
|
||||
* }, "test")
|
||||
* unit.canonicalValue("42m", true) // =>"42"
|
||||
* unit.canonicalValue("42", true) // =>"42"
|
||||
* unit.canonicalValue("42 m", true) // =>"42"
|
||||
* unit.canonicalValue("42 meter", true) // =>"42"
|
||||
* }, Validators.get("float"), "test")
|
||||
* unit.canonicalValue("42m", true, false) // =>"42"
|
||||
* unit.canonicalValue("42", true, false) // =>"42"
|
||||
* unit.canonicalValue("42 m", true, false) // =>"42"
|
||||
* unit.canonicalValue("42 meter", true, false) // =>"42"
|
||||
*
|
||||
*
|
||||
*/
|
||||
public canonicalValue(value: string, actAsDefault: boolean): string {
|
||||
public canonicalValue(value: string, actAsDefault: boolean, inverted: boolean): string {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const stripped = this.StrippedValue(value, actAsDefault)
|
||||
const stripped = this.StrippedValue(value, actAsDefault, inverted)
|
||||
if (stripped === null) {
|
||||
return null
|
||||
}
|
||||
if(inverted){
|
||||
return (stripped + "/" + this.canonical).trim()
|
||||
|
||||
}
|
||||
if (stripped === "1" && this._canonicalSingular !== undefined) {
|
||||
return ("1 " + this._canonicalSingular).trim()
|
||||
}
|
||||
|
@ -153,7 +166,7 @@ export class Denomination {
|
|||
*
|
||||
* Returns null if it doesn't match this unit
|
||||
*/
|
||||
public StrippedValue(value: string, actAsDefault: boolean): string {
|
||||
public StrippedValue(value: string, actAsDefault: boolean, inverted: boolean): string {
|
||||
if (value === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -171,10 +184,16 @@ export class Denomination {
|
|||
|
||||
function substr(key) {
|
||||
if (self.prefix) {
|
||||
return value.substr(key.length).trim()
|
||||
} else {
|
||||
return value.substring(0, value.length - key.length).trim()
|
||||
return value.substring(key.length).trim()
|
||||
}
|
||||
let trimmed = value.substring(0, value.length - key.length).trim()
|
||||
if(!inverted){
|
||||
return trimmed
|
||||
}
|
||||
if(trimmed.endsWith("/")){
|
||||
trimmed = trimmed.substring(0, trimmed.length - 1).trim()
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
if (this.canonical !== "" && startsWith(this.canonical.toLowerCase())) {
|
||||
|
@ -199,11 +218,13 @@ export class Denomination {
|
|||
return null
|
||||
}
|
||||
|
||||
const parsed = Number(value.trim())
|
||||
if (!isNaN(parsed)) {
|
||||
return value.trim()
|
||||
if(!this._validator.isValid(value.trim())){
|
||||
return null
|
||||
}
|
||||
return this._validator.reformat(value.trim())
|
||||
}
|
||||
|
||||
return null
|
||||
withValidator(validator: Validator) {
|
||||
return new Denomination(this.canonical, this._canonicalSingular, this.useIfNoUnitGiven, this.prefix, this.addSpace, this.alternativeDenominations, this.human, this.humanSingular, validator)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,12 @@ export class UpdateLegacyLayer extends DesugaringStep<
|
|||
delete config["overpassTags"]
|
||||
}
|
||||
|
||||
if(config.allowMove?.["enableImproveAccuraccy"]){
|
||||
// Fix common misspelling: 'accuracy' is often typo'ed as 'accuraCCy'
|
||||
config.allowMove["enableImproveAccuracy"] = config.allowMove["enableImproveAccuraccy"]
|
||||
delete config.allowMove["enableImproveAccuraccy"]
|
||||
}
|
||||
|
||||
for (const preset of config.presets ?? []) {
|
||||
const preciseInput = preset["preciseInput"]
|
||||
if (typeof preciseInput === "boolean") {
|
||||
|
|
|
@ -230,7 +230,7 @@ class ExpandTagRendering extends Conversion<
|
|||
}
|
||||
for (let foundTr of indirect) {
|
||||
foundTr = Utils.Clone<any>(foundTr)
|
||||
ctx.Merge(tagRenderingConfigJson["override"] ?? {}, foundTr)
|
||||
ctx.MergeObjectsForOverride(tagRenderingConfigJson["override"] ?? {}, foundTr)
|
||||
foundTr["id"] = tagRenderingConfigJson["id"] ?? foundTr["id"]
|
||||
result.push(foundTr)
|
||||
}
|
||||
|
|
|
@ -1647,6 +1647,10 @@ export class ValidateLayer extends Conversion<
|
|||
}
|
||||
}
|
||||
|
||||
if(json.allowMove?.["enableAccuraccy"] !== undefined){
|
||||
context.enters("allowMove", "enableAccuracy").err("`enableAccuracy` is written with two C in the first occurrence and only one in the last")
|
||||
}
|
||||
|
||||
return { raw: json, parsed: layerConfig }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,17 +291,24 @@ export interface LayerConfigJson {
|
|||
forceLoad?: false | boolean
|
||||
|
||||
/**
|
||||
* Presets for this layer.
|
||||
* A preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);
|
||||
* it will prompt the user to add a new point.
|
||||
* <div class='flex'>
|
||||
* <div>
|
||||
* Presets for this layer.
|
||||
*
|
||||
* The most important aspect are the tags, which define which tags the new point will have;
|
||||
* The title is shown in the dialog, along with the first sentence of the description.
|
||||
* A preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.
|
||||
*
|
||||
* Upon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.
|
||||
* When the contributor wishes to add a point to OpenStreetMap, they'll:
|
||||
*
|
||||
* Note: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!
|
||||
* NB: if no presets are defined, the popup to add new points doesn't show up at all
|
||||
* 1. Press the 'add new point'-button
|
||||
* 2. Choose a preset from the list of all presets
|
||||
* 3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown
|
||||
* 4. Confirm the location
|
||||
* 5. A new point will be created with the attributes that were defined in the preset
|
||||
*
|
||||
* If no presets are defined, the button which invites to add a new preset will not be shown.
|
||||
*</div>
|
||||
* <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/>
|
||||
*</div>
|
||||
*
|
||||
* group: presets
|
||||
*/
|
||||
|
@ -512,6 +519,7 @@ export interface LayerConfigJson {
|
|||
/**
|
||||
* Either a list with [{"key": "unitname", "key2": {"quantity": "unitname", "denominations": ["denom", "denom"]}}]
|
||||
*
|
||||
* Use `"inverted": true` if the amount should be _divided_ by the denomination, e.g. for charge over time (`€5/day`)
|
||||
*
|
||||
* @see UnitConfigJson
|
||||
*
|
||||
|
@ -519,7 +527,7 @@ export interface LayerConfigJson {
|
|||
*/
|
||||
units?: (
|
||||
| UnitConfigJson
|
||||
| Record<string, string | { quantity: string; denominations: string[]; canonical?: string }>
|
||||
| Record<string, string | { quantity: string; denominations: string[]; canonical?: string, inverted?: boolean }>
|
||||
)[]
|
||||
|
||||
/**
|
||||
|
|
|
@ -157,17 +157,16 @@ export interface LayoutConfigJson {
|
|||
* types: hidden | layer | hidden
|
||||
* group: layers
|
||||
* suggestions: return Array.from(layers.keys()).map(key => ({if: "value="+key, then: "<b>"+key+"</b> (builtin) - "+layers.get(key).description}))
|
||||
* Every layer contains a description of which feature to display - the overpassTags which are queried.
|
||||
* Instead of running one query for every layer, the query is fused.
|
||||
*
|
||||
* Afterwards, every layer is given the list of features.
|
||||
* Every layer takes away the features that match with them*, and give the leftovers to the next layers.
|
||||
* A theme must contain at least one layer.
|
||||
*
|
||||
* This implies that the _order_ of the layers is important in the case of features with the same tags;
|
||||
* as the later layers might never receive their feature.
|
||||
* A layer contains all features of a single type, for example "shops", "bicycle pumps", "benches".
|
||||
* Note that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.
|
||||
* If a feature can match multiple layers, the first matching layer in the list will be used.
|
||||
* This implies that the _order_ of the layers is important.
|
||||
*
|
||||
* *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself
|
||||
*
|
||||
* <div class='hidden-in-studio'>
|
||||
* Note that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: "layername", override: ...}
|
||||
*
|
||||
* The 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer
|
||||
|
@ -194,6 +193,7 @@ export interface LayoutConfigJson {
|
|||
* "override": {"minzoom": 12}
|
||||
* }
|
||||
*```
|
||||
* </div>
|
||||
*/
|
||||
layers: (
|
||||
| LayerConfigJson
|
||||
|
@ -362,7 +362,7 @@ export interface LayoutConfigJson {
|
|||
/**
|
||||
* question: Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?
|
||||
* iftrue: Enable the option to download the map as CSV and GeoJson
|
||||
* iffalse: Enable the option to download the map as CSV and GeoJson
|
||||
* iffalse: Disable the option to download the map as CSV and GeoJson
|
||||
* ifunset: MapComplete default: Enable the option to download the map as CSV and GeoJson
|
||||
* group: feature_switches
|
||||
*/
|
||||
|
|
|
@ -97,18 +97,6 @@ export default class LayerConfig extends WithContextLoader {
|
|||
|
||||
this.allowSplit = json.allowSplit ?? false
|
||||
this.name = Translations.T(json.name, translationContext + ".name")
|
||||
if (json.units !== undefined && !Array.isArray(json.units)) {
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
".units: the 'units'-section should be a list; you probably have an object there"
|
||||
)
|
||||
}
|
||||
this.units = [].concat(
|
||||
...(json.units ?? []).map((unitJson, i) =>
|
||||
Unit.fromJson(unitJson, `${context}.unit[${i}]`)
|
||||
)
|
||||
)
|
||||
|
||||
if (json.description !== undefined) {
|
||||
if (Object.keys(json.description).length === 0) {
|
||||
|
@ -280,6 +268,18 @@ export default class LayerConfig extends WithContextLoader {
|
|||
this.id + ".tagRenderings[" + i + "]"
|
||||
)
|
||||
)
|
||||
if (json.units !== undefined && !Array.isArray(json.units)) {
|
||||
throw (
|
||||
"At " +
|
||||
context +
|
||||
".units: the 'units'-section should be a list; you probably have an object there"
|
||||
)
|
||||
}
|
||||
this.units = [].concat(
|
||||
...(json.units ?? []).map((unitJson, i) =>
|
||||
Unit.fromJson(unitJson, this.tagRenderings,`${context}.unit[${i}]`)
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
json.filter !== undefined &&
|
||||
|
@ -368,7 +368,6 @@ export default class LayerConfig extends WithContextLoader {
|
|||
canBeIncluded = true
|
||||
): BaseUIElement {
|
||||
const extraProps: (string | BaseUIElement)[] = []
|
||||
|
||||
extraProps.push("This layer is shown at zoomlevel **" + this.minzoom + "** and higher")
|
||||
|
||||
if (canBeIncluded) {
|
||||
|
@ -424,7 +423,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
if (!addedByDefault) {
|
||||
if (usedInThemes?.length > 0) {
|
||||
usingLayer = [
|
||||
new Title("Themes using this layer", 4),
|
||||
new Title("Themes using this layer", 2),
|
||||
new List(
|
||||
(usedInThemes ?? []).map(
|
||||
(id) => new Link(id, "https://mapcomplete.org/" + id)
|
||||
|
|
|
@ -12,6 +12,8 @@ import { VariableUiElement } from "../../UI/Base/VariableUIElement"
|
|||
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
|
||||
import SvelteUIElement from "../../UI/Base/SvelteUIElement"
|
||||
import DynamicMarker from "../../UI/Map/DynamicMarker.svelte"
|
||||
import { UIElement } from "../../UI/UIElement"
|
||||
import Img from "../../UI/Base/Img"
|
||||
|
||||
export class IconConfig extends WithContextLoader {
|
||||
public static readonly defaultIcon = new IconConfig({ icon: "pin", color: "#ff9939" })
|
||||
|
@ -121,9 +123,14 @@ export default class PointRenderingConfig extends WithContextLoader {
|
|||
context + ".rotationAlignment"
|
||||
)
|
||||
}
|
||||
private static FromHtmlMulti(multiSpec: string, tags: Store<Record<string, string>>) {
|
||||
private static FromHtmlMulti(multiSpec: string, tags: Store<Record<string, string>>): BaseUIElement {
|
||||
const icons: IconConfig[] = []
|
||||
|
||||
for (const subspec of multiSpec.split(";")) {
|
||||
if(subspec.startsWith("http://") || subspec.startsWith("https://")){
|
||||
icons.push(new IconConfig({icon: subspec}))
|
||||
continue
|
||||
}
|
||||
const [icon, color] = subspec.split(":")
|
||||
icons.push(new IconConfig({ icon, color }))
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ export default class TagRenderingConfig {
|
|||
public readonly renderIconClass?: string
|
||||
public readonly question?: TypedTranslation<object>
|
||||
public readonly questionhint?: TypedTranslation<object>
|
||||
public readonly questionHintIsMd?: boolean
|
||||
public readonly condition?: TagsFilter
|
||||
public readonly invalidValues?: TagsFilter
|
||||
/**
|
||||
|
@ -80,7 +81,7 @@ export default class TagRenderingConfig {
|
|||
public readonly classes: string[] | undefined
|
||||
|
||||
constructor(
|
||||
config: string | TagRenderingConfigJson | QuestionableTagRenderingConfigJson,
|
||||
config: string | TagRenderingConfigJson | (QuestionableTagRenderingConfigJson & {questionHintIsMd?: boolean}),
|
||||
context?: string
|
||||
) {
|
||||
let json = <string | QuestionableTagRenderingConfigJson>config
|
||||
|
@ -136,6 +137,7 @@ export default class TagRenderingConfig {
|
|||
this.render = Translations.T(<any>json.render, translationKey + ".render")
|
||||
this.question = Translations.T(json.question, translationKey + ".question")
|
||||
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
|
||||
this.questionHintIsMd = json["questionHintIsMd"] ?? false
|
||||
this.description = Translations.T(json.description, translationKey + ".description")
|
||||
this.editButtonAriaLabel = Translations.T(
|
||||
json.editButtonAriaLabel,
|
||||
|
|
|
@ -2,6 +2,10 @@ import BaseUIElement from "../UI/BaseUIElement"
|
|||
import { Denomination } from "./Denomination"
|
||||
import UnitConfigJson from "./ThemeConfig/Json/UnitConfigJson"
|
||||
import unit from "../../assets/layers/unit/unit.json"
|
||||
import { QuestionableTagRenderingConfigJson } from "./ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import TagRenderingConfig from "./ThemeConfig/TagRenderingConfig"
|
||||
import Validators, { ValidatorType } from "../UI/InputElement/Validators"
|
||||
import { Validator } from "../UI/InputElement/Validator"
|
||||
|
||||
export class Unit {
|
||||
private static allUnits = this.initUnits()
|
||||
|
@ -10,14 +14,20 @@ export class Unit {
|
|||
public readonly denominationsSorted: Denomination[]
|
||||
public readonly eraseInvalid: boolean
|
||||
public readonly quantity: string
|
||||
private readonly _validator: Validator
|
||||
public readonly inverted: boolean
|
||||
|
||||
constructor(
|
||||
quantity: string,
|
||||
appliesToKeys: string[],
|
||||
applicableDenominations: Denomination[],
|
||||
eraseInvalid: boolean
|
||||
eraseInvalid: boolean,
|
||||
validator: Validator,
|
||||
inverted: boolean = false
|
||||
) {
|
||||
this.quantity = quantity
|
||||
this._validator = validator
|
||||
this.inverted = inverted
|
||||
this.appliesToKeys = new Set(appliesToKeys)
|
||||
this.denominations = applicableDenominations
|
||||
this.eraseInvalid = eraseInvalid
|
||||
|
@ -66,13 +76,47 @@ export class Unit {
|
|||
static fromJson(
|
||||
json:
|
||||
| UnitConfigJson
|
||||
| Record<string, string | { quantity: string; denominations: string[] }>,
|
||||
| Record<string, string | { quantity: string; denominations: string[], inverted?: boolean }>,
|
||||
tagRenderings: TagRenderingConfig[],
|
||||
ctx: string
|
||||
): Unit[] {
|
||||
if (!json.appliesToKey && !json.quantity) {
|
||||
return this.loadFromLibrary(<any>json, ctx)
|
||||
|
||||
let types: Record<string, ValidatorType> = {}
|
||||
for (const tagRendering of tagRenderings) {
|
||||
if (tagRendering.freeform?.type) {
|
||||
types[tagRendering.freeform.key] = tagRendering.freeform.type
|
||||
}
|
||||
}
|
||||
return [this.parse(<UnitConfigJson>json, ctx)]
|
||||
|
||||
if (!json.appliesToKey && !json.quantity) {
|
||||
return this.loadFromLibrary(<any>json, types, ctx)
|
||||
}
|
||||
return this.parse(<UnitConfigJson>json, types, ctx)
|
||||
}
|
||||
|
||||
private static parseDenomination(json: UnitConfigJson, validator: Validator, appliesToKey: string, ctx: string): Unit {
|
||||
const applicable = json.applicableUnits.map((u, i) =>
|
||||
Denomination.fromJson(u, validator, `${ctx}.units[${i}]`)
|
||||
)
|
||||
|
||||
if (
|
||||
json.defaultInput &&
|
||||
!applicable.some((denom) => denom.canonical.trim() === json.defaultInput)
|
||||
) {
|
||||
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${
|
||||
json.defaultInput
|
||||
}', but the available denominations are ${applicable
|
||||
.map((denom) => denom.canonical)
|
||||
.join(", ")}`
|
||||
}
|
||||
|
||||
return new Unit(
|
||||
json.quantity ?? "",
|
||||
appliesToKey === undefined ? undefined : [appliesToKey],
|
||||
applicable,
|
||||
json.eraseInvalidValues ?? false,
|
||||
validator
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -113,7 +157,7 @@ export class Unit {
|
|||
* ]
|
||||
* }, "test")
|
||||
*/
|
||||
private static parse(json: UnitConfigJson, ctx: string): Unit {
|
||||
private static parse(json: UnitConfigJson, types: Record<string, ValidatorType>, ctx: string): Unit[] {
|
||||
const appliesTo = json.appliesToKey
|
||||
for (let i = 0; i < (appliesTo ?? []).length; i++) {
|
||||
let key = appliesTo[i]
|
||||
|
@ -127,32 +171,22 @@ export class Unit {
|
|||
}
|
||||
// Some keys do have unit handling
|
||||
|
||||
const applicable = json.applicableUnits.map((u, i) =>
|
||||
Denomination.fromJson(u, `${ctx}.units[${i}]`)
|
||||
)
|
||||
|
||||
if (
|
||||
json.defaultInput &&
|
||||
!applicable.some((denom) => denom.canonical.trim() === json.defaultInput)
|
||||
) {
|
||||
throw `${ctx}: no denomination has the specified default denomination. The default denomination is '${
|
||||
json.defaultInput
|
||||
}', but the available denominations are ${applicable
|
||||
.map((denom) => denom.canonical)
|
||||
.join(", ")}`
|
||||
const units: Unit[] = []
|
||||
if (appliesTo === undefined) {
|
||||
units.push(this.parseDenomination(json, Validators.get("float"), undefined, ctx))
|
||||
}
|
||||
return new Unit(
|
||||
json.quantity ?? "",
|
||||
appliesTo,
|
||||
applicable,
|
||||
json.eraseInvalidValues ?? false
|
||||
)
|
||||
for (const key of appliesTo ?? []) {
|
||||
const validator = Validators.get(types[key] ?? "float")
|
||||
units.push(this.parseDenomination(json, validator, undefined, ctx))
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
||||
private static initUnits(): Map<string, Unit> {
|
||||
const m = new Map<string, Unit>()
|
||||
const units = (<UnitConfigJson[]>unit.units).map((json, i) =>
|
||||
this.parse(json, "unit.json.units." + i)
|
||||
const units = (<UnitConfigJson[]>unit.units).flatMap((json, i) =>
|
||||
this.parse(json, {}, "unit.json.units." + i)
|
||||
)
|
||||
|
||||
for (const unit of units) {
|
||||
|
@ -179,17 +213,19 @@ export class Unit {
|
|||
private static loadFromLibrary(
|
||||
spec: Record<
|
||||
string,
|
||||
string | { quantity: string; denominations: string[]; canonical?: string }
|
||||
string | { quantity: string; denominations: string[]; canonical?: string, inverted?: boolean }
|
||||
>,
|
||||
types: Record<string, ValidatorType>,
|
||||
ctx: string
|
||||
): Unit[] {
|
||||
const units: Unit[] = []
|
||||
for (const key in spec) {
|
||||
const toLoad = spec[key]
|
||||
const validator = Validators.get(types[key] ?? "float")
|
||||
if (typeof toLoad === "string") {
|
||||
const loaded = this.getFromLibrary(toLoad, ctx)
|
||||
units.push(
|
||||
new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid)
|
||||
new Unit(loaded.quantity, [key], loaded.denominations, loaded.eraseInvalid, validator, toLoad["inverted"])
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
@ -213,12 +249,13 @@ export class Unit {
|
|||
const denoms = toLoad.denominations
|
||||
.map((d) => d.toLowerCase())
|
||||
.map((d) => fetchDenom(d))
|
||||
.map(d => d.withValidator(validator))
|
||||
|
||||
if (toLoad.canonical) {
|
||||
const canonical = fetchDenom(toLoad.canonical)
|
||||
const canonical = fetchDenom(toLoad.canonical).withValidator(validator)
|
||||
denoms.unshift(canonical.withBlankCanonical())
|
||||
}
|
||||
units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid))
|
||||
units.push(new Unit(loaded.quantity, [key], denoms, loaded.eraseInvalid, validator, toLoad["inverted"]))
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
@ -240,7 +277,7 @@ export class Unit {
|
|||
}
|
||||
const defaultDenom = this.getDefaultDenomination(country)
|
||||
for (const denomination of this.denominationsSorted) {
|
||||
const bare = denomination.StrippedValue(valueWithDenom, defaultDenom === denomination)
|
||||
const bare = denomination.StrippedValue(valueWithDenom, defaultDenom === denomination, this.inverted)
|
||||
if (bare !== null) {
|
||||
return [bare, denomination]
|
||||
}
|
||||
|
@ -253,10 +290,13 @@ export class Unit {
|
|||
return undefined
|
||||
}
|
||||
const [stripped, denom] = this.findDenomination(value, country)
|
||||
const human = denom?.human
|
||||
if(this.inverted ){
|
||||
return human.Subs({quantity: stripped+"/"})
|
||||
}
|
||||
if (stripped === "1") {
|
||||
return denom?.humanSingular ?? stripped
|
||||
}
|
||||
const human = denom?.human
|
||||
if (human === undefined) {
|
||||
return stripped ?? value
|
||||
}
|
||||
|
@ -266,6 +306,10 @@ export class Unit {
|
|||
|
||||
public toOsm(value: string, denomination: string) {
|
||||
const denom = this.denominations.find((d) => d.canonical === denomination)
|
||||
if(this.inverted){
|
||||
return value+"/"+denom._canonicalSingular
|
||||
}
|
||||
|
||||
const space = denom.addSpace ? " " : ""
|
||||
if (denom.prefix) {
|
||||
return denom.canonical + space + value
|
||||
|
@ -273,7 +317,7 @@ export class Unit {
|
|||
return value + space + denom.canonical
|
||||
}
|
||||
|
||||
public getDefaultDenomination(country: () => string) {
|
||||
public getDefaultDenomination(country: () => string): Denomination {
|
||||
for (const denomination of this.denominations) {
|
||||
if (denomination.useIfNoUnitGiven === true) {
|
||||
return denomination
|
||||
|
|
17
src/UI/Base/Markdown.svelte
Normal file
17
src/UI/Base/Markdown.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { marked } from "marked"
|
||||
export let src: string
|
||||
export let srcWritable: UIEventSource<string> = undefined
|
||||
srcWritable?.addCallbackAndRunD(t => {
|
||||
src = t
|
||||
})
|
||||
if(src !== undefined && typeof src !== "string") {
|
||||
console.trace("Got a non-string object in Markdown", src)
|
||||
throw "Markdown.svelte got a non-string object"
|
||||
}
|
||||
</script>
|
||||
{#if src?.length > 0}
|
||||
{@html marked.parse(src)}
|
||||
{/if}
|
||||
|
|
@ -1,73 +1,21 @@
|
|||
import Combine from "./Combine"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import { FixedUiElement } from "./FixedUiElement"
|
||||
import Title from "./Title"
|
||||
import List from "./List"
|
||||
import Link from "./Link"
|
||||
import { marked } from "marked"
|
||||
import { parse as parse_html } from "node-html-parser"
|
||||
import {default as turndown} from "turndown"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class TableOfContents extends Combine {
|
||||
private readonly titles: Title[]
|
||||
export default class TableOfContents {
|
||||
|
||||
constructor(
|
||||
elements: Combine | Title[],
|
||||
options?: {
|
||||
noTopLevel: false | boolean
|
||||
maxDepth?: number
|
||||
}
|
||||
) {
|
||||
let titles: Title[]
|
||||
if (elements instanceof Combine) {
|
||||
titles = TableOfContents.getTitles(elements.getElements()) ?? []
|
||||
} else {
|
||||
titles = elements ?? []
|
||||
}
|
||||
|
||||
let els: { level: number; content: BaseUIElement }[] = []
|
||||
for (const title of titles) {
|
||||
let content: BaseUIElement
|
||||
if (title.title instanceof Translation) {
|
||||
content = title.title.Clone()
|
||||
} else if (title.title instanceof FixedUiElement) {
|
||||
content = new FixedUiElement(title.title.content)
|
||||
} else if (Utils.runningFromConsole) {
|
||||
content = new FixedUiElement(title.AsMarkdown())
|
||||
} else if (title["title"] !== undefined) {
|
||||
content = new FixedUiElement(title.title.ConstructElement().textContent)
|
||||
} else {
|
||||
console.log("Not generating a title for ", title)
|
||||
continue
|
||||
}
|
||||
|
||||
const vis = new Link(content, "#" + title.id)
|
||||
|
||||
els.push({ level: title.level, content: vis })
|
||||
}
|
||||
const minLevel = Math.min(...els.map((e) => e.level))
|
||||
if (options?.noTopLevel) {
|
||||
els = els.filter((e) => e.level !== minLevel)
|
||||
}
|
||||
|
||||
if (options?.maxDepth) {
|
||||
els = els.filter((e) => e.level <= options.maxDepth + minLevel)
|
||||
}
|
||||
|
||||
super(TableOfContents.mergeLevel(els).map((el) => el.SetClass("mt-2")))
|
||||
this.SetClass("flex flex-col")
|
||||
this.titles = titles
|
||||
}
|
||||
|
||||
private static getTitles(elements: BaseUIElement[]): Title[] {
|
||||
const titles = []
|
||||
for (const uiElement of elements) {
|
||||
if (uiElement instanceof Combine) {
|
||||
titles.push(...TableOfContents.getTitles(uiElement.getElements()))
|
||||
} else if (uiElement instanceof Title) {
|
||||
titles.push(uiElement)
|
||||
}
|
||||
}
|
||||
return titles
|
||||
private static asLinkableId(text: string): string {
|
||||
return text
|
||||
?.replace(/ /g, "-")
|
||||
?.replace(/[?#.;:/]/, "")
|
||||
?.toLowerCase() ?? ""
|
||||
}
|
||||
|
||||
private static mergeLevel(
|
||||
|
@ -88,7 +36,7 @@ export default class TableOfContents extends Combine {
|
|||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1,
|
||||
level: maxLevel - 1
|
||||
})
|
||||
running = []
|
||||
}
|
||||
|
@ -97,24 +45,81 @@ export default class TableOfContents extends Combine {
|
|||
if (running.length !== undefined) {
|
||||
result.push({
|
||||
content: new List(running),
|
||||
level: maxLevel - 1,
|
||||
level: maxLevel - 1
|
||||
})
|
||||
}
|
||||
|
||||
return TableOfContents.mergeLevel(result)
|
||||
}
|
||||
|
||||
AsMarkdown(): string {
|
||||
const depthIcons = ["1.", " -", " +", " *"]
|
||||
const lines = ["## Table of contents\n"]
|
||||
const minLevel = Math.min(...this.titles.map((t) => t.level))
|
||||
for (const title of this.titles) {
|
||||
const prefix = depthIcons[title.level - minLevel] ?? " ~"
|
||||
const text = title.title.AsMarkdown().replace("\n", "")
|
||||
const link = title.id
|
||||
lines.push(prefix + " [" + text + "](#" + link + ")")
|
||||
public static insertTocIntoMd(md: string): string {
|
||||
const htmlSource = <string>marked.parse(md)
|
||||
const el = parse_html(htmlSource)
|
||||
const structure = TableOfContents.generateStructure(<any>el)
|
||||
let firstTitle = structure[1]
|
||||
let minDepth = undefined
|
||||
do {
|
||||
minDepth = Math.min(...structure.map(s => s.depth))
|
||||
const minDepthCount = structure.filter(s => s.depth === minDepth)
|
||||
if (minDepthCount.length > 1) {
|
||||
break
|
||||
}
|
||||
// Erase a single top level heading
|
||||
structure.splice(structure.findIndex(s => s.depth === minDepth), 1)
|
||||
} while (structure.length > 0)
|
||||
|
||||
if (structure.length <= 1) {
|
||||
return md
|
||||
}
|
||||
const separators = {
|
||||
1: " -",
|
||||
2: " +",
|
||||
3: " *"
|
||||
}
|
||||
|
||||
return lines.join("\n") + "\n\n"
|
||||
let toc = ""
|
||||
let topLevelCount = 0
|
||||
for (const el of structure) {
|
||||
const depthDiff = el.depth - minDepth
|
||||
let link = `[${el.title}](#${TableOfContents.asLinkableId(el.title)})`
|
||||
if (depthDiff === 0) {
|
||||
topLevelCount++
|
||||
toc += `${topLevelCount}. ${link}\n`
|
||||
} else if (depthDiff <= 3) {
|
||||
toc += `${separators[depthDiff]} ${link}\n`
|
||||
}
|
||||
}
|
||||
|
||||
const heading = Utils.Times(() => "#", firstTitle.depth)
|
||||
toc = heading +" Table of contents\n\n"+toc
|
||||
|
||||
const original = el.outerHTML
|
||||
const firstTitleIndex = original.indexOf(firstTitle.el.outerHTML)
|
||||
const tocHtml = (<string>marked.parse(toc))
|
||||
const withToc = original.substring(0, firstTitleIndex) + tocHtml + original.substring(firstTitleIndex)
|
||||
|
||||
const htmlToMd = new turndown()
|
||||
return htmlToMd.turndown(withToc)
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static generateStructure(html: Element): { depth: number, title: string, el: Element }[] {
|
||||
if (html === undefined) {
|
||||
return []
|
||||
}
|
||||
return [].concat(...Array.from(html.childNodes ?? []).map(
|
||||
child => {
|
||||
const tag: string = child["tagName"]?.toLowerCase()
|
||||
if (!tag) {
|
||||
return []
|
||||
}
|
||||
if (tag.match(/h[0-9]/)) {
|
||||
const depth = Number(tag.substring(1))
|
||||
return [{ depth, title: child.textContent, el: child }]
|
||||
}
|
||||
return TableOfContents.generateStructure(<Element>child)
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
56
src/UI/BigComponents/ExtraLinkButton.svelte
Normal file
56
src/UI/BigComponents/ExtraLinkButton.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
import Translations from "../i18n/Translations"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Pop_out from "../../assets/svg/Pop_out.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
let theme = state.layout?.id ?? ""
|
||||
let config: ExtraLinkConfig = state.layout.extraLink
|
||||
console.log(">>>",config)
|
||||
const isIframe = window !== window.top
|
||||
let basepath = window.location.host
|
||||
let showWelcomeMessageSwitch = state.featureSwitches.featureSwitchWelcomeMessage
|
||||
|
||||
const t = Translations.t.general
|
||||
const href = state.mapProperties.location.map(
|
||||
(loc) => {
|
||||
const subs = {
|
||||
...loc,
|
||||
theme: theme,
|
||||
basepath,
|
||||
language: Locale.language.data
|
||||
}
|
||||
return Utils.SubstituteKeys(config.href, subs)
|
||||
},
|
||||
[state.mapProperties.zoom]
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
{#if config !== undefined &&
|
||||
!(config.requirements.has("iframe") && !isIframe) &&
|
||||
!(config.requirements.has("no-iframe") && isIframe) &&
|
||||
!(config.requirements.has("welcome-message") && !$showWelcomeMessageSwitch) &&
|
||||
!(config.requirements.has("no-welcome-message") && $showWelcomeMessageSwitch)}
|
||||
<div class="links-as-button">
|
||||
|
||||
<a href={$href} target={config.newTab ? "_blank" : ""} rel="noopener"
|
||||
class="flex pointer-events-auto rounded-full border-black">
|
||||
|
||||
<Icon icon={config.icon} clss="w-6 h-6 m-2"/>
|
||||
|
||||
{#if config.text}
|
||||
<Tr t={config.text} />
|
||||
{:else}
|
||||
<Tr t={t.screenToSmall.Subs({theme: state.layout.title})} />
|
||||
{/if}
|
||||
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
|
@ -1,101 +0,0 @@
|
|||
import { UIElement } from "../UIElement"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
|
||||
import Img from "../Base/Img"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
|
||||
interface ExtraLinkButtonState {
|
||||
layout: { id: string; title: Translation }
|
||||
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> }
|
||||
mapProperties: {
|
||||
location: Store<{ lon: number; lat: number }>
|
||||
zoom: Store<number>
|
||||
}
|
||||
}
|
||||
export default class ExtraLinkButton extends UIElement {
|
||||
private readonly _config: ExtraLinkConfig
|
||||
private readonly state: ExtraLinkButtonState
|
||||
|
||||
constructor(state: ExtraLinkButtonState, config: ExtraLinkConfig) {
|
||||
super()
|
||||
this.state = state
|
||||
this._config = config
|
||||
}
|
||||
|
||||
protected InnerRender(): BaseUIElement {
|
||||
if (this._config === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const c = this._config
|
||||
|
||||
const isIframe = window !== window.top
|
||||
if (c.requirements?.has("iframe") && !isIframe) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (c.requirements?.has("no-iframe") && isIframe) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let link: BaseUIElement
|
||||
const theme = this.state.layout?.id ?? ""
|
||||
const basepath = window.location.host
|
||||
const href = this.state.mapProperties.location.map(
|
||||
(loc) => {
|
||||
const subs = {
|
||||
...loc,
|
||||
theme: theme,
|
||||
basepath,
|
||||
language: Locale.language.data,
|
||||
}
|
||||
return Utils.SubstituteKeys(c.href, subs)
|
||||
},
|
||||
[this.state.mapProperties.zoom]
|
||||
)
|
||||
|
||||
let img: BaseUIElement = Svg.pop_out_svg()
|
||||
if (c.icon !== undefined) {
|
||||
img = new Img(c.icon).SetClass("h-6")
|
||||
}
|
||||
|
||||
let text: Translation
|
||||
if (c.text === undefined) {
|
||||
text = Translations.t.general.screenToSmall.Subs({
|
||||
theme: this.state.layout.title,
|
||||
})
|
||||
} else {
|
||||
text = c.text.Clone()
|
||||
}
|
||||
|
||||
link = new SubtleButton(img, text, {
|
||||
url: href,
|
||||
newTab: c.newTab,
|
||||
})
|
||||
|
||||
if (c.requirements?.has("no-welcome-message")) {
|
||||
link = new Toggle(
|
||||
undefined,
|
||||
link,
|
||||
this.state.featureSwitches.featureSwitchWelcomeMessage
|
||||
)
|
||||
}
|
||||
|
||||
if (c.requirements?.has("welcome-message")) {
|
||||
link = new Toggle(
|
||||
link,
|
||||
undefined,
|
||||
this.state.featureSwitches.featureSwitchWelcomeMessage
|
||||
)
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
}
|
|
@ -119,7 +119,7 @@
|
|||
<Tr t={t.conflicting.intro} />
|
||||
{/if}
|
||||
{#if $different.length > 0}
|
||||
{#each $different as key}
|
||||
{#each $different as key (key)}
|
||||
<div class="mx-2 rounded-2xl">
|
||||
<ComparisonAction
|
||||
{key}
|
||||
|
@ -136,7 +136,7 @@
|
|||
|
||||
{#if $missing.length > 0}
|
||||
{#if currentStep === "init"}
|
||||
{#each $missing as key}
|
||||
{#each $missing as key (key)}
|
||||
<div class:glowing-shadow={applyAllHovered} class="mx-2 rounded-2xl">
|
||||
<ComparisonAction
|
||||
{key}
|
||||
|
@ -174,7 +174,7 @@
|
|||
{#if readonly}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div class="flex h-32 w-max gap-x-2">
|
||||
{#each $unknownImages as image}
|
||||
{#each $unknownImages as image (image)}
|
||||
<AttributedImage
|
||||
imgClass="h-32 w-max shrink-0"
|
||||
image={{ url: image }}
|
||||
|
@ -184,7 +184,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#each $unknownImages as image}
|
||||
{#each $unknownImages as image (image)}
|
||||
<LinkableImage
|
||||
{tags}
|
||||
{state}
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export class FixedInputElement<T> extends InputElement<T> {
|
||||
private readonly value: UIEventSource<T>
|
||||
private readonly _comparator: (t0: T, t1: T) => boolean
|
||||
|
||||
private readonly _el: HTMLElement
|
||||
|
||||
constructor(
|
||||
rendering: BaseUIElement | string,
|
||||
value: T | UIEventSource<T>,
|
||||
comparator: (t0: T, t1: T) => boolean = undefined
|
||||
) {
|
||||
super()
|
||||
this._comparator = comparator ?? ((t0, t1) => t0 == t1)
|
||||
if (value instanceof UIEventSource) {
|
||||
this.value = value
|
||||
} else {
|
||||
this.value = new UIEventSource<T>(value)
|
||||
}
|
||||
|
||||
this._el = document.createElement("span")
|
||||
const e = Translations.W(rendering)?.ConstructElement()
|
||||
if (e) {
|
||||
this._el.appendChild(e)
|
||||
}
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._comparator(t, this.value.data)
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._el
|
||||
}
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
import { InputElement } from "./InputElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export class RadioButton<T> extends InputElement<T> {
|
||||
private static _nextId = 0
|
||||
|
||||
private readonly value: UIEventSource<T>
|
||||
private _elements: InputElement<T>[]
|
||||
private _selectFirstAsDefault: boolean
|
||||
private _dontStyle: boolean
|
||||
|
||||
constructor(
|
||||
elements: InputElement<T>[],
|
||||
options?: {
|
||||
selectFirstAsDefault?: true | boolean
|
||||
dontStyle?: boolean
|
||||
value?: UIEventSource<T>
|
||||
}
|
||||
) {
|
||||
super()
|
||||
options = options ?? {}
|
||||
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true
|
||||
this._elements = Utils.NoNull(elements)
|
||||
this.value = options?.value ?? new UIEventSource<T>(undefined)
|
||||
this._dontStyle = options.dontStyle ?? false
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
for (const inputElement of this._elements) {
|
||||
if (inputElement.IsValid(t)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this.value
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const elements = this._elements
|
||||
const selectFirstAsDefault = this._selectFirstAsDefault
|
||||
|
||||
const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(null)
|
||||
|
||||
const value = UIEventSource.flatten(
|
||||
selectedElementIndex.map((selectedIndex) => {
|
||||
if (selectedIndex !== undefined && selectedIndex !== null) {
|
||||
return elements[selectedIndex].GetValue()
|
||||
}
|
||||
}),
|
||||
elements.map((e) => e?.GetValue())
|
||||
)
|
||||
value.syncWith(this.value)
|
||||
|
||||
if (selectFirstAsDefault) {
|
||||
value.addCallbackAndRun((selected) => {
|
||||
if (selected === undefined) {
|
||||
for (const element of elements) {
|
||||
const v = element.GetValue().data
|
||||
if (v !== undefined) {
|
||||
value.setData(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
// If an element is clicked, the radio button corresponding with it should be selected as well
|
||||
elements[i]?.onClick(() => {
|
||||
selectedElementIndex.setData(i)
|
||||
})
|
||||
|
||||
elements[i].GetValue().addCallback(() => {
|
||||
selectedElementIndex.setData(i)
|
||||
})
|
||||
}
|
||||
|
||||
const groupId = "radiogroup" + RadioButton._nextId
|
||||
RadioButton._nextId++
|
||||
|
||||
const form = document.createElement("form")
|
||||
|
||||
const inputs = []
|
||||
const wrappers: HTMLElement[] = []
|
||||
|
||||
for (let i1 = 0; i1 < elements.length; i1++) {
|
||||
let element = elements[i1]
|
||||
const labelHtml = element.ConstructElement()
|
||||
if (labelHtml === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
const input = document.createElement("input")
|
||||
input.id = "radio" + groupId + "-" + i1
|
||||
input.name = groupId
|
||||
input.type = "radio"
|
||||
input.classList.add("cursor-pointer", "p-1", "mr-2")
|
||||
|
||||
if (!this._dontStyle) {
|
||||
input.classList.add("p-1", "ml-2", "pl-2", "pr-0", "m-3", "mr-0")
|
||||
}
|
||||
input.onchange = () => {
|
||||
if (input.checked) {
|
||||
selectedElementIndex.setData(i1)
|
||||
}
|
||||
}
|
||||
|
||||
inputs.push(input)
|
||||
|
||||
const label = document.createElement("label")
|
||||
label.appendChild(labelHtml)
|
||||
label.htmlFor = input.id
|
||||
label.classList.add("flex", "w-full", "cursor-pointer")
|
||||
|
||||
if (!this._dontStyle) {
|
||||
labelHtml.classList.add("p-2")
|
||||
}
|
||||
|
||||
const block = document.createElement("div")
|
||||
block.appendChild(input)
|
||||
block.appendChild(label)
|
||||
block.classList.add("flex", "w-full")
|
||||
if (!this._dontStyle) {
|
||||
block.classList.add("m-1", "border", "border-gray-400")
|
||||
}
|
||||
block.style.borderRadius = "1.5rem"
|
||||
wrappers.push(block)
|
||||
|
||||
form.appendChild(block)
|
||||
}
|
||||
|
||||
value.addCallbackAndRun((selected: T) => {
|
||||
let somethingChecked = false
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
let input = inputs[i]
|
||||
input.checked = !somethingChecked && elements[i].IsValid(selected)
|
||||
somethingChecked = somethingChecked || input.checked
|
||||
|
||||
if (input.checked) {
|
||||
wrappers[i].classList.remove("border-gray-400")
|
||||
wrappers[i].classList.add("border-attention")
|
||||
} else {
|
||||
wrappers[i].classList.add("border-gray-400")
|
||||
wrappers[i].classList.remove("border-attention")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
|
||||
return form
|
||||
}
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import nmd from "nano-markdown"
|
||||
import FromHtml from "../../Base/FromHtml.svelte"
|
||||
import Markdown from "../../Base/Markdown.svelte"
|
||||
export let value: UIEventSource<undefined | string>
|
||||
export let args: string[] = []
|
||||
let uploadableOnly: boolean = args[0] === "uploadableOnly"
|
||||
|
@ -34,6 +34,6 @@
|
|||
{#if $dropdownFocussed}
|
||||
<div class="m-2 border border-dashed border-black p-2">
|
||||
<b>{documentation.name}</b>
|
||||
<FromHtml src={nmd(documentation.docs)} />
|
||||
<Markdown src={documentation.docs} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -91,11 +91,6 @@
|
|||
return
|
||||
}
|
||||
|
||||
if (unit !== undefined && isNaN(Number(v))) {
|
||||
value.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
feedback?.setData(undefined)
|
||||
if (selectedUnit.data) {
|
||||
value.setData(unit.toOsm(v, selectedUnit.data))
|
||||
|
|
|
@ -28,6 +28,7 @@ import TagValidator from "./Validators/TagValidator"
|
|||
import IdValidator from "./Validators/IdValidator"
|
||||
import SlopeValidator from "./Validators/SlopeValidator"
|
||||
import VeloparkValidator from "./Validators/VeloparkValidator"
|
||||
import CurrencyValidator from "./Validators/CurrencyValidator"
|
||||
|
||||
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
||||
|
||||
|
@ -60,6 +61,7 @@ export default class Validators {
|
|||
"id",
|
||||
"slope",
|
||||
"velopark",
|
||||
"currency"
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
@ -89,6 +91,7 @@ export default class Validators {
|
|||
new IdValidator(),
|
||||
new SlopeValidator(),
|
||||
new VeloparkValidator(),
|
||||
new CurrencyValidator()
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
|
73
src/UI/InputElement/Validators/CurrencyValidator.ts
Normal file
73
src/UI/InputElement/Validators/CurrencyValidator.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
||||
export default class CurrencyValidator extends Validator {
|
||||
private readonly segmenter: Intl.Segmenter
|
||||
private readonly symbolToCurrencyMapping: Map<string, string>
|
||||
private readonly supportedCurrencies: Set<string>
|
||||
|
||||
constructor() {
|
||||
super("currency", "Validates monetary amounts")
|
||||
if (Intl.Segmenter === undefined || Utils.runningFromConsole) {
|
||||
// Librewolf doesn't support this
|
||||
return
|
||||
}
|
||||
let locale = "en-US"
|
||||
if(!Utils.runningFromConsole){
|
||||
locale??= navigator.language
|
||||
}
|
||||
this.segmenter = new Intl.Segmenter(locale, {
|
||||
granularity: "word"
|
||||
})
|
||||
|
||||
const mapping: Map<string, string> = new Map<string, string>()
|
||||
const supportedCurrencies: Set<string> = new Set(Intl.supportedValuesOf("currency"))
|
||||
this.supportedCurrencies = supportedCurrencies
|
||||
for (const currency of supportedCurrencies) {
|
||||
const symbol = (0).toLocaleString(
|
||||
locale,
|
||||
{
|
||||
style: "currency",
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}
|
||||
).replace(/\d/g, "").trim()
|
||||
|
||||
mapping.set(symbol.toLowerCase(), currency)
|
||||
}
|
||||
this.symbolToCurrencyMapping = mapping
|
||||
}
|
||||
|
||||
reformat(s: string): string {
|
||||
if (!this.segmenter) {
|
||||
return s
|
||||
}
|
||||
|
||||
const parts = Array.from(this.segmenter.segment(s)).map(i => i.segment).filter(part => part.trim().length > 0)
|
||||
if(parts.length !== 2){
|
||||
return s
|
||||
}
|
||||
const mapping = this.symbolToCurrencyMapping
|
||||
let currency: string = undefined
|
||||
let amount = undefined
|
||||
for (const part of parts) {
|
||||
const lc = part.toLowerCase()
|
||||
if (this.supportedCurrencies.has(part.toUpperCase())) {
|
||||
currency = part.toUpperCase()
|
||||
continue
|
||||
}
|
||||
|
||||
if (mapping.has(lc)) {
|
||||
currency = mapping.get(lc)
|
||||
continue
|
||||
}
|
||||
amount = part
|
||||
}
|
||||
if(amount === undefined || currency === undefined){
|
||||
return s
|
||||
}
|
||||
|
||||
return amount+" "+currency
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import UrlValidator from "./UrlValidator"
|
|||
|
||||
export default class VeloparkValidator extends UrlValidator {
|
||||
constructor() {
|
||||
super("velopark", "A custom element to allow copy-pasting velopark-pages")
|
||||
super("velopark", "A special URL-validator that checks the domain name and rewrites to the correct velopark format.")
|
||||
}
|
||||
|
||||
getFeedback(s: string): Translation {
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
import { LinkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Square_rounded from "../../assets/svg/Square_rounded.svelte"
|
||||
import Bug from "../../assets/svg/Bug.svelte"
|
||||
import Pop_out from "../../assets/svg/Pop_out.svelte"
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
|
@ -123,6 +124,9 @@
|
|||
<AddSmall {color} class={clss} />
|
||||
{:else if icon === "link"}
|
||||
<LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
|
||||
{:else if icon === "popout"}
|
||||
<LinkIcon style="--svg-color: {color}" />
|
||||
|
||||
{:else}
|
||||
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
|
||||
{/if}
|
||||
|
|
|
@ -38,11 +38,13 @@
|
|||
{#if !allCalculatedTags.has(key)}
|
||||
<tr>
|
||||
<td>{key}</td>
|
||||
<td>
|
||||
<td style="width: 75%">
|
||||
{#if $tags[key] === undefined}
|
||||
<i>undefined</i>
|
||||
{:else if $tags[key] === ""}
|
||||
<i>Empty string</i>
|
||||
{:else if typeof $tags[key] === "object"}
|
||||
<div class="literal-code" >{JSON.stringify($tags[key])}</div>
|
||||
{:else}
|
||||
{$tags[key]}
|
||||
{/if}
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
import { placeholder } from "../../../Utils/placeholder"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { Tag } from "../../../Logic/Tags/Tag"
|
||||
import { get, writable } from "svelte/store"
|
||||
import { get } from "svelte/store"
|
||||
import Markdown from "../../Base/Markdown.svelte"
|
||||
|
||||
export let config: TagRenderingConfig
|
||||
export let tags: UIEventSource<Record<string, string>>
|
||||
|
@ -68,13 +69,17 @@
|
|||
/**
|
||||
* Prepares and fills the checkedMappings
|
||||
*/
|
||||
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
|
||||
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig): void {
|
||||
mappings = confg.mappings?.filter((m) => {
|
||||
if (typeof m.hideInAnswer === "boolean") {
|
||||
return !m.hideInAnswer
|
||||
}
|
||||
return !m.hideInAnswer.matchesProperties(tgs)
|
||||
})
|
||||
selectedMapping = mappings?.findIndex(mapping => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs))
|
||||
if(selectedMapping < 0){
|
||||
selectedMapping = undefined
|
||||
}
|
||||
// We received a new config -> reinit
|
||||
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
|
||||
|
||||
|
@ -85,7 +90,7 @@
|
|||
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
|
||||
) {
|
||||
const seenFreeforms = []
|
||||
// Initial setup of the mappings
|
||||
// Initial setup of the mappings; detect checked mappings
|
||||
checkedMappings = [
|
||||
...confg.mappings.map((mapping) => {
|
||||
if (mapping.hideInAnswer === true) {
|
||||
|
@ -97,7 +102,7 @@
|
|||
seenFreeforms.push(newProps[confg.freeform.key])
|
||||
}
|
||||
return matches
|
||||
}),
|
||||
})
|
||||
]
|
||||
|
||||
if (tgs !== undefined && confg.freeform) {
|
||||
|
@ -128,6 +133,8 @@
|
|||
freeformInput.set(undefined)
|
||||
}
|
||||
feedback.setData(undefined)
|
||||
|
||||
|
||||
}
|
||||
|
||||
$: {
|
||||
|
@ -171,6 +178,9 @@
|
|||
checkedMappings,
|
||||
tags.data
|
||||
)
|
||||
if(state.featureSwitches.featureSwitchIsDebugging.data){
|
||||
console.log("Constructing change spec from", {freeform: $freeformInput, selectedMapping, checkedMappings, currentTags: tags.data}, " --> ", selectedTags)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not calculate changeSpecification:", e)
|
||||
selectedTags = undefined
|
||||
|
@ -203,7 +213,7 @@
|
|||
dispatch("saved", { config, applied: selectedTags })
|
||||
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
|
||||
theme: tags.data["_orig_theme"] ?? state.layout.id,
|
||||
changeType: "answer",
|
||||
changeType: "answer"
|
||||
})
|
||||
freeformInput.set(undefined)
|
||||
selectedMapping = undefined
|
||||
|
@ -255,15 +265,19 @@
|
|||
</div>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="max-h-60 overflow-y-auto">
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{#if config.questionHintIsMd}
|
||||
<Markdown srcWritable={ config.questionhint.current} />
|
||||
{:else}
|
||||
<div class="max-h-60 overflow-y-auto">
|
||||
<SpecialTranslation
|
||||
t={config.questionhint}
|
||||
{tags}
|
||||
{state}
|
||||
{layer}
|
||||
feature={selectedElement}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</legend>
|
||||
|
||||
|
|
|
@ -64,10 +64,14 @@
|
|||
)
|
||||
</script>
|
||||
|
||||
{#if unit.inverted}
|
||||
<div class="bold px-2">/</div>
|
||||
{/if}
|
||||
|
||||
<select bind:value={$selectedUnit}>
|
||||
{#each unit.denominations as denom (denom.canonical)}
|
||||
<option value={denom.canonical}>
|
||||
{#if $isSingle}
|
||||
{#if $isSingle || unit.inverted}
|
||||
<Tr t={denom.humanSingular} />
|
||||
{:else}
|
||||
<Tr t={denom.human.Subs({ quantity: "" })} />
|
||||
|
|
|
@ -20,12 +20,16 @@
|
|||
import NextButton from "../Base/NextButton.svelte"
|
||||
import BackButton from "../Base/BackButton.svelte"
|
||||
import DeleteButton from "./DeleteButton.svelte"
|
||||
import StudioHashSetter from "./StudioHashSetter"
|
||||
|
||||
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
|
||||
|
||||
export let state: EditLayerState
|
||||
|
||||
export let backToStudio: () => void
|
||||
|
||||
new StudioHashSetter("layer", state.selectedTab, state.getStoreFor(["id"]))
|
||||
|
||||
let messages = state.messages
|
||||
let hasErrors = messages.mapD(
|
||||
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
|
||||
|
@ -127,14 +131,14 @@
|
|||
{/each}
|
||||
{:else}
|
||||
<div class="m4 h-full overflow-y-auto">
|
||||
<TabbedGroup>
|
||||
<TabbedGroup tab={state.selectedTab}>
|
||||
<div slot="title0" class="flex">
|
||||
General properties
|
||||
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
|
||||
</div>
|
||||
<div class="flex flex-col mb-8" slot="content0">
|
||||
<Region {state} configs={perRegion["Basic"]} />
|
||||
<DeleteButton {state} {backToStudio} objectType="layer"/>
|
||||
<DeleteButton {state} {backToStudio} objectType="layer" />
|
||||
</div>
|
||||
|
||||
<div slot="title1" class="flex">
|
||||
|
@ -185,19 +189,18 @@
|
|||
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
|
||||
debugging purposes, but you can also edit the file directly if you want.
|
||||
</div>
|
||||
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
|
||||
<RawEditor {state} />
|
||||
</div>
|
||||
<ShowConversionMessages messages={$messages} />
|
||||
<div class="flex h-full w-full flex-row justify-between overflow-y-auto">
|
||||
<div class="literal-code h-full w-5/6 overflow-y-auto">
|
||||
<RawEditor {state} />
|
||||
</div>
|
||||
<div class="h-full w-1/6">
|
||||
<div>
|
||||
The testobject (which is used to render the questions in the 'information panel'
|
||||
item has the following tags:
|
||||
</div>
|
||||
|
||||
<AllTagsPanel tags={state.testTags} />
|
||||
<div class="flex w-full flex-col">
|
||||
<div>
|
||||
The testobject (which is used to render the questions in the 'information panel'
|
||||
item has the following tags:
|
||||
</div>
|
||||
|
||||
<AllTagsPanel tags={state.testTags} />
|
||||
</div>
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
|
|
|
@ -42,6 +42,11 @@ export abstract class EditJsonState<T> {
|
|||
public readonly configuration: UIEventSource<Partial<T>> = new UIEventSource<Partial<T>>({})
|
||||
public readonly messages: Store<ConversionMessage[]>
|
||||
|
||||
/**
|
||||
* The tab in the UI that is selected, used for deeplinks
|
||||
*/
|
||||
public readonly selectedTab: UIEventSource<number> = new UIEventSource<number>(0)
|
||||
|
||||
/**
|
||||
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
|
||||
*/
|
||||
|
@ -508,6 +513,9 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
|
|||
}
|
||||
const prepare = this.buildValidation(state)
|
||||
const context = ConversionContext.construct([], ["prepare"])
|
||||
if(configuration.layers){
|
||||
Utils.NoNullInplace(configuration.layers)
|
||||
}
|
||||
try {
|
||||
prepare.convert(<LayoutConfigJson>configuration, context)
|
||||
} catch (e) {
|
||||
|
|
|
@ -9,12 +9,15 @@
|
|||
import RawEditor from "./RawEditor.svelte"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import DeleteButton from "./DeleteButton.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import StudioHashSetter from "./StudioHashSetter"
|
||||
|
||||
export let state: EditThemeState
|
||||
export let osmConnection: OsmConnection
|
||||
export let backToStudio: () => void
|
||||
|
||||
let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
|
||||
new StudioHashSetter("theme", state.selectedTab, state.getStoreFor(["id"]))
|
||||
|
||||
export let selfLayers: { owner: number; id: string }[]
|
||||
export let otherLayers: { owner: number; id: string }[]
|
||||
|
@ -94,7 +97,7 @@
|
|||
|
||||
<div class="m4 h-full overflow-y-auto">
|
||||
<!-- {Object.keys(perRegion).join(";")} -->
|
||||
<TabbedGroup>
|
||||
<TabbedGroup tab={state.selectedTab}>
|
||||
<div slot="title0">Basic properties</div>
|
||||
<div slot="content0" class="mb-8">
|
||||
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" />
|
||||
|
@ -123,11 +126,10 @@
|
|||
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
|
||||
debugging purposes, but you can also edit the file directly if you want.
|
||||
</div>
|
||||
<ShowConversionMessages messages={$messages} />
|
||||
<div class="literal-code h-full w-full">
|
||||
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
|
||||
<RawEditor {state} />
|
||||
</div>
|
||||
</div>
|
||||
<ShowConversionMessages messages={$messages} />
|
||||
</TabbedGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import nmd from "nano-markdown"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
|
||||
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
|
||||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Markdown from "../Base/Markdown.svelte"
|
||||
|
||||
export let state: EditLayerState
|
||||
export let path: ReadonlyArray<string | number>
|
||||
|
@ -62,13 +62,6 @@
|
|||
let messages = state.messagesFor(path)
|
||||
|
||||
let description = schema.description
|
||||
if (description) {
|
||||
try {
|
||||
description = nmd(description)
|
||||
} catch (e) {
|
||||
console.error("Could not convert description to markdown", { description })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex">
|
||||
|
@ -82,7 +75,7 @@
|
|||
{/if}
|
||||
</NextButton>
|
||||
{#if description}
|
||||
<FromHtml src={description} />
|
||||
<Markdown src={description}/>
|
||||
{/if}
|
||||
{#each $messages as message}
|
||||
<ShowConversionMessage {message} />
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import QuestionPreview from "./QuestionPreview.svelte"
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
import Markdown from "../Base/Markdown.svelte"
|
||||
|
||||
export let state: EditLayerState
|
||||
export let schema: ConfigMeta
|
||||
|
@ -30,10 +31,9 @@
|
|||
schema.description = undefined
|
||||
}
|
||||
|
||||
const subparts: ConfigMeta = state
|
||||
const subparts: ConfigMeta[] = state
|
||||
.getSchemaStartingWith(schema.path)
|
||||
.filter((part) => part.path.length - 1 === schema.path.length)
|
||||
|
||||
let messages = state.messagesFor(path)
|
||||
|
||||
const currentValue: UIEventSource<any[]> = state.getStoreFor(path)
|
||||
|
@ -97,9 +97,7 @@
|
|||
<h3>{schema.path.at(-1)}</h3>
|
||||
|
||||
{#if subparts.length > 0}
|
||||
<span class="subtle">
|
||||
{schema.description}
|
||||
</span>
|
||||
<Markdown src={schema.description}/>
|
||||
{/if}
|
||||
{#if $currentValue === undefined}
|
||||
No array defined
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import type { ConfigMeta } from "./configMeta"
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import nmd from "nano-markdown"
|
||||
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
|
||||
import EditLayerState from "./EditLayerState"
|
||||
import { onDestroy } from "svelte"
|
||||
|
@ -68,11 +67,12 @@
|
|||
type = type.substring(0, type.length - 2)
|
||||
}
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean} = {
|
||||
id: path.join("_"),
|
||||
render: rendervalue,
|
||||
question: schema.hints.question,
|
||||
questionHint: nmd(schema.description),
|
||||
questionHint: schema.description,
|
||||
questionHintIsMd: true,
|
||||
freeform:
|
||||
schema.type === "boolean"
|
||||
? undefined
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
import { onDestroy } from "svelte"
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte"
|
||||
import type { JsonSchemaType } from "./jsonSchema"
|
||||
// @ts-ignore
|
||||
import nmd from "nano-markdown"
|
||||
import ShowConversionMessage from "./ShowConversionMessage.svelte"
|
||||
import type { Translatable } from "../../Models/ThemeConfig/Json/Translatable"
|
||||
|
||||
/**
|
||||
* If 'types' is defined: allow the user to pick one of the types to input.
|
||||
|
@ -41,10 +40,11 @@
|
|||
if (lastIsString) {
|
||||
types.splice(types.length - 1, 1)
|
||||
}
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean}= {
|
||||
id: "TYPE_OF:" + path.join("_"),
|
||||
question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
|
||||
questionHint: nmd(schema.description),
|
||||
question: schema.hints.question ?? "Which subcategory is needed for " + schema.path.at(-1) + "?",
|
||||
questionHint: schema.description,
|
||||
questionHintIsMd: true,
|
||||
mappings: types
|
||||
.map((opt) => opt.trim())
|
||||
.filter((opt) => opt.length > 0)
|
||||
|
|
11
src/UI/Studio/StudioHashSetter.ts
Normal file
11
src/UI/Studio/StudioHashSetter.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
|
||||
export default class StudioHashSetter {
|
||||
constructor(mode: "layer" | "theme", tab: Store<number>, name: Store<string>) {
|
||||
tab.mapD(tab => {
|
||||
Hash.hash.setData(mode + "/" + name.data + "/" + tab)
|
||||
}
|
||||
, [name])
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import NextButton from "./Base/NextButton.svelte"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import EditLayerState, { EditThemeState } from "./Studio/EditLayerState"
|
||||
import EditLayerState, { EditJsonState, EditThemeState } from "./Studio/EditLayerState"
|
||||
import EditLayer from "./Studio/EditLayer.svelte"
|
||||
import Loading from "../assets/svg/Loading.svelte"
|
||||
import StudioServer from "./Studio/StudioServer"
|
||||
|
@ -30,6 +30,7 @@
|
|||
import Tr from "./Base/Tr.svelte"
|
||||
import Add from "../assets/svg/Add.svelte"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Hash from "../Logic/Web/Hash"
|
||||
|
||||
export let studioUrl =
|
||||
window.location.hostname === "127.0.0.2"
|
||||
|
@ -43,11 +44,11 @@
|
|||
)
|
||||
let osmConnection = new OsmConnection({
|
||||
oauth_token,
|
||||
checkOnlineRegularly: true,
|
||||
checkOnlineRegularly: true
|
||||
})
|
||||
const expertMode = UIEventSource.asBoolean(
|
||||
osmConnection.GetPreference("studio-expert-mode", "false", {
|
||||
documentation: "Indicates if more options are shown in mapcomplete studio",
|
||||
documentation: "Indicates if more options are shown in mapcomplete studio"
|
||||
})
|
||||
)
|
||||
expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert))
|
||||
|
@ -61,12 +62,12 @@
|
|||
l["success"]?.filter((l) => l.category === "layers")
|
||||
)
|
||||
$: selfLayers = layers.mapD(
|
||||
(ls) =>
|
||||
ls.filter(
|
||||
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
|
||||
),
|
||||
[uid]
|
||||
)
|
||||
(ls) =>
|
||||
ls.filter(
|
||||
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
|
||||
),
|
||||
[uid]
|
||||
)
|
||||
$: otherLayers = layers.mapD(
|
||||
(ls) =>
|
||||
ls.filter(
|
||||
|
@ -132,16 +133,17 @@
|
|||
|
||||
const version = meta.version
|
||||
|
||||
async function editLayer(event: Event) {
|
||||
async function editLayer(event: { detail }): Promise<EditLayerState> {
|
||||
const layerId: { owner: number; id: string } = event["detail"]
|
||||
state = "loading"
|
||||
editLayerState.startSavingUpdates(false)
|
||||
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner))
|
||||
editLayerState.startSavingUpdates()
|
||||
state = "editing_layer"
|
||||
return editLayerState
|
||||
}
|
||||
|
||||
async function editTheme(event: Event) {
|
||||
async function editTheme(event: { detail }): Promise<EditThemeState> {
|
||||
const id: { id: string; owner: number } = event["detail"]
|
||||
state = "loading"
|
||||
editThemeState.startSavingUpdates(false)
|
||||
|
@ -149,6 +151,7 @@
|
|||
editThemeState.configuration.setData(layout)
|
||||
editThemeState.startSavingUpdates()
|
||||
state = "editing_theme"
|
||||
return editThemeState
|
||||
}
|
||||
|
||||
async function createNewLayer() {
|
||||
|
@ -162,23 +165,50 @@
|
|||
marker: [
|
||||
{
|
||||
icon: "circle",
|
||||
color: "white",
|
||||
},
|
||||
],
|
||||
},
|
||||
color: "white"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
tagRenderings: ["images"],
|
||||
lineRendering: [
|
||||
{
|
||||
width: 1,
|
||||
color: "blue",
|
||||
},
|
||||
],
|
||||
color: "blue"
|
||||
}
|
||||
]
|
||||
}
|
||||
editLayerState.configuration.setData(initialLayerConfig)
|
||||
editLayerState.startSavingUpdates()
|
||||
state = "editing_layer"
|
||||
}
|
||||
|
||||
async function selectStateBasedOnHash() {
|
||||
const hash = Hash.hash.data
|
||||
if (!hash) {
|
||||
return
|
||||
}
|
||||
console.log("Selecting state based on ", hash)
|
||||
const [mode, id, tab] = hash.split("/")
|
||||
// Not really an event, we just set the 'detail'
|
||||
const event = {
|
||||
detail: {
|
||||
id,
|
||||
owner: uid.data
|
||||
}
|
||||
}
|
||||
const statePromise: Promise<EditJsonState<any>> = mode === "layer" ? editLayer(event) : editTheme(event)
|
||||
const state = await statePromise
|
||||
state.selectedTab.setData(Number(tab))
|
||||
}
|
||||
|
||||
selectStateBasedOnHash()
|
||||
|
||||
function backToStudio() {
|
||||
console.log("Back to studio")
|
||||
state = undefined
|
||||
Hash.hash.setData(undefined)
|
||||
}
|
||||
</script>
|
||||
|
||||
<If condition={layersWithErr.map((d) => d?.["error"] !== undefined)}>
|
||||
|
@ -191,8 +221,8 @@
|
|||
<li>Try again in a few minutes</li>
|
||||
<li>
|
||||
Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">
|
||||
the MapComplete community via the chat.
|
||||
</a>
|
||||
the MapComplete community via the chat.
|
||||
</a>
|
||||
Someone might be able to help you
|
||||
</li>
|
||||
<li>
|
||||
|
@ -257,9 +287,7 @@
|
|||
<BackButton
|
||||
clss="small p-1"
|
||||
imageClass="w-8 h-8"
|
||||
on:click={() => {
|
||||
state = undefined
|
||||
}}
|
||||
on:click={() => backToStudio()}
|
||||
>
|
||||
MapComplete Studio
|
||||
</BackButton>
|
||||
|
@ -306,9 +334,7 @@
|
|||
<BackButton
|
||||
clss="small p-1"
|
||||
imageClass="w-8 h-8"
|
||||
on:click={() => {
|
||||
state = undefined
|
||||
}}
|
||||
on:click={() => backToStudio()}
|
||||
>
|
||||
MapComplete Studio
|
||||
</BackButton>
|
||||
|
@ -348,30 +374,23 @@
|
|||
{:else if state === "editing_layer"}
|
||||
<EditLayer
|
||||
state={editLayerState}
|
||||
backToStudio={() => {
|
||||
state = undefined
|
||||
}}
|
||||
{backToStudio}
|
||||
>
|
||||
<BackButton
|
||||
clss="small p-1"
|
||||
imageClass="w-8 h-8"
|
||||
on:click={() => {
|
||||
state = undefined
|
||||
}}
|
||||
on:click={() => backToStudio()}
|
||||
>
|
||||
MapComplete Studio
|
||||
</BackButton>
|
||||
</EditLayer>
|
||||
{:else if state === "editing_theme"}
|
||||
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection} backToStudio={() => {
|
||||
state = undefined
|
||||
}}>
|
||||
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection}
|
||||
{backToStudio}>
|
||||
<BackButton
|
||||
clss="small p-1"
|
||||
imageClass="w-8 h-8"
|
||||
on:click={() => {
|
||||
state = undefined
|
||||
}}
|
||||
on:click={() => backToStudio()}
|
||||
>
|
||||
MapComplete Studio
|
||||
</BackButton>
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
import { Utils } from "../Utils"
|
||||
import Hotkeys from "./Base/Hotkeys"
|
||||
import LevelSelector from "./BigComponents/LevelSelector.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
|
||||
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
|
||||
import type { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
|
@ -73,6 +72,7 @@
|
|||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import ReviewsOverview from "./Reviews/ReviewsOverview.svelte"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
@ -260,9 +260,7 @@
|
|||
/>
|
||||
</MapControlButton>
|
||||
{/if}
|
||||
<ToSvelte
|
||||
construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")}
|
||||
/>
|
||||
<ExtraLinkButton {state} />
|
||||
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
|
||||
<PendingChangesIndicator {state} />
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
|
@ -285,9 +283,9 @@
|
|||
<div class="flex w-full items-end justify-between px-4">
|
||||
<div class="flex flex-col">
|
||||
<If condition={featureSwitches.featureSwitchEnableLogin}>
|
||||
{#if state.layout.hasPresets() || state.layout.hasNoteLayer()}
|
||||
{#if (state.layout.hasPresets() && state.layout.enableAddNewPoints) || state.layout.hasNoteLayer()}
|
||||
<button
|
||||
class="pointer-events-auto w-fit"
|
||||
class="pointer-events-auto w-fit low-interaction"
|
||||
class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint}
|
||||
on:click={() => {
|
||||
state.openNewDialog()
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import nmd from "nano-markdown"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import WalkthroughStep from "./WalkthroughStep.svelte"
|
||||
import FromHtml from "../Base/FromHtml.svelte"
|
||||
import Markdown from "../Base/Markdown.svelte"
|
||||
|
||||
/**
|
||||
* Markdown
|
||||
|
@ -31,5 +31,5 @@
|
|||
totalPages={pages.length}
|
||||
pageNumber={currentPage}
|
||||
>
|
||||
<FromHtml src={nmd(pages[currentPage])} />
|
||||
<Markdown src={pages[currentPage]} />
|
||||
</WalkthroughStep>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
</script>
|
||||
|
||||
<div class="link-underline flex h-full w-full flex-col justify-between">
|
||||
<div class="overflow-y-auto">
|
||||
<div class="overflow-y-auto h-full">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
|
22
src/Utils.ts
22
src/Utils.ts
|
@ -263,6 +263,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
return res
|
||||
}
|
||||
|
||||
public static NoNull<T>(array: T[] | undefined): (T[] | undefined)
|
||||
public static NoNull<T>(array: undefined): undefined
|
||||
public static NoNull<T>(array: T[]): T[]
|
||||
public static NoNull<T>(array: T[]): NonNullable<T>[] {
|
||||
return <any>array?.filter((o) => o !== undefined && o !== null)
|
||||
}
|
||||
|
@ -507,7 +510,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
let result = ""
|
||||
while (match) {
|
||||
const [_, normal, key, leftover] = match
|
||||
let v = tags === undefined ? undefined : tags[key]
|
||||
let v = tags?.[key]
|
||||
if (v !== undefined && v !== null) {
|
||||
if (v["toISOString"] != undefined) {
|
||||
// This is a date, probably the timestamp of the object
|
||||
|
@ -1039,7 +1042,14 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
Utils._download_cache.set(url, { promise, timestamp: new Date().getTime() })
|
||||
return await promise
|
||||
}
|
||||
|
||||
public static async downloadJson(
|
||||
url: string,
|
||||
headers?: Record<string, string>
|
||||
): Promise<object | []>
|
||||
public static async downloadJson<T>(
|
||||
url: string,
|
||||
headers?: Record<string, string>
|
||||
): Promise<T>
|
||||
public static async downloadJson(
|
||||
url: string,
|
||||
headers?: Record<string, string>
|
||||
|
@ -1649,4 +1659,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
return n + Utils._metrixPrefixes[index]
|
||||
}
|
||||
|
||||
static NoNullInplace(layers: any[]):void {
|
||||
for (let i = layers.length - 1; i >= 0; i--) {
|
||||
if(layers[i] === null || layers[i] === undefined){
|
||||
layers.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -9965,7 +9965,7 @@
|
|||
"group": "presets"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
|
||||
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -10275,6 +10275,10 @@
|
|||
"if": "value=food",
|
||||
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
|
||||
},
|
||||
{
|
||||
"if": "value=food_courts",
|
||||
"then": "food_courts - Food courts with a variety of food options."
|
||||
},
|
||||
{
|
||||
"if": "value=ghost_bike",
|
||||
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
|
||||
|
@ -10407,6 +10411,10 @@
|
|||
"if": "value=osm_community_index",
|
||||
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
|
||||
},
|
||||
{
|
||||
"if": "value=outdoor_seating",
|
||||
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
|
||||
},
|
||||
{
|
||||
"if": "value=parcel_lockers",
|
||||
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
|
||||
|
@ -11646,7 +11654,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -12918,7 +12930,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -14223,7 +14239,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -15544,7 +15564,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -16864,7 +16888,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -18185,7 +18213,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -556,6 +556,10 @@
|
|||
"if": "value=food",
|
||||
"then": "<b>food</b> (builtin) - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
|
||||
},
|
||||
{
|
||||
"if": "value=food_courts",
|
||||
"then": "<b>food_courts</b> (builtin) - Food courts with a variety of food options."
|
||||
},
|
||||
{
|
||||
"if": "value=ghost_bike",
|
||||
"then": "<b>ghost_bike</b> (builtin) - A layer showing memorials for cyclists, killed in road accidents"
|
||||
|
@ -688,6 +692,10 @@
|
|||
"if": "value=osm_community_index",
|
||||
"then": "<b>osm_community_index</b> (builtin) - A layer showing the OpenStreetMap Communities"
|
||||
},
|
||||
{
|
||||
"if": "value=outdoor_seating",
|
||||
"then": "<b>outdoor_seating</b> (builtin) - Outdoor seating areas, usually located near cafes and restaurants."
|
||||
},
|
||||
{
|
||||
"if": "value=parcel_lockers",
|
||||
"then": "<b>parcel_lockers</b> (builtin) - Layer showing parcel lockers for collecting and sending parcels."
|
||||
|
@ -1315,7 +1323,7 @@
|
|||
"type": "boolean"
|
||||
},
|
||||
"presets": {
|
||||
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\n\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\n\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\n\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all\n\ngroup: presets",
|
||||
"description": "<div class='flex'>\n <div>\nPresets for this layer.\n\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\n\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\n\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>\n\ngroup: presets",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
@ -1908,7 +1916,7 @@
|
|||
"type": "string"
|
||||
}
|
||||
],
|
||||
"description": "Every layer contains a description of which feature to display - the overpassTags which are queried.\nInstead of running one query for every layer, the query is fused.\nAfterwards, every layer is given the list of features.\nEvery layer takes away the features that match with them*, and give the leftovers to the next layers.\nThis implies that the _order_ of the layers is important in the case of features with the same tags;\nas the later layers might never receive their feature.\n*layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```"
|
||||
"description": "A theme must contain at least one layer.\nA layer contains all features of a single type, for example \"shops\", \"bicycle pumps\", \"benches\".\nNote that every layer contains a specification of attributes that it should match. MapComplete will fetch the relevant data from either overpass, the OSM-API or the cache server.\nIf a feature can match multiple layers, the first matching layer in the list will be used.\nThis implies that the _order_ of the layers is important.\n<div class='hidden-in-studio'>\nNote that builtin layers can be reused. Either put in the name of the layer to reuse, or use {builtin: \"layername\", override: ...}\nThe 'override'-object will be copied over the original values of the layer, which allows to change certain aspects of the layer\nFor example: If you would like to use layer nature reserves, but only from a specific operator (eg. Natuurpunt) you would use the following in your theme:\n```\n\"layer\": {\n \"builtin\": \"nature_reserve\",\n \"override\": {\"source\":\n {\"osmTags\": {\n \"+and\":[\"operator=Natuurpunt\"]\n }\n }\n }\n}\n```\nIt's also possible to load multiple layers at once, for example, if you would like for both drinking water and benches to start at the zoomlevel at 12, you would use the following:\n```\n\"layer\": {\n \"builtin\": [\"benches\", \"drinking_water\"],\n \"override\": {\"minzoom\": 12}\n}\n```\n</div>"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -12195,7 +12203,7 @@
|
|||
"group": "presets"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
|
||||
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -12510,6 +12518,10 @@
|
|||
"if": "value=food",
|
||||
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
|
||||
},
|
||||
{
|
||||
"if": "value=food_courts",
|
||||
"then": "food_courts - Food courts with a variety of food options."
|
||||
},
|
||||
{
|
||||
"if": "value=ghost_bike",
|
||||
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
|
||||
|
@ -12642,6 +12654,10 @@
|
|||
"if": "value=osm_community_index",
|
||||
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
|
||||
},
|
||||
{
|
||||
"if": "value=outdoor_seating",
|
||||
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
|
||||
},
|
||||
{
|
||||
"if": "value=parcel_lockers",
|
||||
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
|
||||
|
@ -13908,7 +13924,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -15229,7 +15249,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -16584,7 +16608,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -17954,7 +17982,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -19323,7 +19355,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -20693,7 +20729,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -32813,7 +32853,7 @@
|
|||
"group": "presets"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Presets for this layer.\nA preset shows up when clicking the map on a without data (or when right-clicking/long-pressing);\nit will prompt the user to add a new point.\nThe most important aspect are the tags, which define which tags the new point will have;\nThe title is shown in the dialog, along with the first sentence of the description.\nUpon confirmation, the full description is shown beneath the buttons - perfect to add pictures and examples.\nNote: the icon of the preset is determined automatically based on the tags and the icon above. Don't worry about that!\nNB: if no presets are defined, the popup to add new points doesn't show up at all"
|
||||
"description": "<div class='flex'>\n <div>\nPresets for this layer.\nA preset consists of one or more attributes (tags), a title and optionally a description and optionally example images.\nWhen the contributor wishes to add a point to OpenStreetMap, they'll:\n1. Press the 'add new point'-button\n2. Choose a preset from the list of all presets\n3. Confirm the choice. In this step, the `description` (if set) and `exampleImages` (if given) will be shown\n4. Confirm the location\n5. A new point will be created with the attributes that were defined in the preset\nIf no presets are defined, the button which invites to add a new preset will not be shown.\n</div>\n<a class='block m-2 min-w-64' href='./Docs/Screenshots/AddNewItemScreencast.webm' target='_blank'> <video controls autoplay muted src='./Docs/Screenshots/AddNewItemScreencast.webm' class='w-64'/></a>\n</div>"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
|
@ -33133,6 +33173,10 @@
|
|||
"if": "value=food",
|
||||
"then": "food - A layer showing restaurants and fast-food amenities (with a special rendering for friteries)"
|
||||
},
|
||||
{
|
||||
"if": "value=food_courts",
|
||||
"then": "food_courts - Food courts with a variety of food options."
|
||||
},
|
||||
{
|
||||
"if": "value=ghost_bike",
|
||||
"then": "ghost_bike - A layer showing memorials for cyclists, killed in road accidents"
|
||||
|
@ -33265,6 +33309,10 @@
|
|||
"if": "value=osm_community_index",
|
||||
"then": "osm_community_index - A layer showing the OpenStreetMap Communities"
|
||||
},
|
||||
{
|
||||
"if": "value=outdoor_seating",
|
||||
"then": "outdoor_seating - Outdoor seating areas, usually located near cafes and restaurants."
|
||||
},
|
||||
{
|
||||
"if": "value=parcel_lockers",
|
||||
"then": "parcel_lockers - Layer showing parcel lockers for collecting and sending parcels."
|
||||
|
@ -34558,7 +34606,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -35928,7 +35980,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -37333,7 +37389,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -38752,7 +38812,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -40170,7 +40234,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -41589,7 +41657,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -43424,7 +43496,7 @@
|
|||
"group": "feature_switches",
|
||||
"question": "Should the 'download as CSV'- and 'download as Geojson'-buttons be enabled?",
|
||||
"iftrue": "Enable the option to download the map as CSV and GeoJson",
|
||||
"iffalse": "Enable the option to download the map as CSV and GeoJson",
|
||||
"iffalse": "Disable the option to download the map as CSV and GeoJson",
|
||||
"ifunset": "MapComplete default: Enable the option to download the map as CSV and GeoJson"
|
||||
},
|
||||
"type": "boolean",
|
||||
|
|
|
@ -663,7 +663,11 @@
|
|||
},
|
||||
{
|
||||
"if": "value=velopark",
|
||||
"then": "<b>velopark</b> A custom element to allow copy-pasting velopark-pages"
|
||||
"then": "<b>velopark</b> A special URL-validator that checks the domain name and rewrites to the correct velopark format."
|
||||
},
|
||||
{
|
||||
"if": "value=currency",
|
||||
"then": "<b>currency</b> Validates monetary amounts"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
--alert-foreground-color: var(--foreground-color);
|
||||
|
||||
--low-interaction-background: #eeeeee;
|
||||
--low-interaction-background-50: #eeeeee90;
|
||||
--low-interaction-foreground: black;
|
||||
--low-interaction-contrast: #ff00ff;
|
||||
|
||||
|
@ -264,6 +265,10 @@ button.primary:not(.no-image-background) svg path, .button.primary:not(.no-image
|
|||
transition: all 250ms;
|
||||
}
|
||||
|
||||
button.disabled.low-interaction, .button.disabled.low-interaction {
|
||||
background-color: var(--low-interaction-background-50);
|
||||
}
|
||||
|
||||
|
||||
button.disabled, .button.disabled {
|
||||
cursor: default;
|
||||
|
@ -274,6 +279,7 @@ button.disabled, .button.disabled {
|
|||
}
|
||||
|
||||
|
||||
|
||||
button.disabled:hover, .button.disabled:hover {
|
||||
cursor: default;
|
||||
border: 2px dashed var(--button-background);
|
||||
|
@ -494,7 +500,7 @@ textarea {
|
|||
}
|
||||
|
||||
|
||||
.literal-code {
|
||||
.literal-code, code {
|
||||
/* A codeblock */
|
||||
display: inline-block;
|
||||
background-color: lightgray;
|
||||
|
|
|
@ -30,8 +30,8 @@ async function getAvailableLayers(): Promise<Set<string>> {
|
|||
try {
|
||||
const host = new URL(Constants.VectorTileServer).host
|
||||
const status: { layers: string[] } = await Promise.any([
|
||||
// Utils.downloadJson("https://" + host + "/summary/status.json"),
|
||||
timeout(0),
|
||||
Utils.downloadJson<{layers}>("https://" + host + "/summary/status.json"),
|
||||
timeout(2500),
|
||||
])
|
||||
return new Set<string>(status.layers)
|
||||
} catch (e) {
|
||||
|
|
|
@ -29,7 +29,7 @@ async function getAvailableLayers(): Promise<Set<string>> {
|
|||
try {
|
||||
const host = new URL(Constants.VectorTileServer).host
|
||||
const status = await Promise.any([
|
||||
// Utils.downloadJson("https://" + host + "/summary/status.json"),
|
||||
Utils.downloadJson("https://" + host + "/summary/status.json"),
|
||||
timeout(0)
|
||||
])
|
||||
return new Set<string>(status.layers)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue