Merge branches

This commit is contained in:
Pieter Vander Vennet 2023-07-17 01:43:53 +02:00
commit 7eeac66471
554 changed files with 8193 additions and 7079 deletions

View file

@ -0,0 +1,211 @@
/**
* The part of the global state which initializes the feature switches, based on default values and on the layoutToUse
*/
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { UIEventSource } from "../UIEventSource"
import { QueryParameters } from "../Web/QueryParameters"
import Constants from "../../Models/Constants"
import { Utils } from "../../Utils"
class FeatureSwitchUtils {
static initSwitch(key: string, deflt: boolean, documentation: string): UIEventSource<boolean> {
const defaultValue = deflt
const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation)
// It takes the current layout, extracts the default value for this query parameter. A query parameter event source is then retrieved and flattened
return queryParam.sync(
(str) => (str === undefined ? defaultValue : str !== "false"),
[],
(b) => (b == defaultValue ? undefined : "" + b)
)
}
}
export class OsmConnectionFeatureSwitches {
public readonly featureSwitchFakeUser: UIEventSource<boolean>
public readonly featureSwitchApiURL: UIEventSource<string>
constructor() {
this.featureSwitchApiURL = QueryParameters.GetQueryParameter(
"backend",
"osm",
"The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'"
)
this.featureSwitchFakeUser = QueryParameters.GetBooleanQueryParameter(
"fake-user",
false,
"If true, 'dryrun' mode is activated and a fake user account is loaded"
)
}
}
export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
/**
* The layout that is being used in this run
*/
public readonly layoutToUse: LayoutConfig
public readonly featureSwitchUserbadge: UIEventSource<boolean>
public readonly featureSwitchSearch: UIEventSource<boolean>
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>
public readonly featureSwitchAddNew: UIEventSource<boolean>
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>
public readonly featureSwitchCommunityIndex: UIEventSource<boolean>
public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>
public readonly featureSwitchMoreQuests: UIEventSource<boolean>
public readonly featureSwitchShareScreen: UIEventSource<boolean>
public readonly featureSwitchGeolocation: UIEventSource<boolean>
public readonly featureSwitchIsTesting: UIEventSource<boolean>
public readonly featureSwitchIsDebugging: UIEventSource<boolean>
public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>
public readonly featureSwitchFilter: UIEventSource<boolean>
public readonly featureSwitchEnableExport: UIEventSource<boolean>
public readonly overpassUrl: UIEventSource<string[]>
public readonly overpassTimeout: UIEventSource<number>
public readonly overpassMaxZoom: UIEventSource<number>
public readonly osmApiTileSize: UIEventSource<number>
public readonly backgroundLayerId: UIEventSource<string>
public constructor(layoutToUse?: LayoutConfig) {
super()
this.layoutToUse = layoutToUse
// Helper function to initialize feature switches
this.featureSwitchUserbadge = FeatureSwitchUtils.initSwitch(
"fs-userbadge",
layoutToUse?.enableUserBadge ?? true,
"Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode."
)
this.featureSwitchSearch = FeatureSwitchUtils.initSwitch(
"fs-search",
layoutToUse?.enableSearch ?? true,
"Disables/Enables the search bar"
)
this.featureSwitchBackgroundSelection = FeatureSwitchUtils.initSwitch(
"fs-background",
layoutToUse?.enableBackgroundLayerSelection ?? true,
"Disables/Enables the background layer control"
)
this.featureSwitchFilter = FeatureSwitchUtils.initSwitch(
"fs-filter",
layoutToUse?.enableLayers ?? true,
"Disables/Enables the filter view"
)
this.featureSwitchAddNew = FeatureSwitchUtils.initSwitch(
"fs-add-new",
layoutToUse?.enableAddNewPoints ?? true,
"Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"
)
this.featureSwitchWelcomeMessage = FeatureSwitchUtils.initSwitch(
"fs-welcome-message",
true,
"Disables/enables the help menu or welcome message"
)
this.featureSwitchCommunityIndex = FeatureSwitchUtils.initSwitch(
"fs-community-index",
true,
"Disables/enables the button to get in touch with the community"
)
this.featureSwitchExtraLinkEnabled = FeatureSwitchUtils.initSwitch(
"fs-iframe-popout",
true,
"Disables/Enables the extraLink button. By default, if in iframe mode and the welcome message is hidden, a popout button to the full mapcomplete instance is shown instead (unless disabled with this switch or another extraLink button is enabled)"
)
this.featureSwitchMoreQuests = FeatureSwitchUtils.initSwitch(
"fs-more-quests",
layoutToUse?.enableMoreQuests ?? true,
"Disables/Enables the 'More Quests'-tab in the welcome message"
)
this.featureSwitchShareScreen = FeatureSwitchUtils.initSwitch(
"fs-share-screen",
layoutToUse?.enableShareScreen ?? true,
"Disables/Enables the 'Share-screen'-tab in the welcome message"
)
this.featureSwitchGeolocation = FeatureSwitchUtils.initSwitch(
"fs-geolocation",
layoutToUse?.enableGeolocation ?? true,
"Disables/Enables the geolocation button"
)
this.featureSwitchShowAllQuestions = FeatureSwitchUtils.initSwitch(
"fs-all-questions",
layoutToUse?.enableShowAllQuestions ?? false,
"Always show all questions"
)
this.featureSwitchEnableExport = FeatureSwitchUtils.initSwitch(
"fs-export",
layoutToUse?.enableExportButton ?? true,
"Enable the export as GeoJSON and CSV button"
)
let testingDefaultValue = false
if (
this.featureSwitchApiURL.data !== "osm-test" &&
!Utils.runningFromConsole &&
(location.hostname === "localhost" || location.hostname === "127.0.0.1")
) {
testingDefaultValue = true
}
this.featureSwitchIsTesting = QueryParameters.GetBooleanQueryParameter(
"test",
testingDefaultValue,
"If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org"
)
this.featureSwitchIsDebugging = QueryParameters.GetBooleanQueryParameter(
"debug",
false,
"If true, shows some extra debugging help such as all the available tags on every object"
)
this.overpassUrl = QueryParameters.GetQueryParameter(
"overpassUrl",
(layoutToUse?.overpassUrl ?? Constants.defaultOverpassUrls).join(","),
"Point mapcomplete to a different overpass-instance. Example: https://overpass-api.de/api/interpreter"
).sync(
(param) => param?.split(","),
[],
(urls) => urls?.join(",")
)
this.overpassTimeout = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(
"overpassTimeout",
"" + layoutToUse?.overpassTimeout,
"Set a different timeout (in seconds) for queries in overpass"
)
)
this.overpassMaxZoom = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(
"overpassMaxZoom",
"" + layoutToUse?.overpassMaxZoom,
" point to switch between OSM-api and overpass"
)
)
this.osmApiTileSize = UIEventSource.asFloat(
QueryParameters.GetQueryParameter(
"osmApiTileSize",
"" + layoutToUse?.osmApiTileSize,
"Tilesize when the OSM-API is used to fetch data within a BBOX"
)
)
this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => {
if (!userbadge) {
this.featureSwitchAddNew.setData(false)
}
})
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId ?? "osm",
"The id of the background layer to start with"
)
}
}

View file

@ -0,0 +1,153 @@
import { UIEventSource } from "../UIEventSource"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { QueryParameters } from "../Web/QueryParameters"
export type GeolocationPermissionState = "prompt" | "requested" | "granted" | "denied"
export interface GeoLocationPointProperties extends GeolocationCoordinates {
id: "gps"
"user:location": "yes"
date: string
}
/**
* An abstract representation of the current state of the geolocation.
*/
export class GeoLocationState {
/**
* What do we know about the current state of having access to the GPS?
* If 'prompt', then we just started and didn't request access yet
* 'requested' means the user tapped the 'locate me' button at least once
* 'granted' means that it is granted
* 'denied' means that we don't have access
*/
public readonly permission: UIEventSource<GeolocationPermissionState> = 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)
/**
* If true: the map will center (and re-center) to this location
*/
public readonly allowMoving: UIEventSource<boolean> = new UIEventSource<boolean>(true)
/**
* The latest GeoLocationCoordinates, as given by the WebAPI
*/
public readonly currentGPSLocation: UIEventSource<GeolocationCoordinates | undefined> =
new UIEventSource<GeolocationCoordinates | undefined>(undefined)
/**
* A small flag on localstorage. If the user previously granted the geolocation, it will be set.
* On firefox, the permissions api is broken (probably fingerprint resistiance) and "granted + don't ask again" doesn't stick between sessions.
*
* Instead, we set this flag. If this flag is set upon loading the page, we start geolocating immediately.
* If the user denies the geolocation this time, we unset this flag
* @private
*/
private readonly _previousLocationGrant: UIEventSource<"true" | "false"> = <any>(
LocalStorageSource.Get("geolocation-permissions")
)
/**
* Used to detect a permission retraction
*/
private readonly _grantedThisSession: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor() {
const self = this
this.permission.addCallbackAndRunD(async (state) => {
if (state === "granted") {
self._previousLocationGrant.setData("true")
self._grantedThisSession.setData(true)
}
if (state === "prompt" && self._grantedThisSession.data) {
// This is _really_ weird: we had a grant earlier, but it's 'prompt' now?
// This means that the rights have been revoked again!
// self.permission.setData("denied")
self._previousLocationGrant.setData("false")
self.permission.setData("denied")
self.currentGPSLocation.setData(undefined)
console.warn("Detected a downgrade in permissions!")
}
if (state === "denied") {
self._previousLocationGrant.setData("false")
}
})
console.log("Previous location grant:", this._previousLocationGrant.data)
if (this._previousLocationGrant.data === "true") {
// A previous visit successfully granted permission. Chance is high that we are allowed to use it again!
// 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")
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()
}
}
/**
* Installs the listener for updates
* @private
*/
private async startWatching() {
const self = this
navigator.geolocation.watchPosition(
function (position) {
self.currentGPSLocation.setData(position.coords)
self._previousLocationGrant.setData("true")
},
function () {
console.warn("Could not get location with navigator.geolocation")
},
{
enableHighAccuracy: true,
}
)
}
/**
* Requests the user to allow access to their position.
* When granted, will be written to the 'geolocationState'.
* This class will start watching
*/
public requestPermission() {
if (typeof navigator === "undefined") {
// Not compatible with this browser
this.permission.setData("denied")
return
}
if (this.permission.data !== "prompt" && this.permission.data !== "requested") {
// If the user denies the first prompt, revokes the deny and then tries again, we have to run the flow as well
// Hence that we continue the flow if it is "requested"
return
}
this.permission.setData("requested")
try {
navigator?.permissions
?.query({ name: "geolocation" })
.then((status) => {
console.log("Status update: received geolocation permission is ", status.state)
this.permission.setData(status.state)
const self = this
status.onchange = function () {
self.permission.setData(status.state)
}
this.permission.setData("requested")
// We _must_ call 'startWatching', as that is the actual trigger for the popup...
self.startWatching()
})
.catch((e) => console.error("Could not get geopermission", e))
} catch (e) {
console.error("Could not get permission:", e)
}
}
}

View file

@ -0,0 +1,119 @@
import { UIEventSource } from "../UIEventSource"
import { GlobalFilter } from "../../Models/GlobalFilter"
import FilteredLayer from "../../Models/FilteredLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { OsmConnection } from "../Osm/OsmConnection"
import { Tag } from "../Tags/Tag"
import Translations from "../../UI/i18n/Translations"
import { RegexTag } from "../Tags/RegexTag"
import { Or } from "../Tags/Or"
/**
* The layer state keeps track of:
* - Which layers are enabled
* - Which filters are used, including 'global' filters
*/
export default class LayerState {
/**
* Filters which apply onto all layers
*/
public readonly globalFilters: UIEventSource<GlobalFilter[]> = new UIEventSource(
[],
"globalFilters"
)
/**
* Which layers are enabled in the current theme and what filters are applied onto them
*/
public readonly filteredLayers: ReadonlyMap<string, FilteredLayer>
private readonly osmConnection: OsmConnection
/**
*
* @param osmConnection
* @param layers
* @param context: the context, probably the name of the theme. Used to disambiguate the upstream user preference
*/
constructor(osmConnection: OsmConnection, layers: LayerConfig[], context: string) {
this.osmConnection = osmConnection
const filteredLayers = new Map()
for (const layer of layers) {
filteredLayers.set(
layer.id,
FilteredLayer.initLinkedState(layer, context, this.osmConnection)
)
}
this.filteredLayers = filteredLayers
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
}
/**
* Sets the global filter which looks to the 'level'-tag.
* Only features with the given 'level' will be shown.
*
* If undefined is passed, _all_ levels will be shown
* @param level
*/
public setLevelFilter(level?: string) {
// Remove all previous
const l = this.globalFilters.data.length
this.globalFilters.data = this.globalFilters.data.filter((f) => f.id !== "level")
if (!level) {
if (l !== this.globalFilters.data.length) {
this.globalFilters.ping()
}
return
}
const t = Translations.t.general.levelSelection
const conditionsOrred = [
new Tag("level", "" + level),
new RegexTag("level", new RegExp("(.*;)?" + level + "(;.*)?")),
]
if (level === "0") {
conditionsOrred.push(new Tag("level", "")) // No level tag is the same as level '0'
}
console.log("Setting levels filter to", conditionsOrred)
this.globalFilters.data.push({
id: "level",
state: level,
osmTags: new Or(conditionsOrred),
onNewPoint: {
tags: [new Tag("level", level)],
icon: "./assets/svg/elevator.svg",
confirmAddNew: t.confirmLevel.PartialSubs({ level }),
safetyCheck: t.addNewOnLevel.Subs({ level }),
},
})
this.globalFilters.ping()
}
/**
* Some layers copy the filter state of another layer - this is quite often the case for 'sibling'-layers,
* (where two variations of the same layer are used, e.g. a specific type of shop on all zoom levels and all shops on high zoom).
*
* This methods links those states for the given layer
*/
private static linkFilterStates(
layer: LayerConfig,
filteredLayers: Map<string, FilteredLayer>
) {
if (layer.filterIsSameAs === undefined) {
return
}
const toReuse = filteredLayers.get(layer.filterIsSameAs)
if (toReuse === undefined) {
throw (
"Error in layer " +
layer.id +
": it defines that it should be use the filters of " +
layer.filterIsSameAs +
", but this layer was not loaded"
)
}
console.warn(
"Linking filter and isDisplayed-states of " + layer.id + " and " + layer.filterIsSameAs
)
const copy = new FilteredLayer(layer, toReuse.appliedFilters, toReuse.isDisplayed)
filteredLayers.set(layer.id, copy)
}
}

View file

@ -0,0 +1,386 @@
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../Osm/OsmConnection"
import { MangroveIdentity } from "../Web/MangroveReviews"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
import { FeatureSource } from "../FeatureSource/FeatureSource"
import { Feature } from "geojson"
import { Utils } from "../../Utils"
import translators from "../../assets/translators.json"
import codeContributors from "../../assets/contributors.json"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
import usersettings from "../../../src/assets/generated/layers/usersettings.json"
import Locale from "../../UI/i18n/Locale"
import LinkToWeblate from "../../UI/Base/LinkToWeblate"
import FeatureSwitchState from "./FeatureSwitchState"
import Constants from "../../Models/Constants"
/**
* The part of the state which keeps track of user-related stuff, e.g. the OSM-connection,
* which layers they enabled, ...
*/
export default class UserRelatedState {
public static readonly usersettingsConfig = UserRelatedState.initUserRelatedState()
public static readonly availableUserSettingsIds: string[] =
UserRelatedState.usersettingsConfig?.tagRenderings?.map((tr) => tr.id) ?? []
public static readonly SHOW_TAGS_VALUES = ["always", "yes", "full"] as const
/**
The user credentials
*/
public osmConnection: OsmConnection
/**
* The key for mangrove
*/
public readonly mangroveIdentity: MangroveIdentity
public readonly installedUserThemes: Store<string[]>
public readonly showAllQuestionsAtOnce: UIEventSource<boolean>
public readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
public readonly homeLocation: FeatureSource
public readonly language: UIEventSource<string>
/**
* The number of seconds that the GPS-locations are stored in memory.
* Time in seconds
*/
public readonly gpsLocationHistoryRetentionTime = new UIEventSource(
7 * 24 * 60 * 60,
"gps_location_retention"
)
/**
* Preferences as tags exposes many preferences and state properties as record.
* This is used to bridge the internal state with the usersettings.json layerconfig file
*/
public readonly preferencesAsTags: UIEventSource<Record<string, string>>
constructor(
osmConnection: OsmConnection,
availableLanguages?: string[],
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState
) {
this.osmConnection = osmConnection
{
const translationMode: UIEventSource<undefined | "true" | "false" | "mobile" | string> =
this.osmConnection.GetPreference("translation-mode", "false")
translationMode.addCallbackAndRunD((mode) => {
mode = mode.toLowerCase()
if (mode === "true" || mode === "yes") {
Locale.showLinkOnMobile.setData(false)
Locale.showLinkToWeblate.setData(true)
} else if (mode === "false" || mode === "no") {
Locale.showLinkToWeblate.setData(false)
} else if (mode === "mobile") {
Locale.showLinkOnMobile.setData(true)
Locale.showLinkToWeblate.setData(true)
} else {
Locale.showLinkOnMobile.setData(false)
Locale.showLinkToWeblate.setData(false)
}
})
}
this.showAllQuestionsAtOnce = UIEventSource.asBoolean(
this.osmConnection.GetPreference("show-all-questions", "false", {
documentation:
"Either 'true' or 'false'. If set, all questions will be shown all at once",
})
)
this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove")
)
this.language.addCallbackAndRunD((language) => Locale.language.setData(language))
this.installedUserThemes = this.InitInstalledUserThemes()
this.homeLocation = this.initHomeLocation()
this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches)
}
private static initUserRelatedState(): LayerConfig {
try {
return new LayerConfig(<LayerConfigJson>usersettings, "userinformationpanel")
} catch (e) {
return undefined
}
}
public GetUnofficialTheme(id: string):
| {
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
}
| undefined {
console.log("GETTING UNOFFICIAL THEME")
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
const str = pref.data
if (str === undefined || str === "undefined" || str === "") {
pref.setData(null)
return undefined
}
try {
const value: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
isOfficial: boolean
} = JSON.parse(str)
value.isOfficial = false
return value
} catch (e) {
console.warn(
"Removing theme " +
id +
" as it could not be parsed from the preferences; the content is:",
str
)
pref.setData(null)
return undefined
}
}
public markLayoutAsVisited(layout: LayoutConfig) {
if (!layout) {
console.error("Trying to mark a layout as visited, but ", layout, " got passed")
return
}
if (layout.hideFromOverview) {
this.osmConnection.isLoggedIn.addCallbackAndRunD((loggedIn) => {
if (loggedIn) {
this.osmConnection
.GetPreference("hidden-theme-" + layout?.id + "-enabled")
.setData("true")
return true
}
})
}
if (!layout.official) {
this.osmConnection.GetLongPreference("unofficial-theme-" + layout.id).setData(
JSON.stringify({
id: layout.id,
icon: layout.icon,
title: layout.title.translations,
shortDescription: layout.shortDescription.translations,
definition: layout["definition"],
})
)
}
}
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))
)
}
private initHomeLocation(): FeatureSource {
const empty = []
const feature: Store<Feature[]> = Stores.ListStabilized(
this.osmConnection.userDetails.map((userDetails) => {
if (userDetails === undefined) {
return undefined
}
const home = userDetails.home
if (home === undefined) {
return undefined
}
return [home.lon, home.lat]
})
).map((homeLonLat) => {
if (homeLonLat === undefined) {
return empty
}
return [
<Feature>{
type: "Feature",
properties: {
id: "home",
"user:home": "yes",
_lon: homeLonLat[0],
_lat: homeLonLat[1],
},
geometry: {
type: "Point",
coordinates: homeLonLat,
},
},
]
})
return new StaticFeatureSource(feature)
}
/**
* Initialize the 'amended preferences'.
* This is inherently a dirty and chaotic method, as it shoves many properties into this EventSourcd
* */
private initAmendedPrefs(
layout?: LayoutConfig,
featureSwitches?: FeatureSwitchState
): UIEventSource<Record<string, string>> {
const amendedPrefs = new UIEventSource<Record<string, string>>({
_theme: layout?.id,
_backend: this.osmConnection.Backend(),
_applicationOpened: new Date().toISOString(),
_supports_sharing:
typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no",
})
for (const key in Constants.userJourney) {
amendedPrefs.data["__userjourney_" + key] = Constants.userJourney[key]
}
const osmConnection = this.osmConnection
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
for (const k in newPrefs) {
const v = newPrefs[k]
if (k.endsWith("-combined-length")) {
const l = Number(v)
const key = k.substring(0, k.length - "length".length)
let combined = ""
for (let i = 0; i < l; i++) {
combined += newPrefs[key + i]
}
amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined
} else {
amendedPrefs.data[k] = newPrefs[k]
}
}
amendedPrefs.ping()
console.log("Amended prefs are:", amendedPrefs.data)
})
const usersettingsConfig = UserRelatedState.usersettingsConfig
const translationMode = osmConnection.GetPreference("translation-mode")
Locale.language.mapD(
(language) => {
amendedPrefs.data["_language"] = language
const trmode = translationMode.data
if ((trmode === "true" || trmode === "mobile") && layout !== undefined) {
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)
amendedPrefs.data["_translation_links"] = JSON.stringify(zenLinks)
}
amendedPrefs.ping()
},
[translationMode]
)
osmConnection.userDetails.addCallback((userDetails) => {
for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k]
}
for (const [name, code, _] of usersettingsConfig.calculatedTags) {
try {
let result = new Function("feat", "return " + code + ";")({
properties: amendedPrefs.data,
})
if (result !== undefined && result !== "" && result !== null) {
if (typeof result !== "string") {
result = JSON.stringify(result)
}
amendedPrefs.data[name] = result
}
} catch (e) {
console.error(
"Calculating a tag for userprofile-settings failed for variable",
name,
e
)
}
}
const simplifiedName = userDetails.name.toLowerCase().replace(/\s+/g, "")
const isTranslator = translators.contributors.find(
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
}
)
if (isTranslator) {
amendedPrefs.data["_translation_contributions"] = "" + isTranslator.commits
}
const isCodeContributor = codeContributors.contributors.find(
(c: { contributor: string; commits: number }) => {
const replaced = c.contributor.toLowerCase().replace(/\s+/g, "")
return replaced === simplifiedName
}
)
if (isCodeContributor) {
amendedPrefs.data["_code_contributions"] = "" + isCodeContributor.commits
}
amendedPrefs.ping()
})
amendedPrefs.addCallbackD((tags) => {
for (const key in tags) {
if (key.startsWith("_") || key === "mapcomplete-language") {
// Language is managed seperately
continue
}
this.osmConnection.GetPreference(key, undefined, { prefix: "" }).setData(tags[key])
}
})
for (const key in featureSwitches) {
if (featureSwitches[key].addCallbackAndRun) {
featureSwitches[key].addCallbackAndRun((v) => {
const oldV = amendedPrefs.data["__" + key]
if (oldV === v) {
return
}
amendedPrefs.data["__" + key] = "" + v
amendedPrefs.ping()
})
}
}
return amendedPrefs
}
}