Merge branch 'develop'

This commit is contained in:
Pieter Vander Vennet 2023-02-10 01:36:27 +01:00
commit 2f45be975e
77 changed files with 668 additions and 486 deletions

2
.gitignore vendored
View file

@ -36,3 +36,5 @@ service-worker.js
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
public/*.webmanifest
public/assets/generated/

View file

@ -1,4 +1,4 @@
import * as known_themes from "../assets/generated/known_layers_and_themes.json" import known_themes from "../assets/generated/known_layers_and_themes.json"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import BaseUIElement from "../UI/BaseUIElement" import BaseUIElement from "../UI/BaseUIElement"

View file

@ -1,5 +1,5 @@
import * as questions from "../assets/tagRenderings/questions.json" import questions from "../assets/tagRenderings/questions.json"
import * as icons from "../assets/tagRenderings/icons.json" import icons from "../assets/tagRenderings/icons.json"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"

View file

@ -45,13 +45,10 @@ export default class GeoLocationHandler {
(new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000 (new Date().getTime() - geolocationState.requestMoment.data?.getTime() ?? 0) / 1000
if (!this.mapHasMoved.data) { if (!this.mapHasMoved.data) {
// The map hasn't moved yet; we received our first coordinates, so let's move there! // The map hasn't moved yet; we received our first coordinates, so let's move there!
console.log( self.MoveMapToCurrentLocation()
"Moving the map to an initial location; time since last request is", }
timeSinceLastRequest if (timeSinceLastRequest < Constants.zoomToLocationTimeout) {
) self.MoveMapToCurrentLocation()
if (timeSinceLastRequest < Constants.zoomToLocationTimeout) {
self.MoveMapToCurrentLocation()
}
} }
if (this.geolocationState.isLocked.data) { if (this.geolocationState.isLocked.data) {
@ -109,11 +106,12 @@ export default class GeoLocationHandler {
} }
mapLocation.setData({ mapLocation.setData({
zoom: mapLocation.data.zoom, zoom: Math.max(mapLocation.data.zoom, 16),
lon: newLocation.longitude, lon: newLocation.longitude,
lat: newLocation.latitude, lat: newLocation.latitude,
}) })
this.mapHasMoved.setData(true) this.mapHasMoved.setData(true)
this.geolocationState.requestMoment.setData(undefined)
} }
private CopyGeolocationIntoMapstate() { private CopyGeolocationIntoMapstate() {

View file

@ -90,12 +90,12 @@ export default class SelectedFeatureHandler {
if (feature === undefined) { if (feature === undefined) {
return return
} }
const currentlySeleced = state.selectedElement.data const currentlySelected = state.selectedElement.data
if (currentlySeleced === undefined) { if (currentlySelected === undefined) {
state.selectedElement.setData(feature) state.selectedElement.setData(feature)
return return
} }
if (currentlySeleced.properties?.id === feature.properties.id) { if (currentlySelected.properties?.id === feature.properties.id) {
// We already have the right feature // We already have the right feature
return return
} }

View file

@ -12,9 +12,9 @@ import LZString from "lz-string"
import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert" import { FixLegacyTheme } from "../Models/ThemeConfig/Conversion/LegacyJsonConvert"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import SharedTagRenderings from "../Customizations/SharedTagRenderings" import SharedTagRenderings from "../Customizations/SharedTagRenderings"
import * as known_layers from "../assets/generated/known_layers.json" import known_layers from "../assets/generated/known_layers.json"
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
import * as licenses from "../assets/generated/license_info.json" import licenses from "../assets/generated/license_info.json"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages" import { FixImages } from "../Models/ThemeConfig/Conversion/FixImages"
import Svg from "../Svg" import Svg from "../Svg"

View file

@ -234,7 +234,6 @@ export default class MetaTagging {
for (const f of functions) { for (const f of functions) {
f(feature) f(feature)
} }
state?.allElements?.getEventSourceById(feature.properties.id)?.ping()
} catch (e) { } catch (e) {
console.error("Invalid syntax in calculated tags or some other error: ", e) console.error("Invalid syntax in calculated tags or some other error: ", e)
} }

View file

@ -1,5 +1,5 @@
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import * as polygon_features from "../../assets/polygon-features.json" import polygon_features from "../../assets/polygon-features.json"
import { Store, UIEventSource } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { BBox } from "../BBox" import { BBox } from "../BBox"
import OsmToGeoJson from "osmtogeojson" import OsmToGeoJson from "osmtogeojson"
@ -290,7 +290,7 @@ export abstract class OsmObject {
{ values: Set<string>; blacklist: boolean } { values: Set<string>; blacklist: boolean }
> { > {
const result = new Map<string, { values: Set<string>; blacklist: boolean }>() const result = new Map<string, { values: Set<string>; blacklist: boolean }>()
for (const polygonFeature of polygon_features["default"] ?? polygon_features) { for (const polygonFeature of polygon_features) {
const key = polygonFeature.key const key = polygonFeature.key
if (polygonFeature.polygon === "all") { if (polygonFeature.polygon === "all") {

View file

@ -33,6 +33,10 @@ export class SimpleMetaTagger {
docs: { docs: {
keys: string[] keys: string[]
doc: string doc: string
/**
* Set this flag if the data is volatile or date-based.
* It'll _won't_ be cached in this case
*/
includesDates?: boolean includesDates?: boolean
isLazy?: boolean isLazy?: boolean
cleanupRetagger?: boolean cleanupRetagger?: boolean
@ -492,6 +496,7 @@ export default class SimpleMetaTaggers {
{ {
keys: ["_referencing_ways"], keys: ["_referencing_ways"],
isLazy: true, isLazy: true,
includesDates: true,
doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ", doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ",
}, },
(feature, _, __, state) => { (feature, _, __, state) => {

View file

@ -1,5 +1,6 @@
import { UIEventSource } from "../UIEventSource" import { UIEventSource } from "../UIEventSource"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
type GeolocationState = "prompt" | "requested" | "granted" | "denied" type GeolocationState = "prompt" | "requested" | "granted" | "denied"
@ -11,8 +12,6 @@ export interface GeoLocationPointProperties extends GeolocationCoordinates {
/** /**
* An abstract representation of the current state of the geolocation. * An abstract representation of the current state of the geolocation.
*
*
*/ */
export class GeoLocationState { export class GeoLocationState {
/** /**
@ -21,10 +20,12 @@ export class GeoLocationState {
* 'requested' means the user tapped the 'locate me' button at least once * 'requested' means the user tapped the 'locate me' button at least once
* 'granted' means that it is granted * 'granted' means that it is granted
* 'denied' means that we don't have access * 'denied' means that we don't have access
*
*/ */
public readonly permission: UIEventSource<GeolocationState> = new UIEventSource("prompt") public readonly permission: UIEventSource<GeolocationState> = new UIEventSource("prompt")
/**
* Important to determine e.g. if we move automatically on fix or not
*/
public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined) public readonly requestMoment: UIEventSource<Date | undefined> = new UIEventSource(undefined)
/** /**
* If true: the map will center (and re-center) to this location * If true: the map will center (and re-center) to this location
@ -79,6 +80,11 @@ export class GeoLocationState {
// We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them // We set the flag to false again. If the user only wanted to share their location once, we are not gonna keep bothering them
this._previousLocationGrant.setData("false") this._previousLocationGrant.setData("false")
console.log("Requesting access to GPS as this was previously granted") console.log("Requesting access to GPS as this was previously granted")
const latLonGivenViaUrl =
QueryParameters.wasInitialized("lat") || QueryParameters.wasInitialized("lon")
if (!latLonGivenViaUrl) {
this.requestMoment.setData(new Date())
}
this.requestPermission() this.requestPermission()
} }
window["geolocation_state"] = this window["geolocation_state"] = this
@ -120,7 +126,7 @@ export class GeoLocationState {
// Hence that we continue the flow if it is "requested" // Hence that we continue the flow if it is "requested"
return return
} }
this.requestMoment.setData(new Date())
this.permission.setData("requested") this.permission.setData("requested")
try { try {
navigator?.permissions navigator?.permissions

View file

@ -1,7 +1,7 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../Osm/OsmConnection" import { OsmConnection } from "../Osm/OsmConnection"
import { MangroveIdentity } from "../Web/MangroveReviews" import { MangroveIdentity } from "../Web/MangroveReviews"
import { Store } from "../UIEventSource" import { Store, UIEventSource } from "../UIEventSource"
import { QueryParameters } from "../Web/QueryParameters" import { QueryParameters } from "../Web/QueryParameters"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import ElementsState from "./ElementsState" import ElementsState from "./ElementsState"
@ -9,7 +9,6 @@ import SelectedElementTagsUpdater from "../Actors/SelectedElementTagsUpdater"
import { Changes } from "../Osm/Changes" import { Changes } from "../Osm/Changes"
import ChangeToElementsActor from "../Actors/ChangeToElementsActor" import ChangeToElementsActor from "../Actors/ChangeToElementsActor"
import PendingChangesUploader from "../Actors/PendingChangesUploader" import PendingChangesUploader from "../Actors/PendingChangesUploader"
import * as translators from "../../assets/translators.json"
import Maproulette from "../Maproulette" import Maproulette from "../Maproulette"
/** /**
@ -53,29 +52,25 @@ export default class UserRelatedState extends ElementsState {
osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data, osmConfiguration: <"osm" | "osm-test">this.featureSwitchApiURL.data,
attemptLogin: options?.attemptLogin, attemptLogin: options?.attemptLogin,
}) })
const translationMode = this.osmConnection.GetPreference("translation-mode").sync( {
(str) => (str === undefined ? undefined : str === "true"), const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
[], this.osmConnection.GetPreference("translation-mode")
(b) => (b === undefined ? undefined : b + "") translationMode.addCallbackAndRunD((mode) => {
) mode = mode.toLowerCase()
if (mode === "true" || mode === "yes") {
translationMode.syncWith(Locale.showLinkToWeblate) Locale.showLinkOnMobile.setData(false)
Locale.showLinkToWeblate.setData(true)
this.isTranslator = this.osmConnection.userDetails.map((ud) => { } else if (mode === "false" || mode === "no") {
if (!ud.loggedIn) { Locale.showLinkToWeblate.setData(false)
return false } else if (mode === "mobile") {
} Locale.showLinkOnMobile.setData(true)
const name = ud.name.toLowerCase().replace(/\s+/g, "") Locale.showLinkToWeblate.setData(true)
return translators.contributors.some( } else {
(c) => c.contributor.toLowerCase().replace(/\s+/g, "") === name Locale.showLinkOnMobile.setData(false)
) Locale.showLinkToWeblate.setData(false)
}) }
})
this.isTranslator.addCallbackAndRunD((ud) => { }
if (ud) {
Locale.showLinkToWeblate.setData(true)
}
})
this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false) this.changes = new Changes(this, layoutToUse?.isLeftRightSensitive() ?? false)
@ -117,41 +112,6 @@ export default class UserRelatedState extends ElementsState {
this.installedUserThemes = this.InitInstalledUserThemes() this.installedUserThemes = this.InitInstalledUserThemes()
} }
private InitializeLanguage() {
const layoutToUse = this.layoutToUse
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse === undefined) {
return
}
if (Locale.showLinkToWeblate.data) {
return true // Disable auto switching as we are in translators mode
}
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
console.log(
"Resetting language to",
layoutToUse.language[0],
"as",
currentLanguage,
" is unsupported"
)
// The current language is not supported -> switch to a supported one
Locale.language.setData(layoutToUse.language[0])
}
})
Locale.language.ping()
}
private InitInstalledUserThemes(): Store<string[]> {
const prefix = "mapcomplete-unofficial-theme-"
const postfix = "-combined-length"
return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
Object.keys(prefs)
.filter((k) => k.startsWith(prefix) && k.endsWith(postfix))
.map((k) => k.substring(prefix.length, k.length - postfix.length))
)
}
public GetUnofficialTheme(id: string): public GetUnofficialTheme(id: string):
| { | {
id: string id: string
@ -193,4 +153,39 @@ export default class UserRelatedState extends ElementsState {
return undefined return undefined
} }
} }
private InitializeLanguage() {
const layoutToUse = this.layoutToUse
Locale.language.syncWith(this.osmConnection.GetPreference("language"))
Locale.language.addCallback((currentLanguage) => {
if (layoutToUse === undefined) {
return
}
if (Locale.showLinkToWeblate.data) {
return true // Disable auto switching as we are in translators mode
}
if (this.layoutToUse.language.indexOf(currentLanguage) < 0) {
console.log(
"Resetting language to",
layoutToUse.language[0],
"as",
currentLanguage,
" is unsupported"
)
// The current language is not supported -> switch to a supported one
Locale.language.setData(layoutToUse.language[0])
}
})
Locale.language.ping()
}
private InitInstalledUserThemes(): Store<string[]> {
const prefix = "mapcomplete-unofficial-theme-"
const postfix = "-combined-length"
return this.osmConnection.preferencesHandler.preferences.map((prefs) =>
Object.keys(prefs)
.filter((k) => k.startsWith(prefix) && k.endsWith(postfix))
.map((k) => k.substring(prefix.length, k.length - postfix.length))
)
}
} }

View file

@ -7,13 +7,13 @@ import { RegexTag } from "./RegexTag"
import SubstitutingTag from "./SubstitutingTag" import SubstitutingTag from "./SubstitutingTag"
import { Or } from "./Or" import { Or } from "./Or"
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson" import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
import * as key_counts from "../../assets/key_totals.json" import key_counts from "../../assets/key_totals.json"
type Tags = Record<string, string> type Tags = Record<string, string>
export type UploadableTag = Tag | SubstitutingTag | And export type UploadableTag = Tag | SubstitutingTag | And
export class TagUtils { export class TagUtils {
private static keyCounts: { keys: any; tags: any } = key_counts["default"] ?? key_counts private static keyCounts: { keys: any; tags: any } = key_counts
private static comparators: [string, (a: number, b: number) => boolean][] = [ private static comparators: [string, (a: number, b: number) => boolean][] = [
["<=", (a, b) => a <= b], ["<=", (a, b) => a <= b],
[">=", (a, b) => a >= b], [">=", (a, b) => a >= b],

View file

@ -106,7 +106,7 @@ export default class Constants {
* *
* In seconds * In seconds
*/ */
static zoomToLocationTimeout = 60 static zoomToLocationTimeout = 15
static countryCoderEndpoint: string = static countryCoderEndpoint: string =
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country" "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country"
public static readonly OsmPreferenceKeyPicturesLicense = "pictures-license" public static readonly OsmPreferenceKeyPicturesLicense = "pictures-license"

View file

@ -31,7 +31,7 @@ export class Denomination {
this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? [] this.alternativeDenominations = json.alternativeDenomination?.map((v) => v.trim()) ?? []
if (json["default"] !== undefined) { if (json["default" /* @code-quality: ignore*/] !== undefined) {
throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead` throw `${context} uses the old 'default'-key. Use "useIfNoUnitGiven" or "useAsDefaultInput" instead`
} }
this.useIfNoUnitGiven = json.useIfNoUnitGiven this.useIfNoUnitGiven = json.useIfNoUnitGiven

View file

@ -1,21 +1,20 @@
import { Conversion, DesugaringStep } from "./Conversion" import { Conversion, DesugaringStep } from "./Conversion"
import { LayoutConfigJson } from "../Json/LayoutConfigJson" import { LayoutConfigJson } from "../Json/LayoutConfigJson"
import { Utils } from "../../../Utils" import { Utils } from "../../../Utils"
import * as metapaths from "../../../assets/layoutconfigmeta.json" import metapaths from "../../../assets/layoutconfigmeta.json"
import * as tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json" import tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
export class ExtractImages extends Conversion<LayoutConfigJson, string[]> { export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
private _isOfficial: boolean private _isOfficial: boolean
private _sharedTagRenderings: Map<string, any> private _sharedTagRenderings: Map<string, any>
private static readonly layoutMetaPaths = (metapaths["default"] ?? metapaths).filter( private static readonly layoutMetaPaths = metapaths.filter(
(mp) => (mp) =>
ExtractImages.mightBeTagRendering(mp) || ExtractImages.mightBeTagRendering(<any>mp) ||
(mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon")) (mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
) )
private static readonly tagRenderingMetaPaths = private static readonly tagRenderingMetaPaths = tagrenderingmetapaths
tagrenderingmetapaths["default"] ?? tagrenderingmetapaths
constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) { constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) {
super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages") super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages")
@ -23,14 +22,16 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
this._sharedTagRenderings = sharedTagRenderings this._sharedTagRenderings = sharedTagRenderings
} }
public static mightBeTagRendering(metapath: { type: string | string[] }): boolean { public static mightBeTagRendering(metapath: { type?: string | string[] }): boolean {
if (!Array.isArray(metapath.type)) { if (!Array.isArray(metapath.type)) {
return false return false
} }
return metapath.type.some( return (
(t) => metapath.type?.some(
t["$ref"] == "#/definitions/TagRenderingConfigJson" || (t) =>
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson" t["$ref"] == "#/definitions/TagRenderingConfigJson" ||
t["$ref"] == "#/definitions/QuestionableTagRenderingConfigJson"
) ?? false
) )
} }
@ -83,7 +84,7 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
const errors = [] const errors = []
const warnings = [] const warnings = []
for (const metapath of ExtractImages.layoutMetaPaths) { for (const metapath of ExtractImages.layoutMetaPaths) {
const mightBeTr = ExtractImages.mightBeTagRendering(metapath) const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath)
const allRenderedValuesAreImages = const allRenderedValuesAreImages =
metapath.typeHint === "icon" || metapath.typeHint === "image" metapath.typeHint === "icon" || metapath.typeHint === "image"
const found = Utils.CollectPath(metapath.path, json) const found = Utils.CollectPath(metapath.path, json)
@ -271,14 +272,11 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
json = Utils.Clone(json) json = Utils.Clone(json)
let paths = metapaths["default"] ?? metapaths for (const metapath of metapaths) {
let trpaths = tagrenderingmetapaths["default"] ?? tagrenderingmetapaths
for (const metapath of paths) {
if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") { if (metapath.typeHint !== "image" && metapath.typeHint !== "icon") {
continue continue
} }
const mightBeTr = ExtractImages.mightBeTagRendering(metapath) const mightBeTr = ExtractImages.mightBeTagRendering(<any>metapath)
Utils.WalkPath(metapath.path, json, (leaf, path) => { Utils.WalkPath(metapath.path, json, (leaf, path) => {
if (typeof leaf === "string") { if (typeof leaf === "string") {
return replaceString(leaf) return replaceString(leaf)
@ -287,7 +285,7 @@ export class FixImages extends DesugaringStep<LayoutConfigJson> {
if (mightBeTr) { if (mightBeTr) {
// We might have reached a tagRenderingConfig containing icons // We might have reached a tagRenderingConfig containing icons
// lets walk every rendered value and fix the images in there // lets walk every rendered value and fix the images in there
for (const trpath of trpaths) { for (const trpath of tagrenderingmetapaths) {
if (trpath.typeHint !== "rendered") { if (trpath.typeHint !== "rendered") {
continue continue
} }

View file

@ -16,10 +16,10 @@ import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations" import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
import { Translation } from "../../../UI/i18n/Translation" import { Translation } from "../../../UI/i18n/Translation"
import * as tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json" import tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
import { AddContextToTranslations } from "./AddContextToTranslations" import { AddContextToTranslations } from "./AddContextToTranslations"
import FilterConfigJson from "../Json/FilterConfigJson" import FilterConfigJson from "../Json/FilterConfigJson"
import * as predifined_filters from "../../../assets/layers/filters/filters.json" import predifined_filters from "../../../assets/layers/filters/filters.json"
class ExpandFilter extends DesugaringStep<LayerConfigJson> { class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static load_filters(): Map<string, FilterConfigJson> { private static load_filters(): Map<string, FilterConfigJson> {
@ -730,8 +730,7 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
} { } {
const errors = [] const errors = []
json = Utils.Clone(json) json = Utils.Clone(json)
const paths: { path: string[]; type?: any; typeHint?: string }[] = const paths: { path: string[]; type?: any; typeHint?: string }[] = tagrenderingconfigmeta
tagrenderingconfigmeta["default"] ?? tagrenderingconfigmeta
for (const path of paths) { for (const path of paths) {
if (path.typeHint !== "rendered") { if (path.typeHint !== "rendered") {
continue continue

View file

@ -9,11 +9,9 @@ import LayoutConfig from "../LayoutConfig"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { TagUtils } from "../../../Logic/Tags/TagUtils" import { TagUtils } from "../../../Logic/Tags/TagUtils"
import { ExtractImages } from "./FixImages" import { ExtractImages } from "./FixImages"
import ScriptUtils from "../../../scripts/ScriptUtils"
import { And } from "../../../Logic/Tags/And" import { And } from "../../../Logic/Tags/And"
import Translations from "../../../UI/i18n/Translations" import Translations from "../../../UI/i18n/Translations"
import Svg from "../../../Svg" import Svg from "../../../Svg"
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
import FilterConfigJson from "../Json/FilterConfigJson" import FilterConfigJson from "../Json/FilterConfigJson"
import DeleteConfig from "../DeleteConfig" import DeleteConfig from "../DeleteConfig"
@ -365,7 +363,7 @@ export class PrevalidateTheme extends Fuse<LayoutConfigJson> {
} }
} }
export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRenderingConfigJson> { export class DetectShadowedMappings extends DesugaringStep<TagRenderingConfigJson> {
private readonly _calculatedTagNames: string[] private readonly _calculatedTagNames: string[]
constructor(layerConfig?: LayerConfigJson) { constructor(layerConfig?: LayerConfigJson) {
@ -425,9 +423,9 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
* r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true * r.errors[0].indexOf("The mapping key=value&x=y is fully matched by a previous mapping (namely 0)") >= 0 // => true
*/ */
convert( convert(
json: QuestionableTagRenderingConfigJson, json: TagRenderingConfigJson,
context: string context: string
): { result: QuestionableTagRenderingConfigJson; errors?: string[]; warnings?: string[] } { ): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[] } {
const errors = [] const errors = []
const warnings = [] const warnings = []
if (json.mappings === undefined || json.mappings.length === 0) { if (json.mappings === undefined || json.mappings.length === 0) {
@ -441,12 +439,9 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
const parsedConditions = json.mappings.map((m, i) => { const parsedConditions = json.mappings.map((m, i) => {
const ctx = `${context}.mappings[${i}]` const ctx = `${context}.mappings[${i}]`
const ifTags = TagUtils.Tag(m.if, ctx) const ifTags = TagUtils.Tag(m.if, ctx)
if ( const hideInAnswer = m["hideInAnswer"]
m.hideInAnswer !== undefined && if (hideInAnswer !== undefined && hideInAnswer !== false && hideInAnswer !== true) {
m.hideInAnswer !== false && let conditionTags = TagUtils.Tag(hideInAnswer)
m.hideInAnswer !== true
) {
let conditionTags = TagUtils.Tag(m.hideInAnswer)
// Merge the condition too! // Merge the condition too!
return new And([conditionTags, ifTags]) return new And([conditionTags, ifTags])
} }
@ -467,8 +462,8 @@ export class DetectShadowedMappings extends DesugaringStep<QuestionableTagRender
const doesMatch = parsedConditions[j].matchesProperties(properties) const doesMatch = parsedConditions[j].matchesProperties(properties)
if ( if (
doesMatch && doesMatch &&
json.mappings[j].hideInAnswer === true && json.mappings[j]["hideInAnswer"] === true &&
json.mappings[i].hideInAnswer !== true json.mappings[i]["hideInAnswer"] !== true
) { ) {
warnings.push( warnings.push(
`At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.` `At ${context}: Mapping ${i} is shadowed by mapping ${j}. However, mapping ${j} has 'hideInAnswer' set, which will result in a different rendering in question-mode.`
@ -623,7 +618,8 @@ export class ValidateTagRenderings extends Fuse<TagRenderingConfigJson> {
super( super(
"Various validation on tagRenderingConfigs", "Various validation on tagRenderingConfigs",
new DetectShadowedMappings(layerConfig), new DetectShadowedMappings(layerConfig),
new DetectMappingsWithImages(doesImageExist) new DetectMappingsWithImages(doesImageExist),
new MiscTagRenderingChecks()
) )
} }
} }

View file

@ -26,7 +26,7 @@ export interface MappingConfigJson {
/** /**
* Size of the image * Size of the image
*/ */
class: "small" | "medium" | "large" | string class?: "small" | "medium" | "large" | string
} }
/** /**

View file

@ -125,7 +125,7 @@ export interface TagRenderingConfigJson {
* A hint to mapcomplete on how to render this icon within the mapping. * A hint to mapcomplete on how to render this icon within the mapping.
* This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged) * This is translated to 'mapping-icon-<classtype>', so defining your own in combination with a custom CSS is possible (but discouraged)
*/ */
class: "small" | "medium" | "large" | string class?: "small" | "medium" | "large" | string
} }
}[] }[]
} }

View file

@ -7,7 +7,7 @@ import TilesourceConfig from "./TilesourceConfig"
import { ExtractImages } from "./Conversion/FixImages" import { ExtractImages } from "./Conversion/FixImages"
import ExtraLinkConfig from "./ExtraLinkConfig" import ExtraLinkConfig from "./ExtraLinkConfig"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import * as used_languages from "../../assets/generated/used_languages.json" import used_languages from "../../assets/generated/used_languages.json"
export default class LayoutConfig { export default class LayoutConfig {
public static readonly defaultSocialImage = "assets/SocialImage.png" public static readonly defaultSocialImage = "assets/SocialImage.png"
public readonly id: string public readonly id: string
@ -237,13 +237,11 @@ export default class LayoutConfig {
} }
public missingTranslations(): { public missingTranslations(): {
completeness: Map<string, number>
untranslated: Map<string, string[]> untranslated: Map<string, string[]>
total: number total: number
} { } {
const layout = this const layout = this
let total = 0 let total = 0
const completeness = new Map<string, number>()
const untranslated = new Map<string, string[]>() const untranslated = new Map<string, string[]>()
Utils.WalkObject( Utils.WalkObject(
@ -264,13 +262,21 @@ export default class LayoutConfig {
if (trans["*"] !== undefined) { if (trans["*"] !== undefined) {
return return
} }
if (translation.context.indexOf(":") < 0) {
return
}
if (trans[ln] === undefined) { if (trans[ln] === undefined) {
if (!untranslated.has(ln)) { if (!untranslated.has(ln)) {
untranslated.set(ln, []) untranslated.set(ln, [])
} }
untranslated.get(ln).push(translation.context) untranslated
} else { .get(ln)
completeness.set(ln, 1 + (completeness.get(ln) ?? 0)) .push(
translation.context.replace(
/^note_import_[a-zA-Z0-9_]*/,
"note_import"
)
)
} }
}) })
}, },
@ -282,7 +288,7 @@ export default class LayoutConfig {
} }
) )
return { completeness, untranslated, total } return { untranslated, total }
} }
public getMatchingLayer(tags: any): LayerConfig | undefined { public getMatchingLayer(tags: any): LayerConfig | undefined {
if (tags === undefined) { if (tags === undefined) {

View file

@ -27,7 +27,7 @@ import { QueryParameters } from "../Logic/Web/QueryParameters"
import { SubstitutedTranslation } from "./SubstitutedTranslation" import { SubstitutedTranslation } from "./SubstitutedTranslation"
import { AutoAction } from "./Popup/AutoApplyButton" import { AutoAction } from "./Popup/AutoApplyButton"
import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource" import DynamicGeoJsonTileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicGeoJsonTileSource"
import * as themeOverview from "../assets/generated/theme_overview.json" import themeOverview from "../assets/generated/theme_overview.json"
class AutomationPanel extends Combine { class AutomationPanel extends Combine {
private static readonly openChangeset = new UIEventSource<number>(undefined) private static readonly openChangeset = new UIEventSource<number>(undefined)

View file

@ -2,6 +2,7 @@ import { VariableUiElement } from "./VariableUIElement"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"
import Link from "./Link" import Link from "./Link"
import Svg from "../../Svg" import Svg from "../../Svg"
import show = Mocha.reporters.Base.cursor.show
/** /**
* The little 'translate'-icon next to every icon + some static helper functions * The little 'translate'-icon next to every icon + some static helper functions
@ -21,7 +22,7 @@ export default class LinkToWeblate extends VariableUiElement {
return undefined return undefined
} }
const icon = Svg.translate_svg().SetClass( const icon = Svg.translate_svg().SetClass(
"rounded-full border border-gray-400 inline-block w-4 h-4 m-1 weblate-link self-center" "rounded-full inline-block w-3 h-3 ml-1 weblate-link self-center"
) )
if (availableTranslations[ln] === undefined) { if (availableTranslations[ln] === undefined) {
icon.SetClass("bg-red-400") icon.SetClass("bg-red-400")
@ -31,7 +32,15 @@ export default class LinkToWeblate extends VariableUiElement {
[Locale.showLinkToWeblate] [Locale.showLinkToWeblate]
) )
) )
this.SetClass("enable-links hidden-on-mobile") this.SetClass("enable-links")
const self = this
Locale.showLinkOnMobile.addCallbackAndRunD((showOnMobile) => {
if (showOnMobile) {
self.RemoveClass("hidden-on-mobile")
} else {
self.SetClass("hidden-on-mobile")
}
})
} }
/** /**

View file

@ -143,7 +143,7 @@ export default class ScrollableFullScreen {
) )
const contentWrapper = new Combine([content]).SetClass( const contentWrapper = new Combine([content]).SetClass(
"block p-2 md:pt-4 w-full h-full overflow-y-auto desktop:max-h-65vh" "block p-2 md:pt-4 w-full h-full overflow-y-auto"
) )
this._resetScrollSignal.addCallback((_) => { this._resetScrollSignal.addCallback((_) => {
@ -159,7 +159,7 @@ export default class ScrollableFullScreen {
// We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide // We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide
]).SetClass("flex flex-col h-full relative bg-white"), ]).SetClass("flex flex-col h-full relative bg-white"),
]).SetClass( ]).SetClass(
"fixed top-0 left-0 right-0 h-screen w-screen desktop:max-h-65vh md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden" "fixed top-0 left-0 right-0 h-screen w-screen md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden"
) )
} }

View file

@ -9,9 +9,10 @@ import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg" import Svg from "../../Svg"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import { MapillaryLink } from "./MapillaryLink" import { MapillaryLink } from "./MapillaryLink"
import TranslatorsPanel from "./TranslatorsPanel"
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel" import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState"
export class BackToThemeOverview extends Toggle { export class BackToThemeOverview extends Toggle {
constructor( constructor(
@ -77,7 +78,14 @@ export class ActionButtons extends Combine {
new OpenIdEditor(state, iconStyle), new OpenIdEditor(state, iconStyle),
new MapillaryLink(state, iconStyle), new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"), new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
new TranslatorsPanel(state, iconStyle), new SubtleButton(
Svg.translate_ui().SetStyle(iconStyle),
Translations.t.translations.activateButton
).onClick(() => {
ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode")
}),
]) ])
this.SetClass("block w-full link-no-underline") this.SetClass("block w-full link-no-underline")
} }

View file

@ -1,9 +1,9 @@
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import * as welcome_messages from "../../assets/welcome_message.json" import welcome_messages from "../../assets/welcome_message.json"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import MoreScreen from "./MoreScreen" import MoreScreen from "./MoreScreen"
import * as themeOverview from "../../assets/generated/theme_overview.json" import themeOverview from "../../assets/generated/theme_overview.json"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Title from "../Base/Title" import Title from "../Base/Title"
@ -43,7 +43,7 @@ export default class FeaturedMessage extends Combine {
}[] = [] }[] = []
const themesById = new Map<string, { id: string; title: any; shortDescription: any }>() const themesById = new Map<string, { id: string; title: any; shortDescription: any }>()
for (const theme of themeOverview["default"]) { for (const theme of themeOverview) {
themesById.set(theme.id, theme) themesById.set(theme.id, theme)
} }
@ -88,9 +88,7 @@ export default class FeaturedMessage extends Combine {
const msg = new FixedUiElement(welcome_message.message).SetClass("link-underline font-lg") const msg = new FixedUiElement(welcome_message.message).SetClass("link-underline font-lg")
els.push(new Combine([title, msg]).SetClass("m-4")) els.push(new Combine([title, msg]).SetClass("m-4"))
if (welcome_message.featured_theme !== undefined) { if (welcome_message.featured_theme !== undefined) {
const theme = themeOverview["default"].filter( const theme = themeOverview.filter((th) => th.id === welcome_message.featured_theme)[0]
(th) => th.id === welcome_message.featured_theme
)[0]
els.push( els.push(
MoreScreen.createLinkButton({}, theme) MoreScreen.createLinkButton({}, theme)

View file

@ -6,6 +6,7 @@ import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import Hotkeys from "../Base/Hotkeys" import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
/** /**
* Displays an icon depending on the state of the geolocation. * Displays an icon depending on the state of the geolocation.
@ -20,6 +21,9 @@ export class GeolocationControl extends VariableUiElement {
} }
) { ) {
const lastClick = new UIEventSource<Date>(undefined) const lastClick = new UIEventSource<Date>(undefined)
lastClick.addCallbackD((date) => {
geolocationHandler.geolocationState.requestMoment.setData(date)
})
const lastClickWithinThreeSecs = lastClick.map((lastClick) => { const lastClickWithinThreeSecs = lastClick.map((lastClick) => {
if (lastClick === undefined) { if (lastClick === undefined) {
return false return false
@ -27,6 +31,16 @@ export class GeolocationControl extends VariableUiElement {
const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000 const timeDiff = (new Date().getTime() - lastClick.getTime()) / 1000
return timeDiff <= 3 return timeDiff <= 3
}) })
const lastRequestWithinTimeout = geolocationHandler.geolocationState.requestMoment.map(
(date) => {
if (date === undefined) {
return false
}
const timeDiff = (new Date().getTime() - date.getTime()) / 1000
console.log("Timediff", timeDiff)
return timeDiff <= Constants.zoomToLocationTimeout
}
)
const geolocationState = geolocationHandler?.geolocationState const geolocationState = geolocationHandler?.geolocationState
super( super(
geolocationState?.permission?.map( geolocationState?.permission?.map(
@ -43,9 +57,10 @@ export class GeolocationControl extends VariableUiElement {
return Svg.location_empty_svg() return Svg.location_empty_svg()
} }
// Position not yet found, but permission is either requested or granted: we spin to indicate activity // Position not yet found, but permission is either requested or granted: we spin to indicate activity
const icon = !geolocationHandler.mapHasMoved.data const icon =
? Svg.location_svg() !geolocationHandler.mapHasMoved.data || lastRequestWithinTimeout.data
: Svg.location_empty_svg() ? Svg.location_svg()
: Svg.location_empty_svg()
return icon return icon
.SetClass("cursor-wait") .SetClass("cursor-wait")
.SetStyle("animation: spin 4s linear infinite;") .SetStyle("animation: spin 4s linear infinite;")
@ -65,6 +80,7 @@ export class GeolocationControl extends VariableUiElement {
geolocationState.isLocked, geolocationState.isLocked,
geolocationHandler.mapHasMoved, geolocationHandler.mapHasMoved,
lastClickWithinThreeSecs, lastClickWithinThreeSecs,
lastRequestWithinTimeout,
] ]
) )
) )
@ -74,6 +90,8 @@ export class GeolocationControl extends VariableUiElement {
geolocationState.permission.data !== "granted" && geolocationState.permission.data !== "granted" &&
geolocationState.currentGPSLocation.data === undefined geolocationState.currentGPSLocation.data === undefined
) { ) {
lastClick.setData(new Date())
geolocationState.requestMoment.setData(new Date())
await geolocationState.requestPermission() await geolocationState.requestPermission()
} }
@ -85,6 +103,7 @@ export class GeolocationControl extends VariableUiElement {
if (geolocationState.currentGPSLocation.data === undefined) { if (geolocationState.currentGPSLocation.data === undefined) {
// No location is known yet, not much we can do // No location is known yet, not much we can do
lastClick.setData(new Date())
return return
} }
@ -126,5 +145,12 @@ export class GeolocationControl extends VariableUiElement {
} }
}, 500) }, 500)
}) })
geolocationHandler.geolocationState.requestMoment.addCallbackAndRunD((_) => {
window.setTimeout(() => {
if (lastRequestWithinTimeout.data) {
geolocationHandler.geolocationState.requestMoment.ping()
}
}, 500)
})
} }
} }

View file

@ -3,7 +3,7 @@ import Svg from "../../Svg"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import * as personal from "../../assets/themes/personal/personal.json" import personal from "../../assets/themes/personal/personal.json"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
@ -14,7 +14,7 @@ import UserRelatedState from "../../Logic/State/UserRelatedState"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils" import { Utils } from "../../Utils"
import Title from "../Base/Title" import Title from "../Base/Title"
import * as themeOverview from "../../assets/generated/theme_overview.json" import themeOverview from "../../assets/generated/theme_overview.json"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import { TextField } from "../Input/TextField" import { TextField } from "../Input/TextField"
import FilteredCombine from "../Base/FilteredCombine" import FilteredCombine from "../Base/FilteredCombine"
@ -30,7 +30,7 @@ export default class MoreScreen extends Combine {
mustHaveLanguage?: boolean mustHaveLanguage?: boolean
hideFromOverview: boolean hideFromOverview: boolean
keywors?: any[] keywors?: any[]
}[] = themeOverview["default"] }[] = themeOverview
constructor( constructor(
state: UserRelatedState & { state: UserRelatedState & {
@ -287,7 +287,7 @@ export default class MoreScreen extends Combine {
): BaseUIElement { ): BaseUIElement {
const t = Translations.t.general.morescreen const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-" const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes = themeOverview["default"].filter((layout) => layout.hideFromOverview) const hiddenThemes = themeOverview.filter((layout) => layout.hideFromOverview)
const hiddenTotal = hiddenThemes.length const hiddenTotal = hiddenThemes.length
return new Toggle( return new Toggle(

View file

@ -1,148 +0,0 @@
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Translation } from "../i18n/Translation"
import { VariableUiElement } from "../Base/VariableUIElement"
import Link from "../Base/Link"
import LinkToWeblate from "../Base/LinkToWeblate"
import Toggleable from "../Base/Toggleable"
import Title from "../Base/Title"
import { Store } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import * as native_languages from "../../assets/language_native.json"
import BaseUIElement from "../BaseUIElement"
class TranslatorsPanelContent extends Combine {
constructor(layout: LayoutConfig, isTranslator: Store<boolean>) {
const t = Translations.t.translations
const { completeness, untranslated, total } = layout.missingTranslations()
const seed = t.completeness
for (const ln of Array.from(completeness.keys())) {
if (ln === "*") {
continue
}
if (seed.translations[ln] === undefined) {
seed.translations[ln] = seed.translations["en"]
}
}
const completenessTr = {}
const completenessPercentage = {}
seed.SupportedLanguages().forEach((ln) => {
completenessTr[ln] = "" + (completeness.get(ln) ?? 0)
completenessPercentage[ln] =
"" + Math.round((100 * (completeness.get(ln) ?? 0)) / total)
})
function missingTranslationsFor(language: string): BaseUIElement[] {
// e.g. "themes:<themename>.layers.0.tagRenderings..., or "layers:<layername>.description
const missingKeys = Utils.NoNull(untranslated.get(language) ?? [])
.filter((ctx) => ctx.indexOf(":") >= 0)
.map((ctx) => ctx.replace(/note_import_[a-zA-Z0-9_]*/, "note_import"))
const hasMissingTheme = missingKeys.some((k) => k.startsWith("themes:"))
const missingLayers = Utils.Dedup(
missingKeys
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
console.log(
"Getting untranslated string for",
language,
"raw:",
missingKeys,
"hasMissingTheme:",
hasMissingTheme,
"missingLayers:",
missingLayers
)
return Utils.NoNull([
hasMissingTheme
? new Link(
"themes:" + layout.id + ".* (zen mode)",
LinkToWeblate.hrefToWeblateZen(language, "themes", layout.id),
true
)
: undefined,
...missingLayers.map(
(id) =>
new Link(
"layer:" + id + ".* (zen mode)",
LinkToWeblate.hrefToWeblateZen(language, "layers", id),
true
)
),
...missingKeys.map(
(context) =>
new Link(context, LinkToWeblate.hrefToWeblate(language, context), true)
),
])
}
// "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
const translated = seed.Subs({
total,
theme: layout.title,
percentage: new Translation(completenessPercentage),
translated: new Translation(completenessTr),
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng),
})
super([
new Title(Translations.t.translations.activateButton),
new Toggle(t.isTranslator.SetClass("thanks block"), undefined, isTranslator),
t.help,
translated,
/*Disable button:*/
new SubtleButton(undefined, t.deactivate).onClick(() => {
Locale.showLinkToWeblate.setData(false)
}),
new VariableUiElement(
Locale.language.map((ln) => {
const missing = missingTranslationsFor(ln)
if (missing.length === 0) {
return undefined
}
let title = Translations.t.translations.allMissing
if (untranslated.get(ln) !== undefined) {
title = Translations.t.translations.missing.Subs({
count: untranslated.get(ln).length,
})
}
return new Toggleable(
new Title(title),
new Combine(missing).SetClass("flex flex-col")
)
})
),
])
}
}
export default class TranslatorsPanel extends Toggle {
constructor(
state: { layoutToUse: LayoutConfig; isTranslator: Store<boolean> },
iconStyle?: string
) {
const t = Translations.t.translations
super(
new Lazy(
() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
).SetClass("flex flex-col"),
new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() =>
Locale.showLinkToWeblate.setData(true)
),
Locale.showLinkToWeblate
)
this.SetClass("hidden-on-mobile")
}
}

View file

@ -19,11 +19,14 @@ import EditableTagRendering from "../Popup/EditableTagRendering"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { SaveButton } from "../Popup/SaveButton" import { SaveButton } from "../Popup/SaveButton"
import { TagUtils } from "../../Logic/Tags/TagUtils" import { TagUtils } from "../../Logic/Tags/TagUtils"
import * as usersettings from "../../assets/generated/layers/usersettings.json" import usersettings from "../../assets/generated/layers/usersettings.json"
import { LoginToggle } from "../Popup/LoginButton" import { LoginToggle } from "../Popup/LoginButton"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import * as translators from "../../assets/translators.json" import translators from "../../assets/translators.json"
import * as codeContributors from "../../assets/contributors.json" import codeContributors from "../../assets/contributors.json"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import LinkToWeblate from "../Base/LinkToWeblate"
export class ImportViewerLinks extends VariableUiElement { export class ImportViewerLinks extends VariableUiElement {
constructor(osmConnection: OsmConnection) { constructor(osmConnection: OsmConnection) {
@ -53,7 +56,7 @@ class SingleUserSettingsPanel extends EditableTagRendering {
userInfoFocusedQuestion?: UIEventSource<string> userInfoFocusedQuestion?: UIEventSource<string>
) { ) {
const editMode = new UIEventSource(false) const editMode = new UIEventSource(false)
// Isolate the preferences. THey'll be updated explicitely later on anyway // Isolate the preferences. They'll be updated explicitely later on anyway
super( super(
amendedPrefs, amendedPrefs,
config, config,
@ -68,6 +71,9 @@ class SingleUserSettingsPanel extends EditableTagRendering {
TagUtils.FlattenAnd(store.data, amendedPrefs.data) TagUtils.FlattenAnd(store.data, amendedPrefs.data)
).asChange(amendedPrefs.data) ).asChange(amendedPrefs.data)
for (const kv of selection) { for (const kv of selection) {
if (kv.k.startsWith("_")) {
continue
}
osmConnection.GetPreference(kv.k, "", "").setData(kv.v) osmConnection.GetPreference(kv.k, "", "").setData(kv.v)
} }
@ -104,13 +110,59 @@ class UserInformationMainPanel extends VariableUiElement {
const settings = new UIEventSource<Record<string, BaseUIElement>>({}) const settings = new UIEventSource<Record<string, BaseUIElement>>({})
const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel") const usersettingsConfig = new LayerConfig(usersettings, "userinformationpanel")
const amendedPrefs = new UIEventSource<any>({}) const amendedPrefs = new UIEventSource<any>({ _theme: layout?.id })
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => { osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) { for (const k in newPrefs) {
amendedPrefs.data[k] = newPrefs[k] amendedPrefs.data[k] = newPrefs[k]
} }
amendedPrefs.ping() amendedPrefs.ping()
}) })
const translationMode = osmConnection.GetPreference("translation-mode")
Locale.language.mapD(
(language) => {
amendedPrefs.data["_language"] = language
const trmode = translationMode.data
if (trmode === "true" || trmode === "mobile") {
const missing = layout.missingTranslations()
const total = missing.total
const untranslated = missing.untranslated.get(language) ?? []
const hasMissingTheme = untranslated.some((k) => k.startsWith("themes:"))
const missingLayers = Utils.Dedup(
untranslated
.filter((k) => k.startsWith("layers:"))
.map((k) => k.slice("layers:".length).split(".")[0])
)
const zenLinks: { link: string; id: string }[] = Utils.NoNull([
hasMissingTheme
? {
id: "theme:" + layout.id,
link: LinkToWeblate.hrefToWeblateZen(
language,
"themes",
layout.id
),
}
: undefined,
...missingLayers.map((id) => ({
id: "layer:" + id,
link: LinkToWeblate.hrefToWeblateZen(language, "layers", id),
})),
])
const untranslated_count = untranslated.length
amendedPrefs.data["_translation_total"] = "" + total
amendedPrefs.data["_translation_translated_count"] =
"" + (total - untranslated_count)
amendedPrefs.data["_translation_percentage"] =
"" + Math.floor((100 * (total - untranslated_count)) / total)
console.log("Setting zenLinks", zenLinks)
amendedPrefs.data["_translation_links"] = JSON.stringify(zenLinks)
}
amendedPrefs.ping()
},
[translationMode]
)
osmConnection.userDetails.addCallback((userDetails) => { osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) { for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k] amendedPrefs.data["_" + k] = "" + userDetails[k]

View file

@ -5,7 +5,7 @@ import { Utils } from "../Utils"
import Combine from "./Base/Combine" import Combine from "./Base/Combine"
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import * as home_location_json from "../assets/layers/home_location/home_location.json" import home_location_json from "../assets/layers/home_location/home_location.json"
import State from "../State" import State from "../State"
import Title from "./Base/Title" import Title from "./Base/Title"
import { MinimapObj } from "./Base/Minimap" import { MinimapObj } from "./Base/Minimap"

View file

@ -18,7 +18,7 @@ import SimpleAddUI from "./BigComponents/SimpleAddUI"
import StrayClickHandler from "../Logic/Actors/StrayClickHandler" import StrayClickHandler from "../Logic/Actors/StrayClickHandler"
import { DefaultGuiState } from "./DefaultGuiState" import { DefaultGuiState } from "./DefaultGuiState"
import LayerConfig from "../Models/ThemeConfig/LayerConfig" import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import * as home_location_json from "../assets/layers/home_location/home_location.json" import home_location_json from "../assets/layers/home_location/home_location.json"
import NewNoteUi from "./Popup/NewNoteUi" import NewNoteUi from "./Popup/NewNoteUi"
import Combine from "./Base/Combine" import Combine from "./Base/Combine"
import AddNewMarker from "./BigComponents/AddNewMarker" import AddNewMarker from "./BigComponents/AddNewMarker"
@ -33,7 +33,6 @@ import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState" import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys" import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers" import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import Lazy from "./Base/Lazy"
import CopyrightPanel from "./BigComponents/CopyrightPanel" import CopyrightPanel from "./BigComponents/CopyrightPanel"
/** /**

View file

@ -13,12 +13,12 @@ import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import FeatureInfoBox from "../Popup/FeatureInfoBox" import FeatureInfoBox from "../Popup/FeatureInfoBox"
import { ImportUtils } from "./ImportUtils" import { ImportUtils } from "./ImportUtils"
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import Title from "../Base/Title" import Title from "../Base/Title"
import Loading from "../Base/Loading" import Loading from "../Base/Loading"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import * as known_layers from "../../assets/generated/known_layers.json" import known_layers from "../../assets/generated/known_layers.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { Feature } from "geojson" import { Feature } from "geojson"

View file

@ -22,12 +22,12 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ValidatedTextField from "../Input/ValidatedTextField" import ValidatedTextField from "../Input/ValidatedTextField"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource" import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import * as import_candidate from "../../assets/layers/import_candidate/import_candidate.json" import import_candidate from "../../assets/layers/import_candidate/import_candidate.json"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import FeatureInfoBox from "../Popup/FeatureInfoBox" import FeatureInfoBox from "../Popup/FeatureInfoBox"
import { ImportUtils } from "./ImportUtils" import { ImportUtils } from "./ImportUtils"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import * as currentview from "../../assets/layers/current_view/current_view.json" import currentview from "../../assets/layers/current_view/current_view.json"
import { CheckBox } from "../Input/Checkboxes" import { CheckBox } from "../Input/Checkboxes"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch" import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import { Feature, FeatureCollection, Point } from "geojson" import { Feature, FeatureCollection, Point } from "geojson"

View file

@ -14,7 +14,7 @@ import { FixedUiElement } from "../Base/FixedUiElement"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Toggle from "./Toggle" import Toggle from "./Toggle"
import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" import matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import { ElementStorage } from "../../Logic/ElementStorage" import { ElementStorage } from "../../Logic/ElementStorage"

View file

@ -61,17 +61,6 @@ export class TextField extends InputElement<string> {
return this._isValid(t) return this._isValid(t)
} }
private static test() {
const placeholder = new UIEventSource<string>("placeholder")
const tf = new TextField({
placeholder,
})
const html = <HTMLInputElement>tf.InnerConstructElement().children[0]
html.placeholder // => 'placeholder'
placeholder.setData("another piece of text")
html.placeholder // => "another piece of text"
}
/** /**
* *
* // should update placeholders dynamically * // should update placeholders dynamically

View file

@ -52,10 +52,6 @@ export class TextFieldDef {
} }
} }
protectedisValid(s: string, _: (() => string) | undefined): boolean {
return true
}
public getFeedback(s: string): Translation { public getFeedback(s: string): Translation {
const tr = Translations.t.validation[this.name] const tr = Translations.t.validation[this.name]
if (tr !== undefined) { if (tr !== undefined) {
@ -82,6 +78,9 @@ export class TextFieldDef {
} }
options["textArea"] = this.name === "text" options["textArea"] = this.name === "text"
if (this.name === "text") {
options["htmlType"] = "area"
}
const self = this const self = this
@ -589,7 +588,7 @@ class StringTextField extends TextFieldDef {
class TextTextField extends TextFieldDef { class TextTextField extends TextFieldDef {
declare inputmode: "text" declare inputmode: "text"
constructor() { constructor() {
super("text", "A longer piece of text") super("text", "A longer piece of text. Uses an textArea instead of a textField")
} }
} }

View file

@ -1,10 +1,10 @@
import { DropDown } from "./Input/DropDown" import { DropDown } from "./Input/DropDown"
import Locale from "./i18n/Locale" import Locale from "./i18n/Locale"
import BaseUIElement from "./BaseUIElement" import BaseUIElement from "./BaseUIElement"
import * as native from "../assets/language_native.json" import native from "../assets/language_native.json"
import * as language_translations from "../assets/language_translations.json" import language_translations from "../assets/language_translations.json"
import { Translation } from "./i18n/Translation" import { Translation } from "./i18n/Translation"
import * as used_languages from "../assets/generated/used_languages.json" import used_languages from "../assets/generated/used_languages.json"
import Lazy from "./Base/Lazy" import Lazy from "./Base/Lazy"
import Toggle from "./Input/Toggle" import Toggle from "./Input/Toggle"
@ -35,9 +35,8 @@ export default class LanguagePicker extends Toggle {
private static hybrid(lang: string): Translation { private static hybrid(lang: string): Translation {
const nativeText = native[lang] ?? lang const nativeText = native[lang] ?? lang
const allTranslations = language_translations["default"] ?? language_translations
const translation = {} const translation = {}
const trans = allTranslations[lang] const trans = language_translations[lang]
if (trans === undefined) { if (trans === undefined) {
return new Translation({ "*": nativeText }) return new Translation({ "*": nativeText })
} }
@ -45,7 +44,7 @@ export default class LanguagePicker extends Toggle {
if (key.startsWith("_")) { if (key.startsWith("_")) {
continue continue
} }
const translationInKey = allTranslations[lang][key] const translationInKey = language_translations[lang][key]
if (nativeText.toLowerCase() === translationInKey.toLowerCase()) { if (nativeText.toLowerCase() === translationInKey.toLowerCase()) {
translation[key] = nativeText translation[key] = nativeText
} else { } else {

View file

@ -1,7 +1,7 @@
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector" import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import { Store } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import * as all_languages from "../../assets/language_translations.json" import all_languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
export class AllLanguagesSelector extends SearchablePillsSelector<string> { export class AllLanguagesSelector extends SearchablePillsSelector<string> {
@ -18,7 +18,7 @@ export class AllLanguagesSelector extends SearchablePillsSelector<string> {
hasPriority?: Store<boolean> hasPriority?: Store<boolean>
}[] = [] }[] = []
const langs = options?.supportedLanguages ?? all_languages["default"] ?? all_languages const langs = options?.supportedLanguages ?? all_languages
for (const ln in langs) { for (const ln in langs) {
let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } = let languageInfo: Record<string, string> & { _meta?: { countries: string[] } } =
all_languages[ln] all_languages[ln]

View file

@ -35,7 +35,7 @@ import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/Crea
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import TagApplyButton from "./TagApplyButton" import TagApplyButton from "./TagApplyButton"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import * as conflation_json from "../../assets/layers/conflation/conflation.json" import conflation_json from "../../assets/layers/conflation/conflation.json"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import { LoginToggle } from "./LoginButton" import { LoginToggle } from "./LoginButton"
import { AutoAction } from "./AutoApplyButton" import { AutoAction } from "./AutoApplyButton"

View file

@ -4,7 +4,7 @@ import { UIEventSource } from "../../Logic/UIEventSource"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import { OsmTags } from "../../Models/OsmFeature" import { OsmTags } from "../../Models/OsmFeature"
import * as all_languages from "../../assets/language_translations.json" import all_languages from "../../assets/language_translations.json"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Title from "../Base/Title" import Title from "../Base/Title"

View file

@ -11,6 +11,7 @@ import Toggle from "../Input/Toggle"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import FilteredLayer from "../../Models/FilteredLayer" import FilteredLayer from "../../Models/FilteredLayer"
import Hash from "../../Logic/Web/Hash"
export default class NewNoteUi extends Toggle { export default class NewNoteUi extends Toggle {
constructor( constructor(
@ -33,7 +34,7 @@ export default class NewNoteUi extends Toggle {
text.SetClass("border rounded-sm border-grey-500") text.SetClass("border rounded-sm border-grey-500")
const postNote = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.createNote) const postNote = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.createNote)
postNote.onClick(async () => { postNote.OnClickWithLoading(t.creating, async () => {
let txt = text.GetValue().data let txt = text.GetValue().data
if (txt === undefined || txt === "") { if (txt === undefined || txt === "") {
return return
@ -63,6 +64,7 @@ export default class NewNoteUi extends Toggle {
} }
state?.featurePipeline?.InjectNewPoint(feature) state?.featurePipeline?.InjectNewPoint(feature)
state.selectedElement?.setData(feature) state.selectedElement?.setData(feature)
Hash.hash.setData(feature.properties.id)
text.GetValue().setData("") text.GetValue().setData("")
isCreated.setData(true) isCreated.setData(true)
}) })
@ -73,12 +75,12 @@ export default class NewNoteUi extends Toggle {
new Combine([ new Combine([
new Toggle( new Toggle(
undefined, undefined,
t.warnAnonymous.SetClass("alert"), t.warnAnonymous.SetClass("block alert"),
state?.osmConnection?.isLoggedIn state?.osmConnection?.isLoggedIn
), ),
new Toggle( new Toggle(
postNote, postNote,
t.textNeeded.SetClass("alert"), t.textNeeded.SetClass("block alert"),
text.GetValue().map((txt) => txt?.length > 3) text.GetValue().map((txt) => txt?.length > 3)
), ),
]).SetClass("flex justify-end items-center"), ]).SetClass("flex justify-end items-center"),

View file

@ -3,7 +3,7 @@ import Loc from "../../Models/Loc"
import Minimap from "../Base/Minimap" import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import * as left_right_style_json from "../../assets/layers/left_right_style/left_right_style.json" import left_right_style_json from "../../assets/layers/left_right_style/left_right_style.json"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { SpecialVisualization } from "../SpecialVisualization" import { SpecialVisualization } from "../SpecialVisualization"

View file

@ -15,7 +15,7 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox" import { BBox } from "../../Logic/BBox"
import * as split_point from "../../assets/layers/split_point/split_point.json" import split_point from "../../assets/layers/split_point/split_point.json"
import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"

View file

@ -136,7 +136,7 @@ export default class ShowDataLayerImplementation {
if (this._leafletMap.data === undefined) { if (this._leafletMap.data === undefined) {
return return
} }
const v = this.leafletLayersPerId.get(selected.properties.id + selected.geometry.type) const v = this.leafletLayersPerId.get(selected.properties.id)
if (v === undefined) { if (v === undefined) {
return return
} }
@ -335,7 +335,20 @@ export default class ShowDataLayerImplementation {
icon: L.divIcon(style), icon: L.divIcon(style),
}) })
} }
/**
* Creates a function which, for the given feature, will open the featureInfoBox (and lazyly create it)
* This function is cached
* @param feature
* @param key
* @param layer
* @private
*/
private createActivateFunction(feature, key: string, layer: LayerConfig): (event) => void { private createActivateFunction(feature, key: string, layer: LayerConfig): (event) => void {
if (this.leafletLayersPerId.has(key)) {
return this.leafletLayersPerId.get(key).activateFunc
}
let infobox: ScrollableFullScreen = undefined let infobox: ScrollableFullScreen = undefined
const self = this const self = this
@ -373,12 +386,7 @@ export default class ShowDataLayerImplementation {
return return
} }
const key = feature.properties.id const key = feature.properties.id
let activate: (event) => void const activate = this.createActivateFunction(feature, key, layer)
if (this.leafletLayersPerId.has(key)) {
activate = this.leafletLayersPerId.get(key).activateFunc
} else {
activate = this.createActivateFunction(feature, key, layer)
}
// We also have to open on rightclick, doubleclick, ... as users sometimes do this. See #1219 // We also have to open on rightclick, doubleclick, ... as users sometimes do this. See #1219
leafletLayer.on({ leafletLayer.on({

View file

@ -5,7 +5,7 @@ import ShowDataLayer from "./ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import { Tiles } from "../../Models/TileRange" import { Tiles } from "../../Models/TileRange"
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json" import clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
export default class ShowTileInfo { export default class ShowTileInfo {
public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true) public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true)

View file

@ -612,8 +612,8 @@ export default class SpecialVisualizations {
special: { special: {
type: "multi", type: "multi",
key: "_doors_from_building_properties", key: "_doors_from_building_properties",
tagRendering: { tagrendering: {
render: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}", en: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}",
}, },
}, },
}, },

View file

@ -5,6 +5,10 @@ import { QueryParameters } from "../../Logic/Web/QueryParameters"
export default class Locale { export default class Locale {
public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false) public static showLinkToWeblate: UIEventSource<boolean> = new UIEventSource<boolean>(false)
/**
* Indicates that -if showLinkToWeblate is true- a link on mobile mode is shown as well
*/
public static showLinkOnMobile: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public static language: UIEventSource<string> = Locale.setup() public static language: UIEventSource<string> = Locale.setup()
private static setup() { private static setup() {

View file

@ -1,7 +1,7 @@
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import { Translation, TypedTranslation } from "./Translation" import { Translation, TypedTranslation } from "./Translation"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import * as known_languages from "../../assets/generated/used_languages.json" import known_languages from "../../assets/generated/used_languages.json"
import CompiledTranslations from "../../assets/generated/CompiledTranslations" import CompiledTranslations from "../../assets/generated/CompiledTranslations"
export default class Translations { export default class Translations {

View file

@ -1,4 +1,4 @@
import * as colors from "./assets/colors.json" import colors from "./assets/colors.json"
export class Utils { export class Utils {
/** /**

View file

@ -1,4 +1,4 @@
import * as used_languages from "../assets/generated/used_languages.json" import used_languages from "../assets/generated/used_languages.json"
export default class LanguageUtils { export default class LanguageUtils {
/** /**

View file

@ -192,7 +192,7 @@
"fr": "Pilier.", "fr": "Pilier.",
"de": "Überflurhydrant.", "de": "Überflurhydrant.",
"it": "Soprasuolo.", "it": "Soprasuolo.",
"nl": "Pillaar type.", "nl": "Bovengrondse brandkraan.",
"es": "De pilar.", "es": "De pilar.",
"ca": "De pilar." "ca": "De pilar."
}, },
@ -257,7 +257,7 @@
"fr": "Enterré.", "fr": "Enterré.",
"de": "Unterflurhydrant.", "de": "Unterflurhydrant.",
"it": "Sottosuolo.", "it": "Sottosuolo.",
"nl": "Ondergronds type.", "nl": "Ondergrondse brandkraan.",
"ca": "Subterrani.", "ca": "Subterrani.",
"es": "Bajo tierra." "es": "Bajo tierra."
}, },
@ -361,6 +361,29 @@
"nl": "Pijpdiameter:{canonical(fire_hydrant:diameter)}" "nl": "Pijpdiameter:{canonical(fire_hydrant:diameter)}"
} }
}, },
{
"id": "hydrant-number-of-couplings",
"question": {
"en": "How many couplings does this fire hydrant have?",
"de": "Wie viele Kupplungen hat dieser Hydrant?",
"nl": "Hoe veel koppelingen bezit deze brandkraan?",
"ca": "Quants acoblaments té aquest hidrant?"
},
"freeform": {
"key": "couplings",
"placeholder": {
"en": "Number of couplings",
"de": "Anzahl der Kupplungen",
"nl": "Aantal koppelingen"
},
"type": "int"
},
"render": {
"en": "Number of couplings: {couplings}",
"de": "Anzahl der Kupplungen: {couplings}",
"nl": "Aantal koppelingen: {couplings}"
}
},
{ {
"id": "hydrant-couplings", "id": "hydrant-couplings",
"question": { "question": {
@ -526,4 +549,4 @@
] ]
} }
] ]
} }

View file

@ -62,6 +62,118 @@
} }
] ]
}, },
{
"id": "translations-title",
"group": "translations",
"render": "<h3>Translating MapComplete</h3>"
},
{
"group": "translations",
"id": "translation-mode",
"question": {
"en": "Do you want to help translating MapComplete?"
},
"mappings": [
{
"if": "mapcomplete-translation-mode=false",
"then": {
"en": "Don't show a button to quickly change translations"
}
},
{
"if": "mapcomplete-translation-mode=true",
"then": {
"en": "Show a button to quickly open translations when using MapComplete on a big screen"
}
},
{
"if": "mapcomplete-translation-mode=mobile",
"then": {
"en": "Always show the translation buttons, including on mobile"
}
}
]
},
{
"group": "translations",
"id": "translation-help",
"mappings": [
{
"if": {
"or": [
"mapcomplete-translation-mode=yes",
"mapcomplete-translation-mode=true",
"mapcomplete-translation-mode=mobile"
]
},
"then": {
"ca": "Fes clic a la icona 'tradueix' al costat d'una cadena per introduir o actualitzar un fragment de text. Necessites un compte de Weblate per a això. Crea'n un amb el teu nom d'usuari OSM per desbloquejar automàticament el mode de traducció.",
"da": "Klik på 'oversæt'-ikonet ved siden af en streng for at indtaste eller opdatere et stykke tekst. Du skal have en Weblate-konto for at kunne gøre dette. Opret en med dit OSM-brugernavn for automatisk at låse oversættelsestilstanden op.",
"de": "Klicken Sie auf das Übersetzungssymbol neben einer Zeichenfolge, um den Übersetzungstext einzugeben oder zu aktualisieren. Dazu benötigen Sie ein Weblate-Konto. Erstellen Sie eines mit Ihrem OSM-Benutzernamen, um den Übersetzungsmodus automatisch freizuschalten.",
"en": "Click the 'translate'-icon next to a string to enter or update a piece of text. You need a Weblate-account for this. Create one with your OSM-username to automatically unlock translation mode.",
"es": "Haz clic en el icono 'traducir' al lado de una cadena para introducir o actualizar un texto. Necesitas una cuenta de Weblate para esto. Crea una con tu usuario de OSM para desbloquear el modo de traducción automáticamente.",
"fr": "Cliquez sur l'icône \"traduire\" à côté d'une chaîne de caractères pour saisir ou mettre à jour la chaine de texte. Vous aurez besoin d'un compte Weblate pour cela. Créez-en un avec votre nom d'utilisateur OSM pour déverrouiller automatiquement le mode traduction.",
"nl": "Klik op het 'vertaal'-icoontje die naast een stukje tekst staat om deze tekst te vertalen of aan te passen. Hiervoor heb je een (gratis) Weblate-account nodig. Indien je jouw account maakt met dezelfde naam als je OSM-gebruikersnaam, dan zullen de vertaalknoppen automatisch verschijnen.",
"zh_Hant": "點字串旁邊的 'translate'-icon 來輸入或是更新一段文字。你需要 Weblate 帳號。用你 OSM 帳號名稱來創建帳號,並且自動解鎖翻譯模式。"
},
"icon": "translate"
}
]
},
{
"group": "translations",
"id": "translation-completeness",
"render": {
"ca": "Les traduccions de {_theme} en {_language} tenen un {_translation_percentage}%: {_translation_translated_count} cadenes de {_translation_total} estan traduïdes",
"da": "Oversættelser for {_theme} i {_language} er på {_translation_percentage}%: {_translation_translated_count} strenge ud af {_translation_total} er oversat",
"de": "Die Übersetzung für {_theme} in {_language} ist zu {_translation_percentage}% vollständig: {_translation_translated_count} Zeichenfolgen von {_translation_total} sind übersetzt",
"en": "Translations for {_theme} in {_language} are at {_translation_percentage}%: {_translation_translated_count} strings out of {_translation_total} are translated",
"es": "Las traducciones para {_theme} en {_language} están al {_translation_percentage}%: {_translation_translated_count} cadenas de {_translation_total} están traducidas",
"id": "Terjemahan untuk {_theme} dalam {_language} masih {_translation_percentage}%: {_translation_translated_count} string dari {_translation_total} diterjemahkan",
"nb_NO": "Oversettelsen for {_theme} i {_language} har {_translation_percentage}% dekning: {_translation_translated_count} strenger av {_translation_total} har blitt oversatt",
"nl": "Vertalingen voor {_theme} in {_language} zijn momenteel op {_translation_percentage}%: van {_translation_total} teksten zijn er reeds {_translation_translated_count} vertaald",
"zh_Hant": "{_theme} 的 {_language} 翻譯目前是 {_translation_percentage}%{_translation_total} 中的 {_translation_translated_count} 已經翻譯了"
},
"condition": {
"or": [
"mapcomplete-translation-mode=yes",
"mapcomplete-translation-mode=true",
"mapcomplete-translation-mode=mobile"
]
},
"mappings": [
{
"if": "_translation_percentage=100",
"icon": "confirm",
"then": {
"en": "Completely translated",
"nl": "Volledig vertaald"
}
}
]
},
{
"id": "translation-links",
"group": "translations",
"condition": {
"and": [
"_translation_links~*",
{
"or": [
"mapcomplete-translation-mode=true",
"mapcomplete-translation-mode=mobile"
]
}
]
},
"render": {
"special": {
"type": "multi",
"key": "_translation_links",
"tagrendering": "<a href='{link}' target='_blank'>Translate entries of {id}</a>"
}
}
},
{ {
"id": "verified-mastodon", "id": "verified-mastodon",
"mappings": [ "mappings": [
@ -85,6 +197,18 @@
} }
] ]
}, },
{
"id": "cscount-thanks",
"mappings": [
{
"if": "_csCount>0",
"then": {
"en": "You have made changes on {_csCount} different occasions! That is awesome!"
},
"icon": "party"
}
]
},
{ {
"id": "translation-thanks", "id": "translation-thanks",
"mappings": [ "mappings": [
@ -110,15 +234,43 @@
"de": "Sie haben Code zu MapComplete mit {_code_contributions} Commits beigetragen! Das ist großartig!", "de": "Sie haben Code zu MapComplete mit {_code_contributions} Commits beigetragen! Das ist großartig!",
"nl": "Je hebt mee geprogrammeerd aan MapComplete met {_code_contributions} commits! Das supercool van je! Bedankt hiervoor!" "nl": "Je hebt mee geprogrammeerd aan MapComplete met {_code_contributions} commits! Das supercool van je! Bedankt hiervoor!"
}, },
"icon": "party" "icon": "party",
"hideInAnswer": true
}
]
},
{
"id": "show_debug",
"question": {
"en": "Show user settings debug info?"
},
"mappings": [
{
"if": "mapcomplete-show_debug=yes",
"then": {
"en": "Show debug info"
}
},
{
"if": "mapcomplete-show_debug=no",
"then": {
"en": "Don't show debug info"
}
},
{
"if": "mapcomplete-show_debug=",
"then": {
"en": "Don't show debug info"
},
"hideInAnswer": true
} }
] ]
}, },
{ {
"id": "debug", "id": "debug",
"condition": "_name=Pieter Vander Vennet", "condition": "mapcomplete-show_debug=yes",
"render": "{all_tags()}" "render": "{all_tags()}"
} }
], ],
"mapRendering": null "mapRendering": null
} }

View file

@ -31,6 +31,65 @@
"physiotherapist", "physiotherapist",
"dentist", "dentist",
"hospital", "hospital",
"pharmacy" "pharmacy",
{
"builtin": "shops",
"override": {
"id": "medical-shops",
"minzoom": 13,
"=filter": [
"open_now",
"accepts_cash",
"accepts_cards"
],
"=presets": [
{
"title": {
"en": "a medical supply shop"
},
"tags": [
"shop=medical_supply"
]
},
{
"title": {
"en": "a hearing aids shop"
},
"tags": [
"shop=hearing_aids"
]
},
{
"title": {
"en": "an optician"
},
"tags": [
"shop=optician"
]
}
],
"source": {
"osmTags": {
"and+": [
{
"or": [
"shop=medical_supply",
"shop=hearing_aids",
"shop=optician"
]
}
]
}
}
}
},
{
"builtin": "shops",
"=presets": [],
"=name": null,
"override": {
"minzoom": 19
}
}
] ]
} }

View file

@ -102,7 +102,7 @@
}, },
{ {
"id": "uk_addresses_embedding_outline", "id": "uk_addresses_embedding_outline",
"render": "<b>Warning: </b>This point lies within a building or area for which we already have an address. You should only add this address if it is different. <br>The number and street name we have for the <a href='#{_embedding_object:id}' target='blank'>existing address</a> is <b>{_embedding_object:addr:housenumber} {_embedding_object:addr:street}</b>", "render": "<b>Warning: </b>This point lies within a building or area for which we already have an address. You should only add this address if it is different. <br>The number and street name we have for the <a href='#{_embedding_object:id}'>existing address</a> is <b>{_embedding_object:addr:housenumber} {_embedding_object:addr:street}</b>",
"mappings": [ "mappings": [
{ {
"if": "_embedding_object:id=true", "if": "_embedding_object:id=true",
@ -681,4 +681,4 @@
"enableShareScreen": false, "enableShareScreen": false,
"enableMoreQuests": false, "enableMoreQuests": false,
"credits": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett" "credits": "Pieter Vander Vennet, Rob Nickerson, Russ Garrett"
} }

View file

@ -862,6 +862,10 @@ video {
margin-bottom: 6rem; margin-bottom: 6rem;
} }
.ml-1 {
margin-left: 0.25rem;
}
.mb-2 { .mb-2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -910,10 +914,6 @@ video {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.ml-1 {
margin-left: 0.25rem;
}
.mb-1 { .mb-1 {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
@ -1027,6 +1027,10 @@ video {
height: 50%; height: 50%;
} }
.h-3 {
height: 0.75rem;
}
.h-screen { .h-screen {
height: 100vh; height: 100vh;
} }
@ -1047,10 +1051,6 @@ video {
height: 0px; height: 0px;
} }
.h-3 {
height: 0.75rem;
}
.h-48 { .h-48 {
height: 12rem; height: 12rem;
} }
@ -1115,6 +1115,10 @@ video {
width: 0px; width: 0px;
} }
.w-3 {
width: 0.75rem;
}
.w-screen { .w-screen {
width: 100vw; width: 100vw;
} }
@ -1422,11 +1426,6 @@ video {
border-color: rgb(0 0 0 / var(--tw-border-opacity)); border-color: rgb(0 0 0 / var(--tw-border-opacity));
} }
.border-gray-400 {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
}
.border-gray-300 { .border-gray-300 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -1437,6 +1436,11 @@ video {
border-color: rgb(252 165 165 / var(--tw-border-opacity)); border-color: rgb(252 165 165 / var(--tw-border-opacity));
} }
.border-gray-400 {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
}
.border-blue-500 { .border-blue-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity)); border-color: rgb(59 130 246 / var(--tw-border-opacity));

View file

@ -60,7 +60,7 @@ new Combine([
.SetClass("link-underline small") .SetClass("link-underline small")
.onClick(() => { .onClick(() => {
localStorage.clear() localStorage.clear()
window.location.reload(true) window.location.reload()
}), }),
]).AttachTo("centermessage") // Add an initialization and reset button if something goes wrong ]).AttachTo("centermessage") // Add an initialization and reset button if something goes wrong

View file

@ -33,9 +33,9 @@ ShowOverlayLayerImplementation.Implement();
// Miscelleanous // Miscelleanous
Utils.DisableLongPresses() Utils.DisableLongPresses()
if(new URLSearchParams(window.location.search).get("test") === "true"){ if(new URLSearchParams(window.location.search).get("test") === "true"){
console.log(themeConfig["default"]) console.log(themeConfig)
} }
const layoutToUse = new LayoutConfig(themeConfig["default"]) const layoutToUse = new LayoutConfig(themeConfig)
// Workaround/legacy to keep the old paramters working as I renamed some of them // Workaround/legacy to keep the old paramters working as I renamed some of them

View file

@ -586,10 +586,6 @@
}, },
"translations": { "translations": {
"activateButton": "Ajudar a traduir MapComplete", "activateButton": "Ajudar a traduir MapComplete",
"completeness": "Les traduccions de {theme} en {language} tenen un {percentage}%: {translated} cadenes de {total} estan traduïdes",
"deactivate": "Deshabilitar els botons de traducció",
"help": "Fes clic a la icona 'tradueix' al costat d'una cadena per introduir o actualitzar un fragment de text. Necessites un compte de Weblate per a això. Crea'n un amb el teu nom d'usuari OSM per desbloquejar automàticament el mode de traducció.",
"isTranslator": "El mode de traducció està actiu, ja que el vostre nom d'usuari coincideix amb el nom d'un traductor anterior",
"missing": "{count} cadenes sense traduir", "missing": "{count} cadenes sense traduir",
"notImmediate": "Les traduccions no s'actualitzen directament. Això sol trigar uns quants dies" "notImmediate": "Les traduccions no s'actualitzen directament. Això sol trigar uns quants dies"
}, },

View file

@ -803,10 +803,6 @@
"translations": { "translations": {
"activateButton": "Hjælp med at oversætte MapComplete", "activateButton": "Hjælp med at oversætte MapComplete",
"allMissing": "Ingen oversættelser endnu", "allMissing": "Ingen oversættelser endnu",
"completeness": "Oversættelser for {theme} i {language} er på {percentage}%: {translated} strenge ud af {total} er oversat",
"deactivate": "Slå oversættelsesknapper fra",
"help": "Klik på 'oversæt'-ikonet ved siden af en streng for at indtaste eller opdatere et stykke tekst. Du skal have en Weblate-konto for at kunne gøre dette. Opret en med dit OSM-brugernavn for automatisk at låse oversættelsestilstanden op.",
"isTranslator": "Oversættelsestilstanden er slået til, da dit brugernavn svarer til navnet på en tidligere oversætter",
"missing": "{count} uoversatte strenge", "missing": "{count} uoversatte strenge",
"notImmediate": "Oversættelser opdateres ikke direkte. Det tager typisk et par dage" "notImmediate": "Oversættelser opdateres ikke direkte. Det tager typisk et par dage"
}, },

View file

@ -928,10 +928,6 @@
"translations": { "translations": {
"activateButton": "MapComplete übersetzen", "activateButton": "MapComplete übersetzen",
"allMissing": "Noch keine Übersetzungen", "allMissing": "Noch keine Übersetzungen",
"completeness": "Die Übersetzung für {theme} in {language} ist zu {percentage}% vollständig: {translated} Zeichenfolgen von {total} sind übersetzt",
"deactivate": "Übersetzungssymbol ausblenden",
"help": "Klicken Sie auf das Übersetzungssymbol neben einer Zeichenfolge, um den Übersetzungstext einzugeben oder zu aktualisieren. Dazu benötigen Sie ein Weblate-Konto. Erstellen Sie eines mit Ihrem OSM-Benutzernamen, um den Übersetzungsmodus automatisch freizuschalten.",
"isTranslator": "Der Übersetzungsmodus ist aktiv, da Ihr Benutzername mit dem Namen eines früheren Übersetzers übereinstimmt",
"missing": "{count} nicht übersetzte Zeichenfolgen", "missing": "{count} nicht übersetzte Zeichenfolgen",
"notImmediate": "Die Übersetzung wird nicht direkt aktualisiert. Dies dauert in der Regel ein paar Tage" "notImmediate": "Die Übersetzung wird nicht direkt aktualisiert. Dies dauert in der Regel ein paar Tage"
}, },

View file

@ -746,6 +746,7 @@
"createNote": "Create a new note", "createNote": "Create a new note",
"createNoteIntro": "Is something wrong or missing on the map? Create a note here. These will be checked by volunteers.", "createNoteIntro": "Is something wrong or missing on the map? Create a note here. These will be checked by volunteers.",
"createNoteTitle": "Create a new note here", "createNoteTitle": "Create a new note here",
"creating": "Creating note...",
"disableAllNoteFilters": "Disable all filters", "disableAllNoteFilters": "Disable all filters",
"isClosed": "This note is resolved", "isClosed": "This note is resolved",
"isCreated": "Your note has been created!", "isCreated": "Your note has been created!",
@ -930,10 +931,6 @@
"translations": { "translations": {
"activateButton": "Help translate MapComplete", "activateButton": "Help translate MapComplete",
"allMissing": "No translations yet", "allMissing": "No translations yet",
"completeness": "Translations for {theme} in {language} are at {percentage}%: {translated} strings out of {total} are translated",
"deactivate": "Disable translation buttons",
"help": "Click the 'translate'-icon next to a string to enter or update a piece of text. You need a Weblate-account for this. Create one with your OSM-username to automatically unlock translation mode.",
"isTranslator": "Translation mode is active as your username matches the name of a previous translator",
"missing": "{count} untranslated strings", "missing": "{count} untranslated strings",
"notImmediate": "Translations are not updated directly. This typically takes a few days" "notImmediate": "Translations are not updated directly. This typically takes a few days"
}, },

View file

@ -708,10 +708,6 @@
"translations": { "translations": {
"activateButton": "Ayuda a traducir MapComplete", "activateButton": "Ayuda a traducir MapComplete",
"allMissing": "Aún sin traducciónes", "allMissing": "Aún sin traducciónes",
"completeness": "Las traducciones para {theme} en {language} están al {percentage}%: {translated} cadenas de {total} están traducidas",
"deactivate": "Deshabilitar los botones de traducción",
"help": "Haz clic en el icono 'traducir' al lado de una cadena para introducir o actualizar un texto. Necesitas una cuenta de Weblate para esto. Crea una con tu usuario de OSM para desbloquear el modo de traducción automáticamente.",
"isTranslator": "El modo de traducción está activo si tu nombre de usuario coincide con el nombre de un traductor anterior",
"missing": "{count} cadenas sin traducir", "missing": "{count} cadenas sin traducir",
"notImmediate": "Las traducciones no se actualizan directamente. Habitualmente esto lleva unos días" "notImmediate": "Las traducciones no se actualizan directamente. Habitualmente esto lleva unos días"
}, },

View file

@ -473,9 +473,6 @@
}, },
"translations": { "translations": {
"activateButton": "Aidez à traduire MapComplete", "activateButton": "Aidez à traduire MapComplete",
"deactivate": "Désactiver les boutons de traduction",
"help": "Cliquez sur l'icône \"traduire\" à côté d'une chaîne de caractères pour saisir ou mettre à jour la chaine de texte. Vous aurez besoin d'un compte Weblate pour cela. Créez-en un avec votre nom d'utilisateur OSM pour déverrouiller automatiquement le mode traduction.",
"isTranslator": "Mode traduction activé, votre pseudo correspond à celui dune personne de léquipe de traduction",
"missing": "{count} segments non traduits" "missing": "{count} segments non traduits"
}, },
"userinfo": { "userinfo": {

View file

@ -265,8 +265,6 @@
}, },
"translations": { "translations": {
"allMissing": "Belum ada terjemahan", "allMissing": "Belum ada terjemahan",
"completeness": "Terjemahan untuk {theme} dalam {language} masih {percentage}%: {translated} string dari {total} diterjemahkan",
"isTranslator": "Mode terjemahan aktif karena nama pengguna Anda cocok dengan nama penerjemah sebelumnya",
"notImmediate": "Terjemahan tidak diperbarui secara langsung. Biasanya memakan waktu beberapa hari" "notImmediate": "Terjemahan tidak diperbarui secara langsung. Biasanya memakan waktu beberapa hari"
}, },
"validation": { "validation": {

View file

@ -689,9 +689,6 @@
"translations": { "translations": {
"activateButton": "Bistå oversettelsen av MapComplete", "activateButton": "Bistå oversettelsen av MapComplete",
"allMissing": "Ingen oversettelser enda", "allMissing": "Ingen oversettelser enda",
"completeness": "Oversettelsen for {theme} i {language} har {percentage}% dekning: {translated} strenger av {total} har blitt oversatt",
"deactivate": "Skru av oversettelsesknapper",
"isTranslator": "Oversettelsesmodus er aktivt siden brukernavnet ditt samsvarer med navnet på forrige oversetter",
"missing": "{count} uoversatte strenger", "missing": "{count} uoversatte strenger",
"notImmediate": "Oversettelser oppdateres ikke direkte. Dette tar typisk et par dager." "notImmediate": "Oversettelser oppdateres ikke direkte. Dette tar typisk et par dager."
}, },

View file

@ -928,10 +928,6 @@
"translations": { "translations": {
"activateButton": "Help met het vertalen van MapComplete", "activateButton": "Help met het vertalen van MapComplete",
"allMissing": "Nog geen vertalingen", "allMissing": "Nog geen vertalingen",
"completeness": "Vertalingen voor {theme} in {language} zijn momenteel op {percentage}%: van {total} teksten zijn er reeds {translated} vertaald",
"deactivate": "Verberg de vertaalknoppen",
"help": "Klik op het 'vertaal'-icoontje die naast een stukje tekst staat om deze tekst te vertalen of aan te passen. Hiervoor heb je een (gratis) Weblate-account nodig. Indien je jouw account maakt met dezelfde naam als je OSM-gebruikersnaam, dan zullen de vertaalknoppen automatisch verschijnen.",
"isTranslator": "Vertaalmode is actief: je gebruikersnaam is dezelfde als van een vertaler. We gaan er dus vanuit dat jij die vertaler bent",
"missing": "{count} niet-vertaalde teksten", "missing": "{count} niet-vertaalde teksten",
"notImmediate": "Vertalingen worden niet onmiddelijk geupdate. Dit duurt gemiddeld enkele dagen" "notImmediate": "Vertalingen worden niet onmiddelijk geupdate. Dit duurt gemiddeld enkele dagen"
}, },

View file

@ -189,8 +189,6 @@
"translations": { "translations": {
"activateButton": "Pomóż przetłumaczyć MapComplete", "activateButton": "Pomóż przetłumaczyć MapComplete",
"allMissing": "Brak tłumaczeń", "allMissing": "Brak tłumaczeń",
"deactivate": "Wyłącz przyciski tłumaczenia",
"isTranslator": "Tryb tłumaczenia jest aktywny, ponieważ Twoja nazwa użytkownika odpowiada nazwisku poprzedniego tłumacza",
"notImmediate": "Tłumaczenia nie są aktualizowane bezpośrednio. Zwykle trwa to kilka dni" "notImmediate": "Tłumaczenia nie są aktualizowane bezpośrednio. Zwykle trwa to kilka dni"
}, },
"userinfo": { "userinfo": {

View file

@ -720,10 +720,6 @@
"translations": { "translations": {
"activateButton": "協助翻譯 MapComplete", "activateButton": "協助翻譯 MapComplete",
"allMissing": "還沒有翻譯", "allMissing": "還沒有翻譯",
"completeness": "{theme} 的 {language} 翻譯目前是 {percentage}%{total} 中的 {translated} 已經翻譯了",
"deactivate": "關閉翻譯按鈕",
"help": "點字串旁邊的 'translate'-icon 來輸入或是更新一段文字。你需要 Weblate 帳號。用你 OSM 帳號名稱來創建帳號,並且自動解鎖翻譯模式。",
"isTranslator": "翻譯模式已經啟用,你的名字符合前一位翻譯者的名字",
"missing": "{count} 未翻譯字串", "missing": "{count} 未翻譯字串",
"notImmediate": "翻譯不會直接更新,通常會需要幾天時間" "notImmediate": "翻譯不會直接更新,通常會需要幾天時間"
}, },

View file

@ -1,4 +1,3 @@
import * as languages from "../assets/generated/used_languages.json"
import { readFileSync, writeFileSync } from "fs" import { readFileSync, writeFileSync } from "fs"
/** /**

View file

@ -78,13 +78,13 @@ function geoJsonName(targetDir: string, x: number, y: number, z: number): string
return targetDir + "_" + z + "_" + x + "_" + y + ".geojson" return targetDir + "_" + z + "_" + x + "_" + y + ".geojson"
} }
/// Downloads the given feature and saves them to disk /// Downloads the given tilerange from overpass and saves them to disk
async function downloadRaw( async function downloadRaw(
targetdir: string, targetdir: string,
r: TileRange, r: TileRange,
theme: LayoutConfig, theme: LayoutConfig,
relationTracker: RelationsTracker relationTracker: RelationsTracker
) /* : {failed: number, skipped :number} */ { ): Promise<{ failed: number; skipped: number }> {
let downloaded = 0 let downloaded = 0
let failed = 0 let failed = 0
let skipped = 0 let skipped = 0

View file

@ -15,14 +15,15 @@ import List from "../UI/Base/List"
import SharedTagRenderings from "../Customizations/SharedTagRenderings" import SharedTagRenderings from "../Customizations/SharedTagRenderings"
import { writeFile } from "fs" import { writeFile } from "fs"
import Translations from "../UI/i18n/Translations" import Translations from "../UI/i18n/Translations"
import * as themeOverview from "../assets/generated/theme_overview.json" import themeOverview from "../assets/generated/theme_overview.json"
import DefaultGUI from "../UI/DefaultGUI" import DefaultGUI from "../UI/DefaultGUI"
import FeaturePipelineState from "../Logic/State/FeaturePipelineState" import FeaturePipelineState from "../Logic/State/FeaturePipelineState"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import * as bookcases from "../assets/generated/themes/bookcases.json" import bookcases from "../assets/generated/themes/bookcases.json"
import { DefaultGuiState } from "../UI/DefaultGuiState" import { DefaultGuiState } from "../UI/DefaultGuiState"
import * as fakedom from "fake-dom" import fakedom from "fake-dom"
import Hotkeys from "../UI/Base/Hotkeys" import Hotkeys from "../UI/Base/Hotkeys"
import { QueryParameters } from "../Logic/Web/QueryParameters"
function WriteFile( function WriteFile(
filename, filename,
html: BaseUIElement, html: BaseUIElement,
@ -103,7 +104,7 @@ function generateWikipage() {
"! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" + "! Name, link !! Genre !! Covered region !! Language !! Description !! Free materials !! Image\n" +
"|-" "|-"
for (const layout of themeOverview["default"] ?? themeOverview) { for (const layout of themeOverview) {
if (layout.hideFromOverview) { if (layout.hideFromOverview) {
continue continue
} }
@ -225,6 +226,12 @@ WriteFile("./Docs/URL_Parameters.md", QueryParameterDocumentation.GenerateQueryP
if (fakedom === undefined || window === undefined) { if (fakedom === undefined || window === undefined) {
throw "FakeDom not initialized" throw "FakeDom not initialized"
} }
QueryParameters.GetQueryParameter(
"mode",
"map",
"The mode the application starts in, e.g. 'map', 'dashboard' or 'statistics'"
)
new DefaultGUI( new DefaultGUI(
new FeaturePipelineState(new LayoutConfig(<any>bookcases)), new FeaturePipelineState(new LayoutConfig(<any>bookcases)),
new DefaultGuiState() new DefaultGuiState()

View file

@ -1,6 +1,6 @@
import ScriptUtils from "./ScriptUtils" import ScriptUtils from "./ScriptUtils"
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs" import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs"
import * as licenses from "../assets/generated/license_info.json" import licenses from "../assets/generated/license_info.json"
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import Constants from "../Models/Constants" import Constants from "../Models/Constants"
@ -14,8 +14,8 @@ import {
} from "../Models/ThemeConfig/Conversion/Validation" } from "../Models/ThemeConfig/Conversion/Validation"
import { Translation } from "../UI/i18n/Translation" import { Translation } from "../UI/i18n/Translation"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson" import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import * as questions from "../assets/tagRenderings/questions.json" import questions from "../assets/tagRenderings/questions.json"
import * as icons from "../assets/tagRenderings/icons.json" import icons from "../assets/tagRenderings/icons.json"
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson" import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"
import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer" import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme" import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
@ -155,7 +155,7 @@ class LayerOverviewUtils {
const dict = new Map<string, TagRenderingConfigJson>() const dict = new Map<string, TagRenderingConfigJson>()
const validator = new ValidateTagRenderings(undefined, doesImageExist) const validator = new ValidateTagRenderings(undefined, doesImageExist)
for (const key in questions["default"]) { for (const key in questions) {
if (key === "id") { if (key === "id") {
continue continue
} }
@ -168,7 +168,7 @@ class LayerOverviewUtils {
) )
dict.set(key, config) dict.set(key, config)
} }
for (const key in icons["default"]) { for (const key in icons) {
if (key === "id") { if (key === "id") {
continue continue
} }

View file

@ -2,7 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFile, writeFi
import Locale from "../UI/i18n/Locale" import Locale from "../UI/i18n/Locale"
import Translations from "../UI/i18n/Translations" import Translations from "../UI/i18n/Translations"
import { Translation } from "../UI/i18n/Translation" import { Translation } from "../UI/i18n/Translation"
import * as all_known_layouts from "../assets/generated/known_layers_and_themes.json" import all_known_layouts from "../assets/generated/known_layers_and_themes.json"
import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson" import { LayoutConfigJson } from "../Models/ThemeConfig/Json/LayoutConfigJson"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import xml2js from "xml2js" import xml2js from "xml2js"
@ -23,7 +23,7 @@ async function createIcon(iconPath: string, size: number, alreadyWritten: string
name = name.substr(2) name = name.substr(2)
} }
const newname = `assets/generated/images/${name.replace(/\//g, "_")}${size}.png` const newname = `public/assets/generated/images/${name.replace(/\//g, "_")}${size}.png`
if (alreadyWritten.indexOf(newname) >= 0) { if (alreadyWritten.indexOf(newname) >= 0) {
return newname return newname
@ -60,7 +60,7 @@ async function createSocialImage(layout: LayoutConfig, template: "" | "Wide"): P
) )
return undefined return undefined
} }
const path = `./assets/generated/images/social_image_${layout.id}_${template}.svg` const path = `./public/assets/generated/images/social_image_${layout.id}_${template}.svg`
if (existsSync(path)) { if (existsSync(path)) {
return path return path
} }
@ -121,7 +121,7 @@ async function createManifest(
// This is an svg. Lets create the needed pngs and do some checkes! // This is an svg. Lets create the needed pngs and do some checkes!
const whiteBackgroundPath = const whiteBackgroundPath =
"./assets/generated/images/theme_" + layout.id + "_white_background.svg" "./public/assets/generated/images/theme_" + layout.id + "_white_background.svg"
{ {
const svg = await ScriptUtils.ReadSvg(icon) const svg = await ScriptUtils.ReadSvg(icon)
const width: string = svg.$.width const width: string = svg.$.width
@ -136,7 +136,7 @@ async function createManifest(
let path = layout.icon let path = layout.icon
if (layout.icon.startsWith("<")) { if (layout.icon.startsWith("<")) {
// THis is already the svg // THis is already the svg
path = "./assets/generated/images/" + layout.id + "_logo.svg" path = "./public/assets/generated/images/" + layout.id + "_logo.svg"
writeFileSync(path, layout.icon) writeFileSync(path, layout.icon)
} }
@ -235,7 +235,7 @@ async function createLandingPage(layout: LayoutConfig, manifest, whiteIcons, alr
let icon = layout.icon let icon = layout.icon
if (icon.startsWith("<?xml") || icon.startsWith("<svg")) { if (icon.startsWith("<?xml") || icon.startsWith("<svg")) {
// This already is an svg // This already is an svg
icon = `./assets/generated/images/${layout.id}_icon.svg` icon = `./public/assets/generated/images/${layout.id}_icon.svg`
writeFileSync(icon, layout.icon) writeFileSync(icon, layout.icon)
} }
@ -295,7 +295,7 @@ async function createIndexFor(theme: LayoutConfig) {
const filename = "index_" + theme.id + ".ts" const filename = "index_" + theme.id + ".ts"
writeFileSync( writeFileSync(
filename, filename,
`import * as themeConfig from "./assets/generated/themes/${theme.id}.json"\n` `import themeConfig from "./assets/generated/themes/${theme.id}.json"\n`
) )
appendFileSync(filename, codeTemplate) appendFileSync(filename, codeTemplate)
} }
@ -311,7 +311,9 @@ async function main(): Promise<void> {
createDir("./assets/generated") createDir("./assets/generated")
createDir("./assets/generated/layers") createDir("./assets/generated/layers")
createDir("./assets/generated/themes") createDir("./assets/generated/themes")
createDir("./assets/generated/images") createDir("./public/assets/")
createDir("./public/assets/generated")
createDir("./public/assets/generated/images")
const blacklist = [ const blacklist = [
"", "",

View file

@ -1,4 +1,4 @@
import * as known_layers from "../assets/generated/known_layers.json" import known_layers from "../assets/generated/known_layers.json"
import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
import { TagUtils } from "../Logic/Tags/TagUtils" import { TagUtils } from "../Logic/Tags/TagUtils"
import { Utils } from "../Utils" import { Utils } from "../Utils"
@ -10,7 +10,7 @@ import Constants from "../Models/Constants"
async function main(includeTags = true) { async function main(includeTags = true) {
ScriptUtils.fixUtils() ScriptUtils.fixUtils()
const layers: LayerConfigJson[] = (known_layers["default"] ?? known_layers).layers const layers = <LayerConfigJson[]>known_layers.layers
const keysAndTags = new Map<string, Set<string>>() const keysAndTags = new Map<string, Set<string>>()

View file

@ -13,7 +13,7 @@ async function main(args: string[]) {
console.log("Removing translation string ", path, "from the general translations") console.log("Removing translation string ", path, "from the general translations")
const files = ScriptUtils.readDirRecSync("./langs", 1).filter((f) => f.endsWith(".json")) const files = ScriptUtils.readDirRecSync("./langs", 1).filter((f) => f.endsWith(".json"))
for (const file of files) { for (const file of files) {
const json = JSON.parse(fs.readFileSync(file, "UTF-8")) const json = JSON.parse(fs.readFileSync(file, { encoding: "utf-8" }))
Utils.WalkPath(path, json, (_) => undefined) Utils.WalkPath(path, json, (_) => undefined)
fs.writeFileSync(file, JSON.stringify(json, null, " ") + "\n") fs.writeFileSync(file, JSON.stringify(json, null, " ") + "\n")
} }

View file

@ -3,7 +3,7 @@
*/ */
import ScriptUtils from "../ScriptUtils" import ScriptUtils from "../ScriptUtils"
import { existsSync, readFileSync, writeFileSync } from "fs" import { existsSync, readFileSync, writeFileSync } from "fs"
import * as known_languages from "../../assets/language_native.json" import known_languages from "../../assets/language_native.json"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson" import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson" import { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import SmallLicense from "../../Models/smallLicense" import SmallLicense from "../../Models/smallLicense"

View file

@ -7,62 +7,86 @@ import { exec } from "child_process"
* @param reason * @param reason
* @private * @private
*/ */
function detectInCode(forbidden: string, reason: string) { function detectInCode(forbidden: string, reason: string): (done: () => void) => void {
const excludedDirs = [ return (done: () => void) => {
".git", const excludedDirs = [
"node_modules", ".git",
"dist", "node_modules",
".cache", "dist",
".parcel-cache", ".cache",
"assets", ".parcel-cache",
"vendor", "assets",
".idea/", "vendor",
] ".idea/",
]
exec( exec(
'grep -n "' + 'grep -n "' +
forbidden + forbidden +
'" -r . ' + '" -r . ' +
excludedDirs.map((d) => "--exclude-dir=" + d).join(" "), excludedDirs.map((d) => "--exclude-dir=" + d).join(" "),
(error, stdout, stderr) => { (error, stdout, stderr) => {
if (error?.message?.startsWith("Command failed: grep")) { if (error?.message?.startsWith("Command failed: grep")) {
console.warn("Command failed!") console.warn("Command failed!", error)
return return
} }
if (error !== null) { if (error !== null) {
throw error throw error
} }
if (stderr !== "") { if (stderr !== "") {
throw stderr throw stderr
} }
const found = stdout const found = stdout
.split("\n") .split("\n")
.filter((s) => s !== "") .filter((s) => s !== "")
.filter((s) => !s.startsWith("./test/")) .filter((s) => !s.startsWith("./test/"))
if (found.length > 0) { if (found.length > 0) {
throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}` const msg = `Found a '${forbidden}' at \n ${found.join(
"\n "
)}.\n ${reason}`
console.error(msg)
console.error(found.length, "issues found")
throw msg
}
done()
} }
} )
) }
} }
describe("Code quality", () => { describe("Code quality", () => {
it("should not contain reverse", () => { it(
"should not contain reverse",
detectInCode( detectInCode(
"reverse()", "reverse()",
"Reverse is stateful and changes the source list. This often causes subtle bugs" "Reverse is stateful and changes the source list. This often causes subtle bugs"
) )
}) )
it("should not contain 'constructor.name'", () => { it(
"should not contain 'constructor.name'",
detectInCode("constructor\\.name", "This is not allowed, as minification does erase names.") detectInCode("constructor\\.name", "This is not allowed, as minification does erase names.")
}) )
it("should not contain 'innerText'", () => { it(
"should not contain 'innerText'",
detectInCode( detectInCode(
"innerText", "innerText",
"innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead." "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead."
) )
}) )
it(
"should not contain 'import * as name from \"xyz.json\"'",
detectInCode(
'import \\* as [a-zA-Z0-9_]\\+ from \\"[.-_/a-zA-Z0-9]\\+\\.json\\"',
"With vite, json files have a default export. Use import name from file.json instead"
)
)
it(
"should not contain '[\"default\"]'",
detectInCode('\\[\\"default\\"\\]', "Possible leftover of faulty default import")
)
}) })