Merge develop

This commit is contained in:
Pieter Vander Vennet 2025-02-05 11:41:27 +01:00
commit f25f5f156d
86 changed files with 1960 additions and 967 deletions

View file

@ -13,6 +13,7 @@ class SingleTileSaver {
private readonly _registeredIds = new Set<string>()
private readonly _featureProperties: FeaturePropertiesStore
private readonly _isDirty = new UIEventSource(false)
constructor(
storage: UIEventSource<Feature[]> & { flush: () => void },
featureProperties: FeaturePropertiesStore
@ -62,6 +63,7 @@ class SingleTileSaver {
export default class SaveFeatureSourceToLocalStorage {
public readonly storage: TileLocalStorage<Feature[]>
private readonly zoomlevel: number
constructor(
backend: string,
layername: string,
@ -75,8 +77,25 @@ export default class SaveFeatureSourceToLocalStorage {
this.storage = storage
const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>()
features.features.addCallbackAndRunD((features) => {
if (features.some(f => {
let totalPoints = 0
if (f.geometry.type === "MultiPolygon") {
totalPoints = f.geometry.coordinates.map(rings => rings.map(ring => ring.length).reduce((a, b) => a + b)).reduce((a, b) => a + b)
} else if (f.geometry.type === "Polygon" || f.geometry.type === "MultiLineString") {
totalPoints = f.geometry.coordinates.map(ring => ring.length).reduce((a, b) => a + b)
} else if (f.geometry.type === "LineString") {
totalPoints = f.geometry.coordinates.length
}
if (totalPoints > 1000) {
console.warn(`Not caching tiles, detected a big object (${totalPoints} points for ${f.properties.id})`)
return true
}
return false
})) {
// Has big objects
return
}
const sliced = GeoOperations.spreadIntoBboxes(features, zoomlevel)
sliced.forEach((features, tileIndex) => {
let tileSaver = singleTileSavers.get(tileIndex)
if (tileSaver === undefined) {

View file

@ -90,6 +90,9 @@ export default class AllImageProviders {
const allPrefixes = Utils.Dedup(prefixes ?? [].concat(...sources.map(s => s.defaultKeyPrefixes)))
for (const prefix of allPrefixes) {
for (const k in tags) {
if (!tags[k]) {
continue
}
if (k === prefix || k.startsWith(prefix + ":")) {
count++
continue

View file

@ -18,29 +18,29 @@ interface OsmUserInfo {
"contributor_terms": {
"agreed": boolean,
"pd": boolean
},
"img": {
}
"img"?: {
"href": string,
},
"roles": string[],
}
"roles": string[]
"changesets": {
"count": number
},
"traces": {
"count": number
},
}
traces: {
count: number
}
"blocks": {
"received": {
"count": number,
"active": number
}
},
"home": {
"lat": number,
"lon": number,
"zoom": number
},
"languages": string[],
}
home?: {
lat: number,
lon: number,
zoom: number
}
"languages": string[]
"messages": {
"received": {
"count": number,
@ -49,7 +49,22 @@ interface OsmUserInfo {
"sent": {
"count": number
}
}
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
img?: { href: string }
home: { lat: number, lon: number }
languages?: string[]
messages: { received: { count: number, unread: number }, sent: { count: number } }
}
}
@ -231,12 +246,9 @@ export class OsmConnection {
return <UIEventSource<T>>this.preferencesHandler.getPreference(key, defaultValue, prefix)
}
public LogOut() {
this.auth.logout()
this.userDetails.data.csCount = 0
this.userDetails.data.name = ""
this.userDetails.ping()
this.userDetails.setData(undefined)
console.log("Logged out")
this.loadingStatus.setData("not-attempted")
}
@ -250,7 +262,7 @@ export class OsmConnection {
return this._oauth_config.url
}
public AttemptLogin() {
public async AttemptLogin() {
this.updateCapabilities()
if (this.loadingStatus.data !== "logged-in") {
// Stay 'logged-in' if we are already logged in; this simply means we are checking for messages
@ -271,7 +283,6 @@ export class OsmConnection {
this.loadUserInfo()
}
})
}
private async loadUserInfo() {
@ -294,17 +305,15 @@ export class OsmConnection {
description: user.description,
backend: this.Backend(),
home: user.home,
languages: user.languages,
languages: user.languages ?? [],
totalMessages: user.messages.received?.count ?? 0,
img: user.img?.href,
account_created: user.account_created,
tracesCount: user.traces.count,
tracesCount: user.traces?.count ?? 0,
unreadMessages: user.messages.received?.unread ?? 0,
}
console.log("Login completed, userinfo is ", userdetails)
this.userDetails.set(userdetails)
this.loadingStatus.setData("logged-in")
} catch (err) {
console.log("Could not login due to:", err)
this.loadingStatus.setData("error")
@ -314,7 +323,7 @@ export class OsmConnection {
this.auth.logout()
this.LogOut()
} else {
console.log("Other error. Status:", err.status)
console.log("Other error. Status:", err["status"])
this.apiIsOnline.setData("unreachable")
}
}
@ -362,7 +371,7 @@ export class OsmConnection {
method,
headers: header,
content,
path: `/api/0.6/${path}`,
path: `/api/0.6/${path}`
},
function(err, response) {
if (err !== null) {
@ -444,7 +453,7 @@ export class OsmConnection {
"notes.json",
content,
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
true,
)
@ -489,7 +498,7 @@ export class OsmConnection {
file: gpx,
description: options.description,
tags: options.labels?.join(",") ?? "",
visibility: options.visibility,
visibility: options.visibility
}
if (!contents.description) {
@ -499,7 +508,7 @@ export class OsmConnection {
file:
"; filename=\"" +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
"\"\r\nContent-Type: application/gpx+xml",
"\"\r\nContent-Type: application/gpx+xml"
}
const boundary = "987654"
@ -518,7 +527,7 @@ export class OsmConnection {
const response = await this.post("gpx/create", body, {
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": "" + body.length,
"Content-Length": "" + body.length
})
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
@ -539,7 +548,7 @@ export class OsmConnection {
{
method: "POST",
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
},
function(err) {
if (err !== null) {
@ -556,7 +565,6 @@ export class OsmConnection {
* To be called by land.html
*/
public finishLogin(callback: (previousURL: string, oauth_token: string) => void) {
console.log(">>> authenticating")
this.auth.authenticate(() => {
// Fully authed at this point
console.log("Authentication successful!")
@ -593,7 +601,7 @@ export class OsmConnection {
*/
singlepage: !this._iframeMode && !AndroidPolyfill.inAndroid.data,
auto: autoLogin,
apiUrl: this._oauth_config.api_url ?? this._oauth_config.url,
apiUrl: this._oauth_config.api_url ?? this._oauth_config.url
})
if (AndroidPolyfill.inAndroid.data) {
this.loginAndroidPolyfill() // NO AWAIT!

View file

@ -1,12 +1,9 @@
import GeocodingProvider, {
SearchResult,
GeocodingOptions,
GeocodeResult,
} from "./GeocodingProvider"
import GeocodingProvider, { GeocodeResult, GeocodingOptions, SearchResult } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Store, Stores } from "../UIEventSource"
export default class CombinedSearcher implements GeocodingProvider {
public readonly name = "CombinedSearcher"
private _providers: ReadonlyArray<GeocodingProvider>
private _providersWithSuggest: ReadonlyArray<GeocodingProvider>

View file

@ -2,10 +2,12 @@ import GeocodingProvider, { GeocodeResult } from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { ImmutableStore, Store } from "../UIEventSource"
import CoordinateParser from "coordinate-parser"
/**
* A simple search-class which interprets possible locations
*/
export default class CoordinateSearch implements GeocodingProvider {
public readonly name = "CoordinateSearch"
private static readonly latLonRegexes: ReadonlyArray<RegExp> = [
/^ *(-?[0-9]+\.[0-9]+)[ ,;/\\]+(-?[0-9]+\.[0-9]+)/,
/^ *(-?[0-9]+,[0-9]+)[ ;/\\]+(-?[0-9]+,[0-9]+)/,

View file

@ -49,6 +49,13 @@ export interface GeocodingOptions {
}
export default interface GeocodingProvider {
readonly name: string
/**
* Performs search.
* Note: the result _must_ return an empty list in the case of no results.
* Undefined might be interpreted by clients as "still running"
*/
search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]>
/**

View file

@ -1,4 +1,4 @@
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
import ThemeViewState from "../../Models/ThemeViewState"
import { Utils } from "../../Utils"
import { Feature } from "geojson"
@ -20,7 +20,7 @@ type IntermediateResult = {
export default class LocalElementSearch implements GeocodingProvider {
private readonly _state: ThemeViewState
private readonly _limit: number
public readonly name = "LocalElementSearch"
constructor(state: ThemeViewState, limit: number) {
this._state = state
this._limit = limit

View file

@ -8,6 +8,7 @@ import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingPr
export class NominatimGeocoding implements GeocodingProvider {
private readonly _host
private readonly limit: number
public readonly name = "Nominatim"
constructor(limit: number = 3, host: string = Constants.nominatimEndpoint) {
this.limit = limit

View file

@ -8,7 +8,7 @@ export default class OpenLocationCodeSearch implements GeocodingProvider {
*/
public static readonly _isPlusCode =
/^([2-9CFGHJMPQRVWX]{2}|00){2,4}\+([2-9CFGHJMPQRVWX]{2,3})?$/
public readonly name = "OpenLocationCodeSearch"
/**
*
* OpenLocationCodeSearch.isPlusCode("9FFW84J9+XG") // => true
@ -26,7 +26,7 @@ export default class OpenLocationCodeSearch implements GeocodingProvider {
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
if (!OpenLocationCodeSearch.isPlusCode(query)) {
return undefined
return [] // Must be an empty list and not "undefined", the latter is interpreted as 'still searching'
}
const { latitude, longitude } = pluscode_decode(query)

View file

@ -7,7 +7,7 @@ import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
export default class OpenStreetMapIdSearch implements GeocodingProvider {
private static readonly regex =
/((https?:\/\/)?(www.)?(osm|openstreetmap).org\/)?(n|node|w|way|r|relation)[/ ]?([0-9]+)/
public readonly name = "OpenStreetMapId"
private static readonly types: Readonly<Record<string, "node" | "way" | "relation">> = {
n: "node",
w: "way",

View file

@ -5,7 +5,7 @@ import GeocodingProvider, {
GeocodingOptions,
GeocodingUtils,
ReverseGeocodingProvider,
ReverseGeocodingResult,
ReverseGeocodingResult
} from "./GeocodingProvider"
import { Utils } from "../../Utils"
import { Feature, FeatureCollection } from "geojson"
@ -15,6 +15,7 @@ import { Store, Stores } from "../UIEventSource"
export default class PhotonSearch implements GeocodingProvider, ReverseGeocodingProvider {
private readonly _endpoint: string
public readonly name = "photon"
private supportedLanguages = ["en", "de", "fr"]
private static readonly types = {
R: "relation",

View file

@ -5,7 +5,7 @@ import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import { CountryCoder } from "latlon2country"
import Constants from "../Models/Constants"
import { TagUtils } from "./Tags/TagUtils"
import { Feature, LineString } from "geojson"
import { Feature, LineString, MultiPolygon, Polygon } from "geojson"
import { OsmTags } from "../Models/OsmFeature"
import { UIEventSource } from "./UIEventSource"
import ThemeConfig from "../Models/ThemeConfig/ThemeConfig"
@ -80,7 +80,7 @@ export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
super({
keys: ["_referencing_ways"],
isLazy: true,
doc: "_referencing_ways contains - for a node - which ways use this node as point in their geometry. ",
doc: "_referencing_ways contains - for a node - which ways use this node as point in their geometry. "
})
}
@ -116,7 +116,7 @@ class CountryTagger extends SimpleMetaTagger {
super({
keys: ["_country"],
doc: "The country codes of the of the country/countries that the feature is located in (with latlon2country). Might contain _multiple_ countries, separated by a `;`",
includesDates: false,
includesDates: false
})
}
@ -213,9 +213,9 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
"_last_edit:changeset",
"_last_edit:timestamp",
"_version_number",
"_backend",
"_backend"
],
doc: "Information about the last edit of this object. This object will actually _rewrite_ some tags for features coming from overpass",
doc: "Information about the last edit of this object. This object will actually _rewrite_ some tags for features coming from overpass"
})
}
@ -244,6 +244,69 @@ class RewriteMetaInfoTags extends SimpleMetaTagger {
}
}
class NormalizePanoramax extends SimpleMetaTagger {
constructor() {
super(
{
keys: ["panoramax"],
doc: "Converts a `panoramax=hash1;hash2;hash3;...` into `panoramax=hash1`,`panoramax:0=hash1`...",
isLazy: false,
cleanupRetagger: true
})
}
private addValue(comesFromKey: string, tags: Record<string, string>, hashesToAdd: string[], postfix?: string) {
let basekey = "panoramax"
if (postfix) {
basekey = "panoramax:" + postfix
}
let index = -1
for (let i = 0; i < hashesToAdd.length; i++) {
let k = basekey
do {
if (index >= 0) {
k = `${basekey}:${index}`
}
index++
} while (k !== comesFromKey && tags[k])
tags[k] = hashesToAdd[i]
}
}
/**
* const tags = new UIEventSource({panoramax: "abc;def;ghi", "panoramax:2": "xyz;uvw", "panoramax:streetsign":"a;b;c"})
* const _ = undefined
* new NormalizePanoramax().applyMetaTagsOnFeature(_, _, tags, _)
* tags.data // => {"panoramax": "abc", "panoramax:0" : "def", "panoramax:1": "ghi", "panoramax:2":"xyz", "panoramax:3":"uvw", "panoramax:streetsign":"a", "panoramax:streetsign:0":"b","panoramax:streetsign:1": "c"}
*/
applyMetaTagsOnFeature(feature: Feature, layer: LayerConfig, tags: UIEventSource<Record<string, string>>): boolean {
const tgs = tags.data
let somethingChanged = false
for (const key in tgs) {
if (!(key === "panoramax" || key.startsWith("panoramax:"))) {
continue
}
const v = tgs[key]
if (v.indexOf(";") < 0) {
continue
}
const parts = v.split(";")
if (key === "panoramax" || key.match("panoramax:[0-9]+")) {
this.addValue(key, tgs, parts)
somethingChanged = true
} else {
const postfix = key.match(/panoramax:([^:]+)(:[0-9]+)?/)?.[1]
if (postfix) {
this.addValue(key, tgs, parts, postfix)
somethingChanged = true
}
}
}
return somethingChanged
}
}
export default class SimpleMetaTaggers {
/**
* A simple metatagger which rewrites various metatags as needed
@ -253,7 +316,7 @@ export default class SimpleMetaTaggers {
public static geometryType = new InlineMetaTagger(
{
keys: ["_geometry:type"],
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`",
doc: "Adds the geometry type as property. This is identical to the GoeJson geometry type and is one of `Point`,`LineString`, `Polygon` and exceptionally `MultiPolygon` or `MultiLineString`"
},
(feature) => {
const changed = feature.properties["_geometry:type"] === feature.geometry.type
@ -262,6 +325,7 @@ export default class SimpleMetaTaggers {
}
)
public static referencingWays = new ReferencingWaysMetaTagger()
private static normalizePanoramax = new NormalizePanoramax()
private static readonly cardinalDirections = {
N: 0,
NNE: 22.5,
@ -278,12 +342,12 @@ export default class SimpleMetaTaggers {
W: 270,
WNW: 292.5,
NW: 315,
NNW: 337.5,
NNW: 337.5
}
private static latlon = new InlineMetaTagger(
{
keys: ["_lat", "_lon"],
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)",
doc: "The latitude and longitude of the point (or centerpoint in the case of a way/area)"
},
(feature) => {
const centerPoint = GeoOperations.centerpoint(feature)
@ -298,7 +362,7 @@ export default class SimpleMetaTaggers {
{
doc: "The layer-id to which this feature belongs. Note that this might be return any applicable if `passAllFeatures` is defined.",
keys: ["_layer"],
includesDates: false,
includesDates: false
},
(feature, layer) => {
if (feature.properties._layer === layer.id) {
@ -314,11 +378,11 @@ export default class SimpleMetaTaggers {
"sidewalk:left",
"sidewalk:right",
"generic_key:left:property",
"generic_key:right:property",
"generic_key:right:property"
],
doc: "Rewrites tags from 'generic_key:both:property' as 'generic_key:left:property' and 'generic_key:right:property' (and similar for sidewalk tagging). Note that this rewritten tags _will be reuploaded on a change_. To prevent to much unrelated retagging, this is only enabled if the layer has at least some lineRenderings with offset defined",
includesDates: false,
cleanupRetagger: true,
cleanupRetagger: true
},
(feature, layer) => {
if (!layer.lineRendering.some((lr) => lr.leftRightSensitive)) {
@ -332,11 +396,15 @@ export default class SimpleMetaTaggers {
{
keys: ["_surface"],
doc: "The surface area of the feature in square meters. Not set on points and ways",
isLazy: true,
isLazy: true
},
(feature) => {
if (feature.geometry.type !== "Polygon" && feature.geometry.type !== "MultiPolygon") {
return
}
const f = <Feature<Polygon | MultiPolygon>>feature
Utils.AddLazyProperty(feature.properties, "_surface", () => {
return "" + GeoOperations.surfaceAreaInSqMeters(feature)
return "" + GeoOperations.surfaceAreaInSqMeters(f)
})
return true
@ -346,11 +414,15 @@ export default class SimpleMetaTaggers {
{
keys: ["_surface:ha"],
doc: "The surface area of the feature in hectare. Not set on points and ways",
isLazy: true,
isLazy: true
},
(feature) => {
if (feature.geometry.type !== "Polygon" && feature.geometry.type !== "MultiPolygon") {
return
}
const f = <Feature<Polygon | MultiPolygon>>feature
Utils.AddLazyProperty(feature.properties, "_surface:ha", () => {
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature)
const sqMeters = GeoOperations.surfaceAreaInSqMeters(f)
return "" + Math.floor(sqMeters / 1000) / 10
})
@ -360,7 +432,7 @@ export default class SimpleMetaTaggers {
private static levels = new InlineMetaTagger(
{
doc: "Extract the 'level'-tag into a normalized, ';'-separated value called '_level' (which also includes 'repeat_on'). The `level` tag (without underscore) will be normalized with only the value of `level`.",
keys: ["_level"],
keys: ["_level"]
},
(feature) => {
let somethingChanged = false
@ -395,7 +467,7 @@ export default class SimpleMetaTaggers {
private static canonicalize = new InlineMetaTagger(
{
doc: "If 'units' is defined in the layoutConfig, then this metatagger will rewrite the specified keys to have the canonical form (e.g. `1meter` will be rewritten to `1m`; `1` will be rewritten to `1m` as well)",
keys: ["Theme-defined keys"],
keys: ["Theme-defined keys"]
},
(feature, _, __, state) => {
const units = Utils.NoNull(
@ -452,7 +524,7 @@ export default class SimpleMetaTaggers {
private static lngth = new InlineMetaTagger(
{
keys: ["_length", "_length:km"],
doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter",
doc: "The total length of a feature in meters (and in kilometers, rounded to one decimal for '_length:km'). For a surface, the length of the perimeter"
},
(feature) => {
const l = GeoOperations.lengthInMeters(feature)
@ -468,7 +540,7 @@ export default class SimpleMetaTaggers {
keys: ["_isOpen"],
doc: "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')",
includesDates: true,
isLazy: true,
isLazy: true
},
(feature) => {
if (Utils.runningFromConsole) {
@ -507,8 +579,8 @@ export default class SimpleMetaTaggers {
lon: lon,
address: {
country_code: tags._country.toLowerCase(),
state: undefined,
},
state: undefined
}
},
<any>{ tag_key: "opening_hours" }
)
@ -520,14 +592,14 @@ export default class SimpleMetaTaggers {
delete tags._isOpen
tags["_isOpen"] = "parse_error"
}
},
}
})
}
)
private static directionSimplified = new InlineMetaTagger(
{
keys: ["_direction:numerical", "_direction:leftright"],
doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map",
doc: "_direction:numerical is a normalized, numerical direction based on 'camera:direction' or on 'direction'; it is only present if a valid direction is found (e.g. 38.5 or NE). _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map"
},
(feature) => {
const tags = feature.properties
@ -552,7 +624,7 @@ export default class SimpleMetaTaggers {
{
keys: ["_direction:centerpoint"],
isLazy: true,
doc: "_direction:centerpoint is the direction of the linestring (in degrees) if one were standing at the projected centerpoint.",
doc: "_direction:centerpoint is the direction of the linestring (in degrees) if one were standing at the projected centerpoint."
},
(feature: Feature) => {
if (feature.geometry.type !== "LineString") {
@ -575,7 +647,7 @@ export default class SimpleMetaTaggers {
delete feature.properties["_direction:centerpoint"]
feature.properties["_direction:centerpoint"] = bearing
return bearing
},
}
})
return true
@ -585,7 +657,7 @@ export default class SimpleMetaTaggers {
{
keys: ["_now:date", "_now:datetime"],
doc: "Adds the time that the data got loaded - pretty much the time of downloading from overpass. The format is YYYY-MM-DD hh:mm, aka 'sortable' aka ISO-8601-but-not-entirely",
includesDates: true,
includesDates: true
},
(feature) => {
const now = new Date()
@ -609,7 +681,7 @@ export default class SimpleMetaTaggers {
keys: ["_last_edit:passed_time"],
doc: "Gives the number of seconds since the last edit. Note that this will _not_ update, but rather be the number of seconds elapsed at the moment this tag is read first",
isLazy: true,
includesDates: true,
includesDates: true
},
(feature) => {
Utils.AddLazyProperty(feature.properties, "_last_edit:passed_time", () => {
@ -628,7 +700,7 @@ export default class SimpleMetaTaggers {
{
keys: ["_currency"],
doc: "Adds the currency valid for the object, based on country or explicit tagging. Can be a single currency or a semicolon-separated list of currencies. Empty if no currency is found.",
isLazy: true,
isLazy: true
},
(feature: Feature, layer: LayerConfig, tagsStore: UIEventSource<OsmTags>) => {
if (tagsStore === undefined) {
@ -670,6 +742,7 @@ export default class SimpleMetaTaggers {
}
)
public static metatags: SimpleMetaTagger[] = [
SimpleMetaTaggers.latlon,
SimpleMetaTaggers.layerInfo,
@ -689,6 +762,7 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.referencingWays,
SimpleMetaTaggers.timeSinceLastEdit,
SimpleMetaTaggers.currency,
SimpleMetaTaggers.normalizePanoramax
]
/**
@ -770,8 +844,8 @@ export default class SimpleMetaTaggers {
[
"Metatags are extra tags available, in order to display more data or to give better questions.",
"They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.",
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object",
].join("\n"),
"**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object"
].join("\n")
]
subElements.push("## Metatags calculated by MapComplete")

View file

@ -60,7 +60,7 @@ export default class SearchState {
return new ImmutableStore(true)
}
return Stores.concat(suggestions).map((suggestions) =>
suggestions.some((list) => list === undefined)
suggestions.some((list, i) => list === undefined)
)
})
this.suggestions = suggestionsList.bindD((suggestions) =>

View file

@ -350,10 +350,10 @@ export default class UserRelatedState {
* List of all hidden themes that have been seen before
* @param osmConnection
*/
public static initDiscoveredHiddenThemes(osmConnection: OsmConnection): Store<string[]> {
public static initDiscoveredHiddenThemes(osmConnection: OsmConnection): Store<undefined | string[]> {
const prefix = "mapcomplete-hidden-theme-"
const userPreferences = osmConnection.preferencesHandler.allPreferences
return userPreferences.map((preferences) =>
return userPreferences.mapD((preferences) =>
Object.keys(preferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
@ -497,7 +497,7 @@ export default class UserRelatedState {
amendedPrefs.ping()
})
osmConnection.userDetails.addCallback((userDetails) => {
osmConnection.userDetails.addCallbackD((userDetails) => {
for (const k in userDetails) {
amendedPrefs.data["_" + k] = "" + userDetails[k]
}

View file

@ -132,6 +132,16 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
return filters
}
/**
*
* import FilterConfig from "../FilterConfig"
*
* // A multi-answer tagRendering should match any subkey
* const tr = {id: "test", multiAnswer: true, mappings:[{if: "x=a", then: "A"}, {if: "x=b", then:"B"}]}
* const filter = ExpandFilter.buildFilterFromTagRendering(tr, ConversionContext.test("ExpandFilter"))
* const f = new FilterConfig(filter, "test")
* f.options[1].osmTags.matchesProperties({x:"a;b"}) // => true
*/
public static buildFilterFromTagRendering(
tr: TagRenderingConfigJson,
context: ConversionContext
@ -153,7 +163,7 @@ export class ExpandFilter extends DesugaringStep<LayerConfigJson> {
if (qtr.multiAnswer && osmTags instanceof Tag) {
osmTags = new RegexTag(
osmTags.key,
new RegExp("^(.+;)?" + osmTags.value + "(;.+)$", "is")
new RegExp("^(.+;)?" + osmTags.value + "(;.+)?$", "is")
)
}
if (mapping.alsoShowIf) {

View file

@ -436,13 +436,26 @@ export interface LayerConfigJson {
)[]
/**
* All the extra questions for filtering.
* If a string is given, mapComplete will search in
* 1. The tagrenderings for a match on ID and use the mappings as options
* 2. search 'filters.json' for the appropriate filter or
* 3. will try to parse it as `layername.filterid` and us that one.
* Filters are a way to temporarily hide the data from the map (but the data is still loaded from overpass/OSM/the specified source).
* This is used to e.g. show "shops open now", "toilets with wheelchair access", "free toilets", ...
*
* Note: adding "#filter":"no-auto" will disable the filters added by tagRenderings
* Filters can be added in various ways:
*
* - You can specify one explicitly here
* - You can specify the id (as a string) of a tagRendering. The tagrendering will then automatically be converted to a filter
* If the ID is not found locally, it will be searched in `filters.json`.
* If a dot is present, the ID will be interpreted as "<layername>.<filterId>" instead
* - A tagRendering might specify `filter: true`. This will add the tagRendering to the filter list automatically
* This might introduce filters from _imported_ tagRenderings
* Note: adding "#filter":"no-auto" in the layer object will disable this
*
* A special case is setting `filter: {sameAs: "layerId"}`.
* This is only done with twin layers, where one layer is mostly visible (e.g. all shops offering some kind of bicycle service)
* and another layer (e.g. "all shops") shows up at high zoom levels (typically 17 or 18). This way, people can mark an already existing shop
* as bicycle shop and don't create a duplicate entry.
* Of course, if one applies a filter (e.g. "open now") the user will expect the "all shops" layer to also only show shops open now.
* As is often the case, the secondary layer will be hidden from the filter view, so they won't be able to enable those filters.
* In this case, we let the secondary layer follow the first layer with a `sameAs`.
*
* group: filters
*/

View file

@ -41,7 +41,7 @@
const tu = Translations.t.general
const tr = Translations.t.general.morescreen
let userLanguages = osmConnection.userDetails.mapD((ud) => ud.languages)
let userLanguages = osmConnection.userDetails.map((ud) => ud?.languages ?? [])
let search: UIEventSource<string | undefined> = new UIEventSource<string>("")
let searchStable = search.stabilized(100)
@ -53,8 +53,8 @@
const hiddenThemes: MinimalThemeInformation[] = ThemeSearch.officialThemes.themes.filter(
(th) => th.hideFromOverview === true
)
let visitedHiddenThemes: Store<MinimalThemeInformation[]> =
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map((knownIds) =>
let visitedHiddenThemes: Store<undefined | MinimalThemeInformation[]> =
UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).mapD((knownIds) =>
hiddenThemes.filter(
(theme) =>
knownIds.indexOf(theme.id) >= 0 ||
@ -68,6 +68,9 @@
function filtered(themes: Store<MinimalThemeInformation[]>): Store<MinimalThemeInformation[]> {
return searchStable.map(
(search) => {
if (!themes.data) {
return []
}
if (!search) {
return themes.data
}

View file

@ -18,6 +18,7 @@
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { MenuState } from "../../Models/MenuState"
export let image: ProvidedImage
export let state: SpecialVisualizationState
@ -26,7 +27,7 @@
onDestroy(
showDeleteDialog.addCallbackAndRunD((shown) => {
if (shown) {
state.previewedImage.set(undefined)
MenuState.previewedImage.set(undefined)
}
})
)
@ -53,18 +54,31 @@
issue: reportReason.data,
sequence_id: imageInfo.collection,
reporter_comments: (reportFreeText.data ?? "") + "\n\n" + "Reported from " + url,
reporter_email,
reporter_email
})
reported.set(true)
}
async function unlink() {
await state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(image.key, ""), tags.data, {
changeType: "delete-image",
theme: state.theme.id,
})
)
console.log("Unlinking image", image.key, image.id)
if (image.id.length < 10) {
console.error("Suspicious value, not deleting ", image.id)
return
}
// The "key" is the provider key, but not necessarely the actual key that should be reset
// We iterate over all tags. *Every* tag for which the value contains the id will be deleted
const tgs = tags.data
for (const key in tgs) {
if (typeof tgs[key] !== "string" || tgs[key].indexOf(image.id) < 0) {
continue
}
await state?.changes?.applyAction(
new ChangeTagAction(tgs.id, new Tag(key, ""), tgs, {
changeType: "delete-image",
theme: state.theme.id
}))
}
}
const t = Translations.t.image.panoramax
@ -161,7 +175,7 @@
</div>
<style>
:global(.carousel-max-height) {
max-height: var(--image-carousel-height);
}
:global(.carousel-max-height) {
max-height: var(--image-carousel-height);
}
</style>

View file

@ -1,10 +1,5 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import maplibregl, {
Map as MLMap,
Map as MlMap,
ScaleControl,
SourceSpecification,
} from "maplibre-gl"
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
import { RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
@ -179,6 +174,10 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
maplibreMap.addCallbackAndRunD((map) => {
map.on("load", () => {
console.log("Setting projection")
map.setProjection({
type: "globe" // Set projection to globe
})
self.MoveMapToCurrentLoc(self.location.data)
self.SetZoom(self.zoom.data)
self.setMaxBounds(self.maxbounds.data)

View file

@ -7,6 +7,8 @@
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import SelectedElementView from "../BigComponents/SelectedElementView.svelte"
import TagRenderingAnswer from "./TagRendering/TagRenderingAnswer.svelte"
import TagRenderingEditableDynamic from "./TagRendering/TagRenderingEditableDynamic.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
export let state: SpecialVisualizationState
export let selectedElement: Feature
@ -16,11 +18,32 @@
export let layer: LayerConfig
let headerTr = layer.tagRenderings.find((tr) => tr.id === header)
let trgs: TagRenderingConfig[] = []
let seenIds = new Set<string>()
for (const label of labels) {
for (const tr of layer.tagRenderings) {
if (seenIds.has(tr.id)) {
continue
}
if (label === tr.id || tr.labels.some(l => l === label)) {
trgs.push(tr)
seenIds.add(tr.id)
}
}
}
</script>
<AccordionSingle>
<div slot="header">
<TagRenderingAnswer {tags} {layer} config={headerTr} {state} {selectedElement} />
</div>
<SelectedElementView mustMatchLabels={new Set(labels)} {state} {layer} {tags} {selectedElement} />
{#each trgs as config (config.id)}
<TagRenderingEditableDynamic
{tags}
{config}
{state}
{selectedElement}
{layer}
/>
{/each}
</AccordionSingle>

View file

@ -17,7 +17,7 @@ export class LanguageElement implements SpecialVisualization {
{
name: "key",
required: true,
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `language:nl=yes` if nl is picked ",
doc: "What key to use, e.g. `language`, `tactile_writing:braille:language`, ... If a language is supported, the language code will be appended to this key, resulting in `<key>:nl=yes` if _nl_ is picked "
},
{
name: "question",

View file

@ -1,7 +1,6 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"
import MoveConfig from "../../Models/ThemeConfig/MoveConfig"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
@ -9,9 +8,6 @@ import { And } from "../../Logic/Tags/And"
import { Tag } from "../../Logic/Tags/Tag"
import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature, Point } from "geojson"
import SvelteUIElement from "../Base/SvelteUIElement"
import Relocation from "../../assets/svg/Relocation.svelte"
import Location from "../../assets/svg/Location.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { WayId } from "../../Models/OsmFeature"
@ -104,7 +100,7 @@ export class MoveWizardState {
for (const layerId of matchingPreset) {
const snapOntoLayer = this._state.theme.getLayer(layerId)
const text = <Translation>(
t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer.snapName)
t.reasons.reasonSnapTo.PartialSubsTr("name", snapOntoLayer?.snapName)
)
reasons.push({
text,
@ -117,7 +113,7 @@ export class MoveWizardState {
startZoom: 19,
minZoom: 16,
eraseAddressFields: false,
snapTo: [snapOntoLayer.id],
snapTo: [snapOntoLayer?.id],
maxSnapDistance: 5,
})
}

View file

@ -2,10 +2,7 @@
import { Translation } from "../../i18n/Translation"
import SpecialVisualizations from "../../SpecialVisualizations"
import Locale from "../../i18n/Locale"
import type {
RenderingSpecification,
SpecialVisualizationState,
} from "../../SpecialVisualization"
import type { RenderingSpecification, SpecialVisualizationState } from "../../SpecialVisualization"
import { Utils } from "../../../Utils.js"
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource.js"
@ -49,13 +46,9 @@
function createVisualisation(specpart: Exclude<RenderingSpecification, string>): BaseUIElement {
{
try {
const uiEl = specpart.func
return specpart.func
.constr(state, tags, specpart.args, feature, layer)
?.SetClass(specpart.style)
if (uiEl === undefined) {
console.error("Invalid special translation")
}
return uiEl
} catch (e) {
console.error(
"Could not construct a special visualisation with specification",

View file

@ -5,7 +5,6 @@
import Translations from "../i18n/Translations"
import { Store } from "../../Logic/UIEventSource"
import SidebarUnit from "../Base/SidebarUnit.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Loading from "../Base/Loading.svelte"
import { default as GeocodeResultSvelte } from "./GeocodeResult.svelte"
import Tr from "../Base/Tr.svelte"

View file

@ -7,7 +7,7 @@ import LoginButton from "../Base/LoginButton.svelte"
import ThemeViewState from "../../Models/ThemeViewState"
import OrientationDebugPanel from "../Debug/OrientationDebugPanel.svelte"
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
import ClearCaches from "../Popup/ClearCaches.svelte"
import Locale from "../i18n/Locale"
import LanguageUtils from "../../Utils/LanguageUtils"
@ -75,6 +75,22 @@ export class SettingsVisualisations {
})
}
},
{
funcName: "storage_all_tags",
group: "settings",
docs: "Shows the current state of storage",
args: [],
constr(
state: SpecialVisualizationState
): SvelteUIElement {
const data = {}
for (const key in localStorage) {
data[key] = localStorage[key]
}
const tags = new ImmutableStore(data)
return new SvelteUIElement(AllTagsPanel, { state, tags })
}
},
{
funcName: "clear_caches",
docs: "A button which clears the locally downloaded data and the service worker. Login status etc will be kept",
@ -89,7 +105,7 @@ export class SettingsVisualisations {
constr(
_: SpecialVisualizationState,
__: UIEventSource<Record<string, string>>,
argument: string[],
argument: string[]
): SvelteUIElement {
return new SvelteUIElement(ClearCaches, {
msg: argument[0] ?? "Clear local caches"

View file

@ -176,6 +176,7 @@
</script>
<main>
<div class="absolute top-0 left-0 h-screen w-screen" style="background-color: #cccccc"></div>
<!-- Main map -->
<div class="absolute top-0 left-0 h-screen w-screen overflow-hidden">
<MaplibreMap map={maplibremap} mapProperties={mapproperties} autorecovery={true} />

View file

@ -5,13 +5,17 @@ import CustomThemeError from "./UI/CustomThemeError.svelte"
async function main() {
const target = document.getElementById("maindiv")
const childs = Array.from(target.children)
try {
childs.forEach((ch) => target.removeChild(ch))
} catch (e) {
console.error("Huh? Couldn't remove child!")
}
try {
const theme = await DetermineTheme.getTheme()
new SingleThemeGui({
target,
props: { theme },
})
childs.forEach((ch) => target.removeChild(ch))
Array.from(document.getElementsByClassName("delete-on-load")).forEach((el) => {
el.parentElement.removeChild(el)
})