Merge branch 'develop' into feature/eslint

This commit is contained in:
Robin van der Linde 2023-09-02 23:49:36 +02:00
commit ce897d28df
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
1450 changed files with 20081 additions and 16531 deletions

View file

@ -21,11 +21,9 @@ import questions from "../assets/generated/layers/questions.json"
import {
DoesImageExist,
PrevalidateTheme,
ValidateTagRenderings,
ValidateThemeAndLayers,
} from "../Models/ThemeConfig/Conversion/Validation"
import { DesugaringContext } from "../Models/ThemeConfig/Conversion/Conversion"
import { RewriteSpecial } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import Hash from "./Web/Hash"

View file

@ -61,7 +61,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
private async loadData(bbox: BBox) {
if (this.isActive?.data === false) {
console.log("OsmFeatureSource: not triggering: inactive")
return
}
@ -72,6 +71,11 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
return
}
if (neededTiles.total > 100) {
console.error("Too much tiles to download!")
return
}
this.isRunning.setData(true)
try {
const tileNumbers = Tiles.MapRange(neededTiles, (x, y) => {
@ -133,7 +137,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
}
private async LoadTile(z: number, x: number, y: number): Promise<void> {
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
if (z >= 22) {
throw "This is an absurd high zoom level"
}
@ -145,6 +148,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
if (this._downloadedTiles.has(index)) {
return
}
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
this._downloadedTiles.add(index)
const bbox = BBox.fromTile(z, x, y)

View file

@ -81,7 +81,6 @@ export default class OverpassFeatureSource implements FeatureSource {
*/
private async updateAsyncIfNeeded(): Promise<void> {
if (!this._isActive?.data) {
console.log("OverpassFeatureSource: not triggering as not active")
return
}
if (this.runningQuery.data) {

View file

@ -482,7 +482,7 @@ export class GeoOperations {
trackPoints.push(trkpt)
}
const header =
'<gpx version="1.1" creator="MapComplete.osm.be" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
return (
header +
"\n<name>" +

View file

@ -1,8 +1,10 @@
import osmAuth from "osm-auth"
// @ts-ignore
import { osmAuth } from "osm-auth"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences"
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import * as config from "../../../package.json"
export default class UserDetails {
public loggedIn = false
public name = "Not logged in"
@ -22,23 +24,18 @@ export default class UserDetails {
}
}
export interface AuthConfig {
"#"?: string // optional comment
oauth_client_id: string
oauth_secret: string
url: string
}
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
export class OsmConnection {
public static readonly oauth_configs = {
osm: {
oauth_consumer_key: "hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem",
oauth_secret: "wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI",
url: "https://www.openstreetmap.org",
// OAUTH 1.0 application
// https://www.openstreetmap.org/user/Pieter%20Vander%20Vennet/oauth_clients/7404
},
"osm-test": {
oauth_consumer_key: "Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2",
oauth_secret: "3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn",
url: "https://master.apis.dev.openstreetmap.org",
},
}
public static readonly oauth_configs: Record<string, AuthConfig> =
config.config.oauth_credentials
public auth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
@ -53,11 +50,7 @@ export class OsmConnection {
"not-attempted"
)
public preferencesHandler: OsmPreferences
public readonly _oauth_config: {
oauth_consumer_key: string
oauth_secret: string
url: string
}
public readonly _oauth_config: AuthConfig
private readonly _dryRun: Store<boolean>
private fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
@ -83,6 +76,19 @@ export class OsmConnection {
console.debug("Using backend", this._oauth_config.url)
this._iframeMode = Utils.runningFromConsole ? false : window !== window.top
// Check if there are settings available in environment variables, and if so, use those
if (
import.meta.env.VITE_OSM_OAUTH_CLIENT_ID !== undefined &&
import.meta.env.VITE_OSM_OAUTH_SECRET !== undefined
) {
console.debug("Using environment variables for oauth config")
this._oauth_config = {
oauth_client_id: import.meta.env.VITE_OSM_OAUTH_CLIENT_ID,
oauth_secret: import.meta.env.VITE_OSM_OAUTH_SECRET,
url: "https://www.openstreetmap.org",
}
}
this.userDetails = new UIEventSource<UserDetails>(
new UserDetails(this._oauth_config.url),
"userDetails"
@ -190,6 +196,9 @@ export class OsmConnection {
const self = this
console.log("Trying to log in...")
this.updateAuthObject()
LocalStorageSource.Get("location_before_login").setData(
Utils.runningFromConsole ? undefined : window.location.href
)
this.auth.xhr(
{
method: "GET",
@ -202,13 +211,8 @@ export class OsmConnection {
if (err.status == 401) {
console.log("Clearing tokens...")
// Not authorized - our token probably got revoked
// Reset all the tokens
const tokens = [
"https://www.openstreetmap.orgoauth_request_token_secret",
"https://www.openstreetmap.orgoauth_token",
"https://www.openstreetmap.orgoauth_token_secret",
]
tokens.forEach((token) => localStorage.removeItem(token))
self.auth.logout()
self.LogOut()
}
return
}
@ -310,6 +314,7 @@ export class OsmConnection {
): Promise<any> {
return await this.interact(path, "POST", header, content)
}
public async put(
path: string,
content?: string,
@ -486,15 +491,29 @@ export class OsmConnection {
// Same for an iframe...
this.auth = new osmAuth({
oauth_consumer_key: this._oauth_config.oauth_consumer_key,
oauth_secret: this._oauth_config.oauth_secret,
client_id: this._oauth_config.oauth_client_id,
url: this._oauth_config.url,
landing: standalone ? undefined : window.location.href,
scope: "read_prefs write_prefs write_api write_gpx write_notes",
redirect_uri: Utils.runningFromConsole
? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html",
singlepage: !standalone,
auto: true,
})
}
/**
* To be called by land.html
*/
public finishLogin(callback: (previousURL: string) => void) {
this.auth.authenticate(function () {
// Fully authed at this point
console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login")
callback(previousLocation.data)
})
}
private CheckForMessagesContinuously() {
const self = this
if (this.isChecking) {

View file

@ -10,7 +10,12 @@ 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)
const queryParam = QueryParameters.GetQueryParameter(
key,
"" + defaultValue,
documentation,
{ stackOffset: -1 }
)
// 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(
@ -46,10 +51,9 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
*/
public readonly layoutToUse: LayoutConfig
public readonly featureSwitchUserbadge: UIEventSource<boolean>
public readonly featureSwitchEnableLogin: 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>
@ -73,10 +77,10 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
// Helper function to initialize feature switches
this.featureSwitchUserbadge = FeatureSwitchUtils.initSwitch(
"fs-userbadge",
this.featureSwitchEnableLogin = FeatureSwitchUtils.initSwitch(
"fs-enable-login",
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."
"Disables/Enables logging in and thus disables editing all together. This effectively puts MapComplete into read-only mode."
)
this.featureSwitchSearch = FeatureSwitchUtils.initSwitch(
"fs-search",
@ -94,11 +98,7 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
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,
@ -196,12 +196,6 @@ export default class FeatureSwitchState extends OsmConnectionFeatureSwitches {
)
)
this.featureSwitchUserbadge.addCallbackAndRun((userbadge) => {
if (!userbadge) {
this.featureSwitchAddNew.setData(false)
}
})
this.backgroundLayerId = QueryParameters.GetQueryParameter(
"background",
layoutToUse?.defaultBackgroundId ?? "osm",

View file

@ -4,27 +4,29 @@
import { UIEventSource } from "../UIEventSource"
import Hash from "./Hash"
import { Utils } from "../../Utils"
import doc = Mocha.reporters.doc
export class QueryParameters {
static defaults: Record<string, string> = {}
static documentation: Map<string, string> = new Map<string, string>()
private static order: string[] = ["layout", "test", "z", "lat", "lon"]
protected static readonly _wasInitialized: Set<string> = new Set()
protected static readonly knownSources: Record<string, UIEventSource<string>> = {}
private static order: string[] = ["layout", "test", "z", "lat", "lon"]
private static initialized = false
public static GetQueryParameter(
key: string,
deflt: string,
documentation?: string
documentation?: string,
options?: {
stackOffset?: number
}
): UIEventSource<string> {
if (!this.initialized) {
this.init()
}
if (Utils.runningFromConsole) {
const location = Utils.getLocationInCode(-1)
const location = Utils.getLocationInCode(-1 + (options?.stackOffset ?? 0))
documentation +=
"\n\nThis documentation is defined in the source code at [" +
@ -63,7 +65,7 @@ export class QueryParameters {
documentation?: string
): UIEventSource<boolean> {
return UIEventSource.asBoolean(
QueryParameters.GetQueryParameter(key, "" + deflt, documentation)
QueryParameters.GetQueryParameter(key, "" + deflt, documentation, { stackOffset: -1 })
)
}
@ -71,6 +73,7 @@ export class QueryParameters {
this.init()
return QueryParameters._wasInitialized.has(key)
}
public static initializedParameters(): ReadonlyArray<string> {
return Array.from(QueryParameters._wasInitialized.keys())
}
@ -105,14 +108,12 @@ export class QueryParameters {
}
}
/**
* Set the query parameters of the page location
* @constructor
* @private
*/
private static Serialize() {
const parts = []
public static GetParts(exclude?: Set<string>) {
const parts: string[] = []
for (const key of QueryParameters.order) {
if (exclude?.has(key)) {
continue
}
if (QueryParameters.knownSources[key]?.data === undefined) {
continue
}
@ -131,6 +132,16 @@ export class QueryParameters {
encodeURIComponent(QueryParameters.knownSources[key].data)
)
}
return parts
}
/**
* Set the query parameters of the page location
* @constructor
* @private
*/
private static Serialize() {
const parts = QueryParameters.GetParts()
if (!Utils.runningFromConsole) {
// Don't pollute the history every time a parameter changes
try {
@ -148,4 +159,8 @@ export class QueryParameters {
QueryParameters._wasInitialized.clear()
QueryParameters.order = []
}
static GetDefaultFor(key: string): string {
return QueryParameters.defaults[key]
}
}

View file

@ -1,9 +1,25 @@
import ThemeViewState from "../../Models/ThemeViewState"
import Hash from "./Hash"
import { MenuState } from "../../Models/MenuState"
export default class ThemeViewStateHashActor {
private readonly _state: ThemeViewState
public static readonly documentation = [
"The URL-hash can contain multiple values:",
"",
"- The id of the currently selected object, e.g. `node/1234`",
"- The currently opened menu view",
"- The base64-encoded JSON-file specifying a custom theme (only when loading)",
"",
"### Possible hashes to open a menu",
"",
"The possible hashes are:",
"",
MenuState._menuviewTabs.map((tab) => "`menu:" + tab + "`").join(","),
MenuState._themeviewTabs.map((tab) => "`theme-menu:" + tab + "`").join(","),
]
/**
* Converts the hash to the appropriate themeview state and, vice versa, sets the hash.
*
@ -100,7 +116,7 @@ export default class ThemeViewStateHashActor {
private loadStateFromHash(hash: string) {
const state = this._state
const parts = hash.split(";")
const parts = hash.split(":")
outer: for (const { toggle, name, showOverOthers, submenu } of state.guistate.allToggles) {
for (const part of parts) {
if (part === name) {

View file

@ -1,14 +1,13 @@
import { Utils } from "../Utils"
import * as meta from "../../package.json"
import { Utils } from "../Utils"
export type PriviligedLayerType = (typeof Constants.priviliged_layers)[number]
export default class Constants {
public static vNumber = meta.version
public static ImgurApiKey = "7070e7167f0a25a"
public static readonly mapillary_client_token_v4 =
"MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
public static ImgurApiKey = meta.config.api_keys.imgur
public static readonly mapillary_client_token_v4 = meta.config.api_keys.mapillary_v4
/**
* API key for Maproulette
@ -19,15 +18,7 @@ export default class Constants {
*/
public static readonly MaprouletteApiKey = ""
public static defaultOverpassUrls = [
// The official instance, 10000 queries per day per project allowed
"https://overpass-api.de/api/interpreter",
// 'Fair usage'
"https://overpass.kumi.systems/api/interpreter",
// Offline: "https://overpass.nchc.org.tw/api/interpreter",
"https://overpass.openstreetmap.ru/cgi/interpreter",
// Doesn't support nwr: "https://overpass.openstreetmap.fr/api/interpreter"
]
public static defaultOverpassUrls = meta.config.default_overpass_urls
public static readonly added_by_default = [
"selected_element",
@ -100,6 +91,7 @@ export default class Constants {
"etymology",
"food",
"cafes_and_pubs",
"shops",
"playgrounds",
"hailhydrant",
"toilets",
@ -113,9 +105,8 @@ export default class Constants {
* In seconds
*/
static zoomToLocationTimeout = 15
static countryCoderEndpoint: string =
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/latlon2country"
public static readonly OsmPreferenceKeyPicturesLicense = "pictures-license"
static countryCoderEndpoint: string = meta.config.country_coder_host
/**
* These are the values that are allowed to use as 'backdrop' icon for a map pin
*/

View file

@ -1,14 +1,14 @@
import {Store, UIEventSource} from "../Logic/UIEventSource"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import LayerConfig from "./ThemeConfig/LayerConfig"
import {OsmConnection} from "../Logic/Osm/OsmConnection"
import {LocalStorageSource} from "../Logic/Web/LocalStorageSource"
import {QueryParameters} from "../Logic/Web/QueryParameters"
import {FilterConfigOption} from "./ThemeConfig/FilterConfig"
import {TagsFilter} from "../Logic/Tags/TagsFilter"
import {Utils} from "../Utils"
import {TagUtils} from "../Logic/Tags/TagUtils"
import {And} from "../Logic/Tags/And"
import {GlobalFilter} from "./GlobalFilter"
import { OsmConnection } from "../Logic/Osm/OsmConnection"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import { FilterConfigOption } from "./ThemeConfig/FilterConfig"
import { TagsFilter } from "../Logic/Tags/TagsFilter"
import { Utils } from "../Utils"
import { TagUtils } from "../Logic/Tags/TagUtils"
import { And } from "../Logic/Tags/And"
import { GlobalFilter } from "./GlobalFilter"
export default class FilteredLayer {
/**

View file

@ -50,11 +50,15 @@ export class MenuState {
)
public highlightedUserSetting: UIEventSource<string> = new UIEventSource<string>(undefined)
constructor(themeid: string = "") {
constructor(shouldOpenWelcomeMessage: boolean, themeid: string = "") {
// Note: this class is _not_ responsible to update the Hash, @see ThemeViewStateHashActor for this
if (themeid) {
themeid += "-"
}
this.themeIsOpened = LocalStorageSource.GetParsed(themeid + "thememenuisopened", true)
this.themeIsOpened = LocalStorageSource.GetParsed(
themeid + "thememenuisopened",
shouldOpenWelcomeMessage
)
this.themeViewTabIndex = LocalStorageSource.GetParsed(themeid + "themeviewtabindex", 0)
this.themeViewTab = this.themeViewTabIndex.sync(
(i) => MenuState._themeviewTabs[i],

View file

@ -111,7 +111,7 @@ export default class CreateNoteImportLayer extends Conversion<LayerConfigJson, L
},
calculatedTags: [
"_first_comment=get(feat)('comments')[0].text.toLowerCase()",
"_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.osm.be/\\([a-zA-Z_-]+\\)\\(.html\\)?.*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
"_trigger_index=(() => {const lines = feat.properties['_first_comment'].split('\\n'); const matchesMapCompleteURL = lines.map(l => l.match(\".*https://mapcomplete.\\(org|osm.be\\)/\\([a-zA-Z_-]+\\)\\(.html\\)?.*#import\")); const matchedIndexes = matchesMapCompleteURL.map((doesMatch, i) => [doesMatch !== null, i]).filter(v => v[0]).map(v => v[1]); return matchedIndexes[0] })()",
"_comments_count=get(feat)('comments').length",
"_intro=(() => {const lines = get(feat)('comments')[0].text.split('\\n'); lines.splice(get(feat)('_trigger_index')-1, lines.length); return lines.filter(l => l !== '').join('<br/>');})()",
"_tags=(() => {let lines = get(feat)('comments')[0].text.split('\\n').map(l => l.trim()); lines.splice(0, get(feat)('_trigger_index') + 1); lines = lines.filter(l => l != ''); return lines.join(';');})()",

File diff suppressed because it is too large Load diff

View file

@ -228,7 +228,7 @@ export interface LayoutConfigJson {
*
* Note that {lat},{lon},{zoom}, {language} and {theme} will be replaced
*
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.osm.be/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
* Default: {icon: "./assets/svg/pop-out.svg", href: 'https://mapcomplete.org/{theme}.html?lat={lat}&lon={lon}&z={zoom}, requirements: ["iframe","no-welcome-message]},
*
*/
extraLink?: ExtraLinkConfigJson

View file

@ -41,6 +41,26 @@ export interface TagRenderingConfigJson {
| Record<string, string>
| { special: Record<string, string | Record<string, string>> & { type: string } }
/**
* An icon shown next to the rendering; typically shown pretty small
* This is only shown next to the "render" value
* Type: icon
*/
icon?:
| string
| {
/**
* The path to the icon
* Type: icon
*/
path: string
/**
* 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)
*/
class?: "small" | "medium" | "large" | string
}
/**
* Only show this tagrendering (or ask the question) if the selected object also matches the tags specified as `condition`.
*

View file

@ -495,9 +495,7 @@ export default class LayerConfig extends WithContextLoader {
usingLayer = [
new Title("Themes using this layer", 4),
new List(
(usedInThemes ?? []).map(
(id) => new Link(id, "https://mapcomplete.osm.be/" + id)
)
(usedInThemes ?? []).map((id) => new Link(id, "https://mapcomplete.org/" + id))
),
]
}
@ -542,7 +540,7 @@ export default class LayerConfig extends WithContextLoader {
new Combine([
new Link(
Utils.runningFromConsole
? "<img src='https://mapcomplete.osm.be/assets/svg/statistics.svg' height='18px'>"
? "<img src='https://mapcomplete.org/assets/svg/statistics.svg' height='18px'>"
: Svg.statistics_svg().SetClass("w-4 h-4 mr-2"),
"https://taginfo.openstreetmap.org/keys/" + values.key + "#values",
true
@ -579,7 +577,7 @@ export default class LayerConfig extends WithContextLoader {
// This is for the documentation in a markdown-file, so we have to use raw HTML
if (icon !== undefined) {
iconImg = new FixedUiElement(
`<img src='https://mapcomplete.osm.be/${icon}' height="100px"> `
`<img src='https://mapcomplete.org/${icon}' height="100px"> `
)
}
} else {

View file

@ -1,23 +1,24 @@
import { Translation, TypedTranslation } from "../../UI/i18n/Translation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import {Translation, TypedTranslation} from "../../UI/i18n/Translation"
import {TagsFilter} from "../../Logic/Tags/TagsFilter"
import Translations from "../../UI/i18n/Translations"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import {TagUtils, UploadableTag} from "../../Logic/Tags/TagUtils"
import {And} from "../../Logic/Tags/And"
import {Utils} from "../../Utils"
import {Tag} from "../../Logic/Tags/Tag"
import BaseUIElement from "../../UI/BaseUIElement"
import Combine from "../../UI/Base/Combine"
import Title from "../../UI/Base/Title"
import Link from "../../UI/Base/Link"
import List from "../../UI/Base/List"
import {
MappingConfigJson,
QuestionableTagRenderingConfigJson,
} from "./Json/QuestionableTagRenderingConfigJson"
import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import { Paragraph } from "../../UI/Base/Paragraph"
import {MappingConfigJson, QuestionableTagRenderingConfigJson,} from "./Json/QuestionableTagRenderingConfigJson"
import {FixedUiElement} from "../../UI/Base/FixedUiElement"
import {Paragraph} from "../../UI/Base/Paragraph"
import Svg from "../../Svg"
import Validators, { ValidatorType } from "../../UI/InputElement/Validators"
import Validators, {ValidatorType} from "../../UI/InputElement/Validators"
export interface Icon {
}
export interface Mapping {
readonly if: UploadableTag
@ -45,6 +46,8 @@ export interface Mapping {
export default class TagRenderingConfig {
public readonly id: string
public readonly render?: TypedTranslation<object>
public readonly renderIcon?: string
public readonly renderIconClass?: string
public readonly question?: TypedTranslation<object>
public readonly questionhint?: TypedTranslation<object>
public readonly condition?: TagsFilter
@ -58,7 +61,7 @@ export default class TagRenderingConfig {
public readonly freeform?: {
readonly key: string
readonly type: string
readonly type: ValidatorType
readonly placeholder: Translation
readonly addExtraTags: UploadableTag[]
readonly inline: boolean
@ -121,9 +124,16 @@ export default class TagRenderingConfig {
this.question = Translations.T(json.question, translationKey + ".question")
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.description = Translations.T(json.description, translationKey + ".description")
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
this.condition = TagUtils.Tag(json.condition ?? {and: []}, `${context}.condition`)
if (typeof json.icon === "string") {
this.renderIcon = json.icon
this.renderIconClass = "small"
}else if (typeof json.icon === "object"){
this.renderIcon = json.icon.path
this.renderIconClass = json.icon.class
}
this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] },
json.metacondition ?? {and: []},
`${context}.metacondition`
)
if (json.freeform) {
@ -133,7 +143,17 @@ export default class TagRenderingConfig {
) {
throw `Freeform.addExtraTags should be a list of strings - not a single string (at ${context})`
}
const type = json.freeform.type ?? "string"
if (
json.freeform.type &&
Validators.availableTypes.indexOf(<any>json.freeform.type) < 0
) {
throw `At ${context}: invalid type, perhaps you meant ${Utils.sortedByLevenshteinDistance(
json.freeform.key,
<any>Validators.availableTypes,
(s) => <any>s
)}`
}
const type: ValidatorType = <any>json.freeform.type ?? "string"
let placeholder: Translation = Translations.T(json.freeform.placeholder)
if (placeholder === undefined) {
@ -222,21 +242,23 @@ export default class TagRenderingConfig {
if (txt === "") {
throw context + " Rendering for language " + ln + " is empty"
}
if (txt.indexOf("{" + this.freeform.key + "}") >= 0) {
if (txt.indexOf("{" + this.freeform.key + "}") >= 0 || txt.indexOf("&LBRACE" + this.freeform.key + "&RBRACE") ) {
continue
}
if (txt.indexOf("{" + this.freeform.key + ":") >= 0) {
continue
}
if (txt.indexOf("{canonical(" + this.freeform.key + ")") >= 0) {
continue
}
if (
this.freeform.type === "opening_hours" &&
txt.indexOf("{opening_hours_table(") >= 0
) {
continue
}
const keyFirstArg = ["canonical", "fediverse_link"]
if (keyFirstArg.some(funcName => txt.indexOf(`{${funcName}(${this.freeform.key}`) >= 0)) {
continue
}
if (
this.freeform.type === "wikidata" &&
txt.indexOf("{wikipedia(" + this.freeform.key) >= 0
@ -522,7 +544,7 @@ export default class TagRenderingConfig {
*/
public GetRenderValueWithImage(
tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string } | undefined {
): { then: TypedTranslation<any>; icon?: string, iconClass?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
return undefined
@ -541,7 +563,7 @@ export default class TagRenderingConfig {
}
if (this.freeform?.key === undefined || tags[this.freeform.key] !== undefined) {
return { then: this.render }
return {then: this.render, icon: this.renderIcon, iconClass: this.renderIconClass}
}
return undefined
@ -622,7 +644,7 @@ export default class TagRenderingConfig {
*
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
* @param multiSelectedMapping (Only used if multiAnswer == true): all the mappings that must be applied. Set multiSelectedMapping[mappings.length] to use the freeform as well
* @param currentProperties: The current properties of the object for which the question should be answered
* @param currentProperties The current properties of the object for which the question should be answered
*/
public constructChangeSpecification(
freeformValue: string | undefined,
@ -685,38 +707,42 @@ export default class TagRenderingConfig {
return undefined
}
return and
} else {
// Is at least one mapping shown in the answer?
const someMappingIsShown = this.mappings.some((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
const isHidden = m.hideInAnswer.matchesProperties(currentProperties)
return !isHidden
})
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
const useFreeform =
freeformValue !== undefined &&
(singleSelectedMapping === this.mappings.length || !someMappingIsShown)
if (useFreeform) {
return new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
} else if (singleSelectedMapping !== undefined) {
return new And([
this.mappings[singleSelectedMapping].if,
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
])
} else {
console.warn("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
freeformValue,
singleSelectedMapping,
multiSelectedMapping,
currentProperties,
})
return undefined
}
// Is at least one mapping shown in the answer?
const someMappingIsShown = this.mappings.some((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
const isHidden = m.hideInAnswer.matchesProperties(currentProperties)
return !isHidden
})
// If all mappings are hidden for the current tags, we can safely assume that we should use the freeform key
const useFreeform =
freeformValue !== undefined &&
(singleSelectedMapping === this.mappings.length ||
!someMappingIsShown ||
singleSelectedMapping === undefined)
if (useFreeform) {
return new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
])
} else if (singleSelectedMapping !== undefined) {
return new And([
this.mappings[singleSelectedMapping].if,
...(this.mappings[singleSelectedMapping].addExtraTags ?? []),
])
} else {
console.error("TagRenderingConfig.ConstructSpecification has a weird fallback for", {
freeformValue,
singleSelectedMapping,
multiSelectedMapping,
currentProperties,
useFreeform,
})
return undefined
}
}
@ -759,7 +785,7 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) {
msgs.push(
"Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {})
m.ifnot.asHumanString(true, false, {})
)
}
return msgs
@ -789,12 +815,12 @@ export default class TagRenderingConfig {
this.description,
this.question !== undefined
? new Combine([
"The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
])
"The question is ",
new FixedUiElement(this.question.txt).SetClass("font-bold bold"),
])
: new FixedUiElement(
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
"This tagrendering has no question and is thus read-only"
).SetClass("italic"),
new Combine(withRender),
mappings,
condition,

View file

@ -114,15 +114,18 @@ export default class ThemeViewState implements SpecialVisualizationState {
constructor(layout: LayoutConfig) {
this.layout = layout
this.guistate = new MenuState(layout.id)
this.featureSwitches = new FeatureSwitchState(layout)
this.guistate = new MenuState(
this.featureSwitches.featureSwitchWelcomeMessage.data,
layout.id
)
this.map = new UIEventSource<MlMap>(undefined)
const initial = new InitialMapPositioning(layout)
this.mapProperties = new MapLibreAdaptor(this.map, initial)
const geolocationState = new GeoLocationState()
this.featureSwitches = new FeatureSwitchState(layout)
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchUserbadge
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
this.osmConnection = new OsmConnection({
dryRun: this.featureSwitches.featureSwitchIsTesting,
@ -201,6 +204,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
(id) => self.layerState.filteredLayers.get(id).isDisplayed,
this.fullNodeDatabase
)
this.indexedFeatures = layoutSource
const empty = []
@ -222,9 +226,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
)
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
this.dataIsLoading = layoutSource.isLoading
this.dataIsLoading.addCallbackAndRunD((loading) =>
console.log("Data is loading?", loading)
)
const indexedElements = this.indexedFeatures
this.featureProperties = new FeaturePropertiesStore(indexedElements)
@ -342,13 +343,13 @@ export default class ThemeViewState implements SpecialVisualizationState {
[fs.layer.isDisplayed]
)
if (
!doShowLayer.data &&
(this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name)
) {
if (!doShowLayer.data && this.featureSwitches.featureSwitchFilter.data === false) {
/* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined)
*
* This means that we don't have to filter it, nor do we have to display it
*
* Note: it is tempting to also permanently disable the layer if it is not visible _and_ the layer name is hidden.
* However, this is _not_ correct: the layer might be hidden because zoom is not enough. Zooming in more _will_ reveal the layer!
* */
return
}
@ -469,7 +470,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
new ShowDataLayer(this.map, {
features: new FilteringFeatureSource(last_click_layer, last_click),
doShowLayer: new ImmutableStore(true),
doShowLayer: this.featureSwitches.featureSwitchEnableLogin,
layer: last_click_layer.layerDef,
selectedElement: this.selectedElement,
selectedLayer: this.selectedLayer,

View file

@ -46,7 +46,7 @@ export default class Img extends BaseUIElement {
}
let src = this._src
if (this._src.startsWith("./")) {
src = "https://mapcomplete.osm.be/" + src
src = "https://mapcomplete.org/" + src
}
return "![](" + src + ")"
}

View file

@ -34,6 +34,7 @@
class={twMerge(options.extraClasses, "button text-ellipsis")}
{href}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener" : undefined}
>
<slot name="image">
{#if imageUrl !== undefined}

View file

@ -29,7 +29,7 @@ export default class Table extends BaseUIElement {
const header = Utils.NoNull(headerMarkdownParts).join(" | ")
const headerSep = headerMarkdownParts.map((part) => "-".repeat(part.length + 2)).join(" | ")
const table = this._contents
.map((row) => row.map((el) => el?.AsMarkdown()?.replace("|", "\\|") ?? " ").join(" | "))
.map((row) => row.map((el) => el?.AsMarkdown()?.replaceAll("\\","\\\\")?.replaceAll("|", "\\|") ?? " ").join(" | "))
.join("\n")
return "\n\n" + [header, headerSep, table, ""].join("\n")

View file

@ -35,7 +35,7 @@
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
/>
<div class="flex flex-col">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow noopener" class="font-bold">
{resource.resolved.name ?? resource.resolved.url}
</a>
{resource.resolved?.description}

View file

@ -102,7 +102,7 @@ export default class CopyrightPanel extends Combine {
let bgAttr: BaseUIElement | string = undefined
if (attrText && attrUrl) {
bgAttr =
"<a href='" + attrUrl + "' target='_blank'>" + attrText + "</a>"
"<a href='" + attrUrl + "' target='_blank' rel='noopener'>" + attrText + "</a>"
} else if (attrUrl) {
bgAttr = attrUrl
} else {

View file

@ -0,0 +1,121 @@
<script lang="ts">/**
* A screen showing:
* - A link to share the current view
* - Some query parameters that can be enabled/disabled
* - The code to embed MC as IFrame
*/
import ThemeViewState from "../../Models/ThemeViewState";
import { QueryParameters } from "../../Logic/Web/QueryParameters";
import Tr from "../Base/Tr.svelte";
import Translations from "../i18n/Translations";
import { Utils } from "../../Utils";
import Svg from "../../Svg";
import ToSvelte from "../Base/ToSvelte.svelte";
import { DocumentDuplicateIcon } from "@rgossiaux/svelte-heroicons/outline";
export let state: ThemeViewState;
const tr = Translations.t.general.sharescreen;
let url = window.location;
let linkToShare: string = undefined;
/**
* In some cases (local deploys, custom themes), we need to set the URL to `/theme.html?layout=xyz` instead of `/xyz?...`
*/
let needsThemeRedirect = url.port !== "" || url.hostname.match(/^[0-9]/) || !state.layout.official;
let layoutId = state.layout.id;
let baseLink = url.protocol + "//" + url.host + "/" + (needsThemeRedirect ? "theme.html?layout=" + layoutId + "&" : layoutId + "?");
let showWelcomeMessage = true;
let enableLogin = true;
$: {
const layout = state.layout;
let excluded = Utils.NoNull([
showWelcomeMessage ? undefined : "fs-welcome-message",
enableLogin ? undefined : "fs-enable-login"
]);
linkToShare = baseLink + QueryParameters.GetParts(new Set(excluded))
.concat(excluded.map(k => k + "=" + false))
.join("&");
if (layout.definitionRaw !== undefined) {
linkToShare += "&userlayout=" + (layout.definedAtUrl ?? layout.id);
}
}
async function shareCurrentLink() {
await navigator.share({
title: Translations.W(state.layout.title)?.ConstructElement().textContent ?? "MapComplete",
text: Translations.W(state.layout.description)?.ConstructElement().textContent ?? "",
url: linkToShare
});
}
let isCopied = false;
async function copyCurrentLink() {
await navigator.clipboard.writeText(linkToShare);
isCopied = true;
await Utils.waitFor(5000);
isCopied = false;
}
</script>
<div>
<Tr t={tr.intro} />
<div class="flex">
{#if typeof navigator?.share === "function"}
<button class="w-8 h-8 p-1 shrink-0" on:click={shareCurrentLink}>
<ToSvelte construct={Svg.share_svg()} />
</button>
{/if}
{#if navigator.clipboard !== undefined}
<button class="w-8 h-8 p-1 shrink-0 no-image-background" on:click={copyCurrentLink}>
<DocumentDuplicateIcon />
</button>
{/if}
<div class="literal-code" on:click={e => Utils.selectTextIn(e.target)}>
{linkToShare}
</div>
</div>
<div class="flex justify-center">
{#if isCopied}
<Tr t={tr.copiedToClipboard} cls="thanks m-2" />
{/if}
</div>
<Tr t={ tr.embedIntro} />
<div class="flex flex-col my-1 link-underline">
<label>
<input bind:checked={showWelcomeMessage} type="checkbox" />
<Tr t={tr.fsWelcomeMessage} />
</label>
<label>
<input bind:checked={enableLogin} type="checkbox" />
<Tr t={tr.fsUserbadge} />
</label>
</div>
<div class="literal-code m-1">
&lt;span class="literal-code iframe-code-block"&gt; <br />
&lt;iframe src="${url}" <br />
allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" <br />
title="${state.layout.title?.txt ?? "MapComplete" } with MapComplete"&gt; <br />
&lt;/iframe&gt; <br />
&lt;/span&gt;
</div>
<Tr t={tr.documentation} cls="link-underline"/>
</div>

View file

@ -1,257 +0,0 @@
/* eslint-disable prefer-const */
import { VariableUiElement } from "../Base/VariableUIElement"
import { Translation } from "../i18n/Translation"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { InputElement } from "../Input/InputElement"
import { CheckBox } from "../Input/Checkboxes"
import { SubtleButton } from "../Base/SubtleButton"
import LZString from "lz-string"
import { SpecialVisualizationState } from "../SpecialVisualization"
export class ShareScreen extends Combine {
constructor(state: SpecialVisualizationState) {
const layout = state?.layout
const tr = Translations.t.general.sharescreen
const optionCheckboxes: InputElement<boolean>[] = []
const optionParts: Store<string>[] = []
const includeLocation = new CheckBox(tr.fsIncludeCurrentLocation, true)
optionCheckboxes.push(includeLocation)
const currentLocation = state.mapProperties.location
const zoom = state.mapProperties.zoom
optionParts.push(
includeLocation.GetValue().map(
(includeL) => {
if (currentLocation === undefined) {
return null
}
if (includeL) {
return [
["z", zoom.data],
["lat", currentLocation.data?.lat],
["lon", currentLocation.data?.lon],
]
.filter((p) => p[1] !== undefined)
.map((p) => p[0] + "=" + p[1])
.join("&")
} else {
return null
}
},
[currentLocation, zoom]
)
)
function fLayerToParam(flayer: {
isDisplayed: UIEventSource<boolean>
layerDef: LayerConfig
}) {
if (flayer.isDisplayed.data) {
return null // Being displayed is the default
}
return "layer-" + flayer.layerDef.id + "=" + flayer.isDisplayed.data
}
const currentLayer: Store<
{ id: string; name: string | Record<string, string> } | undefined
> = state.mapProperties.rasterLayer.map((l) => l?.properties)
const currentBackground = new VariableUiElement(
currentLayer.map((layer) => {
return tr.fsIncludeCurrentBackgroundMap.Subs({ name: layer?.name ?? "" })
})
)
const includeCurrentBackground = new CheckBox(currentBackground, true)
optionCheckboxes.push(includeCurrentBackground)
optionParts.push(
includeCurrentBackground.GetValue().map(
(includeBG) => {
if (includeBG) {
return "background=" + currentLayer.data?.id
} else {
return null
}
},
[currentLayer]
)
)
const includeLayerChoices = new CheckBox(tr.fsIncludeCurrentLayers, true)
optionCheckboxes.push(includeLayerChoices)
optionParts.push(
includeLayerChoices.GetValue().map(
(includeLayerSelection) => {
if (includeLayerSelection) {
return Utils.NoNull(
Array.from(state.layerState.filteredLayers.values()).map(fLayerToParam)
).join("&")
} else {
return null
}
},
Array.from(state.layerState.filteredLayers.values()).map(
(flayer) => flayer.isDisplayed
)
)
)
const switches = [
{ urlName: "fs-userbadge", human: tr.fsUserbadge },
{ urlName: "fs-search", human: tr.fsSearch },
{ urlName: "fs-welcome-message", human: tr.fsWelcomeMessage },
{ urlName: "fs-layers", human: tr.fsLayers },
{ urlName: "fs-add-new", human: tr.fsAddNew },
{ urlName: "fs-geolocation", human: tr.fsGeolocation },
]
for (const swtch of switches) {
const checkbox = new CheckBox(Translations.W(swtch.human))
optionCheckboxes.push(checkbox)
optionParts.push(
checkbox.GetValue().map((isEn) => {
if (isEn) {
return null
} else {
return `${swtch.urlName}=false`
}
})
)
}
if (layout.definitionRaw !== undefined) {
optionParts.push(new UIEventSource("userlayout=" + (layout.definedAtUrl ?? layout.id)))
}
const options = new Combine(optionCheckboxes).SetClass("flex flex-col")
const url = (currentLocation ?? new UIEventSource(undefined)).map(() => {
const host = window.location.host
let path = window.location.pathname
path = path.substr(0, path.lastIndexOf("/"))
let id = layout.id.toLowerCase()
if (layout.definitionRaw !== undefined) {
id = "theme.html"
}
let literalText = `https://${host}${path}/${id}`
let hash = ""
if (layout.definedAtUrl === undefined && layout.definitionRaw !== undefined) {
hash = "#" + LZString.compressToBase64(Utils.MinifyJSON(layout.definitionRaw))
}
const parts = Utils.NoEmpty(
Utils.NoNull(optionParts.map((eventSource) => eventSource.data))
)
if (parts.length === 0) {
return literalText + hash
}
return literalText + "?" + parts.join("&") + hash
}, optionParts)
const iframeCode = new VariableUiElement(
url.map((url) => {
return `<span class='literal-code iframe-code-block'>
&lt;iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
layout.title?.txt ?? "MapComplete"
} with MapComplete"&gt;&lt;/iframe&gt
</span>`
})
)
const linkStatus = new UIEventSource<string | Translation>("")
const link = new VariableUiElement(
url.map(
(url) =>
`<input type="text" value=" ${url}" id="code-link--copyable" style="width:90%">`
)
).onClick(async () => {
const shareData = {
title: Translations.W(layout.title)?.ConstructElement().textContent ?? "",
text: Translations.W(layout.description)?.ConstructElement().textContent ?? "",
url: url.data,
}
function rejected() {
const copyText = document.getElementById("code-link--copyable")
// @ts-ignore
copyText.select()
// @ts-ignore
copyText.setSelectionRange(0, 99999) /*For mobile devices*/
document.execCommand("copy")
const copied = tr.copiedToClipboard.Clone()
copied.SetClass("thanks")
linkStatus.setData(copied)
}
try {
navigator
.share(shareData)
.then(() => {
const thx = tr.thanksForSharing.Clone()
thx.SetClass("thanks")
linkStatus.setData(thx)
}, rejected)
.catch(rejected)
} catch (err) {
rejected()
}
})
let downloadThemeConfig: BaseUIElement = undefined
if (layout.definitionRaw !== undefined) {
const downloadThemeConfigAsJson = new SubtleButton(
Svg.download_svg(),
new Combine([tr.downloadCustomTheme, tr.downloadCustomThemeHelp.SetClass("subtle")])
.onClick(() => {
Utils.offerContentsAsDownloadableFile(
layout.definitionRaw,
layout.id + ".mapcomplete-theme-definition.json",
{
mimetype: "application/json",
}
)
})
.SetClass("flex flex-col")
)
let editThemeConfig: BaseUIElement = undefined
if (layout.definedAtUrl === undefined) {
const patchedDefinition = JSON.parse(layout.definitionRaw)
patchedDefinition["language"] = Object.keys(patchedDefinition.title)
editThemeConfig = new SubtleButton(
Svg.pencil_svg(),
"Edit this theme on the custom theme generator",
{
url: `https://pietervdvn.github.io/mc/legacy/070/customGenerator.html#${btoa(
JSON.stringify(patchedDefinition)
)}`,
}
)
}
downloadThemeConfig = new Combine([
downloadThemeConfigAsJson,
editThemeConfig,
]).SetClass("flex flex-col")
}
super([
tr.intro,
link,
new VariableUiElement(linkStatus),
downloadThemeConfig,
tr.addToHomeScreen,
tr.embedIntro,
options,
iframeCode,
])
this.SetClass("flex flex-col link-underline")
}
}

View file

@ -7,7 +7,7 @@
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import Tr from "../Base/Tr.svelte"
import SubtleLink from "../Base/SubtleLink.svelte"
import Translations from "../i18n/Translations"
import Translations from "../i18n/Translations"
export let theme: LayoutInformation
export let isCustom: boolean = false

View file

@ -45,9 +45,7 @@
<Tr t={layout.description} />
<Tr t={Translations.t.general.welcomeExplanation.general} />
{#if layout.layers.some((l) => l.presets?.length > 0)}
<If condition={state.featureSwitches.featureSwitchAddNew}>
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
</If>
{/if}
<Tr t={layout.descriptionTail} />

View file

@ -29,7 +29,7 @@
<div class="gap-4 md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3">
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<!-- TODO: doesn't work if first theme is hidden -->
<!-- TODO: doesn't work if first theme is hidden -->
{#if theme === firstTheme && !isCustom && $search !== "" && $search !== undefined}
<ThemeButton
{theme}

View file

@ -37,6 +37,7 @@
<a
href={osmConnection.Backend() + "/profile/edit"}
target="_blank"
rel="noopener"
class="link-no-underline flex items-center self-end"
>
<PencilAltIcon slot="image" class="h-8 w-8 p-2" />

View file

@ -73,7 +73,7 @@ export class ImageUploadFlow extends Toggle {
]).SetClass("w-full flex justify-center items-center")
const licenseStore = state?.osmConnection?.GetPreference(
Constants.OsmPreferenceKeyPicturesLicense,
"pictures-license",
"CC0"
)

View file

@ -1,6 +1,6 @@
import BaseUIElement from "../BaseUIElement"
import {InputElement} from "./InputElement"
import {UIEventSource} from "../../Logic/UIEventSource"
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
/**
* @deprecated
@ -67,20 +67,18 @@ export default class FileSelectorButton extends InputElement<FileList> {
if (actualInputElement.files !== null) {
self._value.setData(actualInputElement.files)
}
actualInputElement.classList.remove("glowing-shadow");
actualInputElement.classList.remove("glowing-shadow")
e.preventDefault()
})
el.appendChild(actualInputElement)
function setDrawAttention(isOn: boolean){
if(isOn){
function setDrawAttention(isOn: boolean) {
if (isOn) {
label.classList.add("glowing-shadow")
}else{
} else {
label.classList.remove("glowing-shadow")
}
}
@ -90,10 +88,9 @@ export default class FileSelectorButton extends InputElement<FileList> {
setDrawAttention(true)
// Style the drag-and-drop as a "copy file" operation.
event.dataTransfer.dropEffect = "copy"
})
window.document.addEventListener("dragenter", () =>{
window.document.addEventListener("dragenter", () => {
setDrawAttention(true)
})
@ -101,7 +98,6 @@ export default class FileSelectorButton extends InputElement<FileList> {
setDrawAttention(false)
})
el.addEventListener("drop", (event) => {
event.stopPropagation()
event.preventDefault()

View file

@ -63,6 +63,7 @@
}
if (unit && isNaN(Number(v))) {
console.debug("Not a number, but a unit is required")
value.setData(undefined)
return
}

View file

@ -16,13 +16,21 @@ export abstract class Validator {
/**
* What HTML-inputmode to use
*/
public readonly inputmode?: string
public readonly inputmode?:
| "none"
| "text"
| "tel"
| "url"
| "email"
| "numeric"
| "decimal"
| "search"
public readonly textArea: boolean
constructor(
name: string,
explanation: string | BaseUIElement,
inputmode?: string,
inputmode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search",
textArea?: false | boolean
) {
this.name = name

View file

@ -18,6 +18,7 @@ import ColorValidator from "./Validators/ColorValidator"
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import FediverseValidator from "./Validators/FediverseValidator";
export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -39,6 +40,7 @@ export default class Validators {
"phone",
"opening_hours",
"color",
"fediverse"
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -58,6 +60,7 @@ export default class Validators {
new PhoneValidator(),
new OpeningHoursValidator(),
new ColorValidator(),
new FediverseValidator()
]
private static _byType = Validators._byTypeConstructor()

View file

@ -0,0 +1,63 @@
import {Validator} from "../Validator"
import {Translation} from "../../i18n/Translation";
import Translations from "../../i18n/Translations";
export default class FediverseValidator extends Validator {
public static readonly usernameAtServer: RegExp = /^@?(\w+)@((\w|\.)+)$/
constructor() {
super("fediverse", "Validates fediverse addresses and normalizes them into `@username@server`-format");
}
/**
* Returns an `@username@host`
* @param s
*/
reformat(s: string): string {
if(!s.startsWith("@")){
s = "@"+s
}
if (s.match(FediverseValidator.usernameAtServer)) {
return s
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return `@${path.substring(1)}@${url.hostname}`;
}
} catch (e) {
// Nothing to do here
}
return undefined
}
getFeedback(s: string): Translation | undefined {
const match = s.match(FediverseValidator.usernameAtServer)
console.log("Match:", match)
if (match) {
const host = match[2]
try {
const url = new URL("https://" + host)
return undefined
} catch (e) {
return Translations.t.validation.fediverse.invalidHost.Subs({host})
}
}
try {
const url = new URL(s)
const path = url.pathname
if (path.match(/^\/\w+$/)) {
return undefined
}
} catch (e) {
// Nothing to do here
}
return Translations.t.validation.fediverse.feedback
}
isValid(s): boolean {
return this.getFeedback(s) === undefined
}
}

View file

@ -1,11 +1,12 @@
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import { Validator } from "../Validator"
import { ValidatorType } from "../Validators"
export default class FloatValidator extends Validator {
inputmode = "decimal"
inputmode: "decimal" = "decimal"
constructor(name?: string, explanation?: string) {
constructor(name?: ValidatorType, explanation?: string) {
super(name ?? "float", explanation ?? "A decimal number", "decimal")
}

View file

@ -1,79 +1,73 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte"
import * as maplibre from "maplibre-gl"
import type {Map} from "maplibre-gl"
import type {Readable, Writable} from "svelte/store"
import {get, writable} from "svelte/store"
import {AvailableRasterLayers} from "../../Models/RasterLayers"
import {Utils} from "../../Utils";
import { onDestroy, onMount } from "svelte"
import * as maplibre from "maplibre-gl"
import type { Map } from "maplibre-gl"
import type { Readable, Writable } from "svelte/store"
import { get, writable } from "svelte/store"
import { AvailableRasterLayers } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
/**
* The 'MaplibreMap' maps various event sources onto MapLibre.
*/
/**
* The 'MaplibreMap' maps various event sources onto MapLibre.
*/
/**
* Beware: this map will _only_ be set by this component
* It should thus be treated as a 'store' by external parties
*/
export let map: Writable<Map>
/**
* Beware: this map will _only_ be set by this component
* It should thus be treated as a 'store' by external parties
*/
export let map: Writable<Map>
let container: HTMLElement
let container: HTMLElement
export let attribution = false
export let center: { lng: number; lat: number } | Readable<{ lng: number; lat: number }> =
writable({ lng: 0, lat: 0 })
export let zoom: Readable<number> = writable(1)
const styleUrl = AvailableRasterLayers.maplibre.properties.url
export let attribution = false
export let center: {lng: number, lat: number} | Readable<{ lng: number; lat: number }> = writable({lng: 0, lat: 0})
export let zoom: Readable<number> = writable(1)
const styleUrl = AvailableRasterLayers.maplibre.properties.url
let _map: Map
onMount(() => {
let _center: {lng: number, lat: number}
if(typeof center["lng"] === "number" && typeof center["lat"] === "number"){
_center = <any> center
}else{
_center = get(<any> center)
}
_map = new maplibre.Map({
container,
style: styleUrl,
zoom: get(zoom),
center: _center,
maxZoom: 24,
interactive: true,
attributionControl: false,
});
_map.on("load", function () {
_map.resize()
})
map.set(_map)
let _map: Map
onMount(() => {
let _center: { lng: number; lat: number }
if (typeof center["lng"] === "number" && typeof center["lat"] === "number") {
_center = <any>center
} else {
_center = get(<any>center)
}
_map = new maplibre.Map({
container,
style: styleUrl,
zoom: get(zoom),
center: _center,
maxZoom: 24,
interactive: true,
attributionControl: false,
})
onDestroy(async () => {
await Utils.waitFor(250);
if (_map) _map.remove();
map = null;
});
_map.on("load", function () {
_map.resize()
})
map.set(_map)
})
onDestroy(async () => {
await Utils.waitFor(250)
if (_map) _map.remove()
map = null
})
</script>
<svelte:head>
<link
href="./maplibre-gl.css"
rel="stylesheet"
/>
<link href="./maplibre-gl.css" rel="stylesheet" />
</svelte:head>
<div bind:this={container} class="map" id="map" style=" position: relative;
<div
bind:this={container}
class="map"
id="map"
style=" position: relative;
top: 0;
bottom: 0;
width: 100%;
height: 100%;"></div>
height: 100%;"
/>

View file

@ -44,7 +44,6 @@ class PointRenderingLayer {
this._onClick = onClick
this._selectedElement = selectedElement
const self = this
features.features.addCallbackAndRunD((features) => self.updateFeatures(features))
visibility?.addCallbackAndRunD((visible) => {
if (visible === true && self._dirty) {
@ -155,19 +154,21 @@ class PointRenderingLayer {
el.addEventListener("click", function (ev) {
ev.preventDefault()
self._onClick(feature)
console.log("Got click:", feature)
// Workaround to signal the MapLibreAdaptor to ignore this click
ev["consumed"] = true
})
}
const marker = new Marker({ element: el}).setLngLat(loc).setOffset(iconAnchor).addTo(this._map)
const marker = new Marker({ element: el })
.setLngLat(loc)
.setOffset(iconAnchor)
.addTo(this._map)
store
.map((tags) => this._config.pitchAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setPitchAlignment(<any> pitchAligment))
.addCallbackAndRun((pitchAligment) => marker.setPitchAlignment(<any>pitchAligment))
store
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any> pitchAligment))
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
if (feature.geometry.type === "Point") {
// When the tags get 'pinged', check that the location didn't change
store.addCallbackAndRunD(() => {
@ -330,7 +331,6 @@ class LineRenderingLayer {
})
if (this._onClick) {
map.on("click", polylayer, (e) => {
console.log("Got polylayer click:", e)
// polygon-layer-listener
if (e.originalEvent["consumed"]) {
// This is a polygon beneath a marker, we can ignore it
@ -348,7 +348,7 @@ class LineRenderingLayer {
map.setLayoutProperty(polylayer, "visibility", visible ? "visible" : "none")
} catch (e) {
console.warn(
"Error while setting visiblity of layers ",
"Error while setting visibility of layers ",
linelayer,
polylayer,
e
@ -458,7 +458,6 @@ export default class ShowDataLayer {
features: FeatureSource,
doShowLayer?: Store<boolean>
): ShowDataLayer {
return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer,
features,

View file

@ -1,24 +1,24 @@
<script lang="ts">
import LoginToggle from "../../Base/LoginToggle.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import Translations from "../../i18n/Translations"
import Tr from "../../Base/Tr.svelte"
import { InformationCircleIcon, TrashIcon } from "@babeard/svelte-heroicons/mini"
import type { OsmId, OsmTags } from "../../../Models/OsmFeature"
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig"
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte"
import type { Feature } from "geojson"
import { UIEventSource } from "../../../Logic/UIEventSource"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { TagsFilter } from "../../../Logic/Tags/TagsFilter"
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction"
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction"
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction"
import Loading from "../../Base/Loading.svelte"
import { DeleteFlowState } from "./DeleteFlowState"
import { twJoin } from "tailwind-merge"
import LoginToggle from "../../Base/LoginToggle.svelte";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import Translations from "../../i18n/Translations";
import Tr from "../../Base/Tr.svelte";
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
import type { OsmId, OsmTags } from "../../../Models/OsmFeature";
import DeleteConfig from "../../../Models/ThemeConfig/DeleteConfig";
import TagRenderingQuestion from "../TagRendering/TagRenderingQuestion.svelte";
import type { Feature } from "geojson";
import { UIEventSource } from "../../../Logic/UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import { TagUtils } from "../../../Logic/Tags/TagUtils";
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
import DeleteAction from "../../../Logic/Osm/Actions/DeleteAction";
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
import Loading from "../../Base/Loading.svelte";
import { DeleteFlowState } from "./DeleteFlowState";
import { twJoin } from "tailwind-merge";
export let state: SpecialVisualizationState
export let deleteConfig: DeleteConfig
@ -83,10 +83,9 @@
</script>
{#if $canBeDeleted === false && !hasSoftDeletion}
<div class="low-interaction flex">
<InformationCircleIcon class="h-6 w-6" />
<div class="low-interaction flex flex-col">
<Tr t={$canBeDeletedReason} />
<Tr class="subtle" t={t.useSomethingElse} />
<Tr cls="subtle" t={t.useSomethingElse} />
</div>
{:else}
<LoginToggle ignoreLoading={true} {state}>

View file

@ -38,7 +38,7 @@
}
</script>
<div class="inline-flex flex-col w-full">
<div class="inline-flex w-full flex-col">
{#if inline}
<Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput

View file

@ -38,11 +38,12 @@
let selectedMapping: number = undefined
let checkedMappings: boolean[]
$: {
let tgs = $tags
mappings = config.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
return m.hideInAnswer.matchesProperties(tags.data)
return !m.hideInAnswer.matchesProperties(tgs)
})
// We received a new config -> reinit
unit = layer.units.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
@ -59,7 +60,7 @@
if (config.freeform?.key) {
if (!config.multiAnswer) {
// Somehow, setting multianswer freeform values is broken if this is not set
freeformInput.setData(tags.data[config.freeform.key])
freeformInput.setData(tgs[config.freeform.key])
}
} else {
freeformInput.setData(undefined)
@ -69,7 +70,7 @@
export let selectedTags: TagsFilter = undefined
let mappings: Mapping[] = config?.mappings
let searchTerm: Store<string> = new UIEventSource("")
let searchTerm: UIEventSource<string> = new UIEventSource("")
$: {
try {

View file

@ -6,6 +6,7 @@ import Translations from "./i18n/Translations"
import { QueryParameters } from "../Logic/Web/QueryParameters"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import ThemeViewStateHashActor from "../Logic/Web/ThemeViewStateHashActor"
export default class QueryParameterDocumentation {
private static QueryParamDocsIntro = [
@ -13,7 +14,7 @@ export default class QueryParameterDocumentation {
"This document gives an overview of which URL-parameters can be used to influence MapComplete.",
new Title("What is a URL parameter?", 2),
'"URL-parameters are extra parts of the URL used to set the state.',
"For example, if the url is `https://mapcomplete.osm.be/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " +
"For example, if the url is `https://mapcomplete.org/cyclofix?lat=51.0&lon=4.3&z=5&test=true#node/1234`, " +
"the URL-parameters are stated in the part between the `?` and the `#`. There are multiple, all separated by `&`, namely: ",
new List(
[
@ -60,6 +61,7 @@ export default class QueryParameterDocumentation {
public static GenerateQueryParameterDocs(): BaseUIElement {
const docs: (string | BaseUIElement)[] = [
...QueryParameterDocumentation.QueryParamDocsIntro,
...ThemeViewStateHashActor.documentation,
]
this.UrlParamDocs().forEach((value, key) => {
const c = new Combine([

View file

@ -1,56 +1,52 @@
import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement"
import {FixedUiElement} from "./Base/FixedUiElement"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
import Table from "./Base/Table"
import {
RenderingSpecification,
SpecialVisualization,
SpecialVisualizationState,
} from "./SpecialVisualization"
import { HistogramViz } from "./Popup/HistogramViz"
import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
import {RenderingSpecification, SpecialVisualization, SpecialVisualizationState,} from "./SpecialVisualization"
import {HistogramViz} from "./Popup/HistogramViz"
import {MinimapViz} from "./Popup/MinimapViz"
import {ShareLinkViz} from "./Popup/ShareLinkViz"
import {UploadToOsmViz} from "./Popup/UploadToOsmViz"
import {MultiApplyViz} from "./Popup/MultiApplyViz"
import {AddNoteCommentViz} from "./Popup/AddNoteCommentViz"
import {PlantNetDetectionViz} from "./Popup/PlantNetDetectionViz"
import TagApplyButton from "./Popup/TagApplyButton"
import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
import {CloseNoteButton} from "./Popup/CloseNoteButton"
import {MapillaryLinkVis} from "./Popup/MapillaryLinkVis"
import {Store, Stores, UIEventSource} from "../Logic/UIEventSource"
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
import { VariableUiElement } from "./Base/VariableUIElement"
import { Utils } from "../Utils"
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation"
import {ImageCarousel} from "./Image/ImageCarousel"
import {ImageUploadFlow} from "./Image/ImageUploadFlow"
import {VariableUiElement} from "./Base/VariableUIElement"
import {Utils} from "../Utils"
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata"
import {Translation} from "./i18n/Translation"
import Translations from "./i18n/Translations"
import ReviewForm from "./Reviews/ReviewForm"
import ReviewElement from "./Reviews/ReviewElement"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
import { SubtleButton } from "./Base/SubtleButton"
import {SubtleButton} from "./Base/SubtleButton"
import Svg from "../Svg"
import NoteCommentElement from "./Popup/NoteCommentElement"
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"
import FileSelectorButton from "./Input/FileSelectorButton"
import { LoginToggle } from "./Popup/LoginButton"
import {LoginToggle} from "./Popup/LoginButton"
import Toggle from "./Input/Toggle"
import { SubstitutedTranslation } from "./SubstitutedTranslation"
import {SubstitutedTranslation} from "./SubstitutedTranslation"
import List from "./Base/List"
import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement"
import {LanguageElement} from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import {BBoxFeatureSourceForLayer} from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import QuestionViz from "./Popup/QuestionViz"
import { Feature, Point } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import {Feature, Point} from "geojson"
import {GeoOperations} from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
import UserProfile from "./BigComponents/UserProfile.svelte"
@ -58,30 +54,27 @@ import LanguagePicker from "./LanguagePicker"
import Link from "./Base/Link"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import NearbyImages, {
NearbyImageOptions,
P4CPicture,
SelectOneNearbyImage,
} from "./Popup/NearbyImages"
import { Tag } from "../Logic/Tags/Tag"
import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage,} from "./Popup/NearbyImages"
import {Tag} from "../Logic/Tags/Tag"
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../Logic/Tags/And"
import { SaveButton } from "./Popup/SaveButton"
import {And} from "../Logic/Tags/And"
import {SaveButton} from "./Popup/SaveButton"
import Lazy from "./Base/Lazy"
import { CheckBox } from "./Input/Checkboxes"
import {CheckBox} from "./Input/Checkboxes"
import Slider from "./Input/Slider"
import { OsmTags, WayId } from "../Models/OsmFeature"
import {OsmTags, WayId} from "../Models/OsmFeature"
import MoveWizard from "./Popup/MoveWizard"
import SplitRoadWizard from "./Popup/SplitRoadWizard"
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"
import { PointImportButtonViz } from "./Popup/ImportButtons/PointImportButtonViz"
import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz"
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz"
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz"
import DeleteWizard from "./Popup/DeleteFlow/DeleteWizard.svelte"
import { OpenJosm } from "./BigComponents/OpenJosm"
import {OpenJosm} from "./BigComponents/OpenJosm"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import FediverseValidator from "./InputElement/Validators/FediverseValidator";
class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
@ -180,7 +173,7 @@ class NearbyImageVis implements SpecialVisualization {
towardsCenter,
new Combine([
new VariableUiElement(
radius.GetValue().map((radius) => t.withinRadius.Subs({ radius }))
radius.GetValue().map((radius) => t.withinRadius.Subs({radius}))
),
radius,
]).SetClass("flex justify-between"),
@ -303,7 +296,7 @@ export default class SpecialVisualizations {
* SpecialVisualizations.constructSpecification("") // => []
*
* // Advanced cases with commas, braces and newlines should be handled without problem
* const templates = SpecialVisualizations.constructSpecification("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.osm.be/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}")
* const templates = SpecialVisualizations.constructSpecification("{send_email(&LBRACEemail&RBRACE,Broken bicycle pump,Hello&COMMA\n\nWith this email&COMMA I'd like to inform you that the bicycle pump located at https://mapcomplete.org/cyclofix?lat=&LBRACE_lat&RBRACE&lon=&LBRACE_lon&RBRACE&z=18#&LBRACEid&RBRACE is broken.\n\n Kind regards,Report this bicycle pump as broken)}")
* const templ = <Exclude<RenderingSpecification, string>> templates[0]
* templ.func.funcName // => "send_email"
* templ.args[0] = "{email}"
@ -393,24 +386,24 @@ export default class SpecialVisualizations {
viz.docs,
viz.args.length > 0
? new Table(
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
["name", "default", "description"],
viz.args.map((arg) => {
let defaultArg = arg.defaultValue ?? "_undefined_"
if (defaultArg == "") {
defaultArg = "_empty string_"
}
return [arg.name, defaultArg, arg.doc]
})
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
).SetClass("literal-code"),
])
}
@ -469,14 +462,14 @@ export default class SpecialVisualizations {
s.structuredExamples === undefined
? []
: s.structuredExamples().map((e) => {
return s.constr(
state,
new UIEventSource<Record<string, string>>(e.feature.properties),
e.args,
e.feature,
undefined
)
})
return s.constr(
state,
new UIEventSource<Record<string, string>>(e.feature.properties),
e.args,
e.feature,
undefined
)
})
return new Combine([new Title(s.funcName), s.docs, ...examples])
}
@ -491,7 +484,7 @@ export default class SpecialVisualizations {
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(AddNewPoint, {
state,
coordinate: { lon, lat },
coordinate: {lon, lat},
})
},
},
@ -610,7 +603,7 @@ export default class SpecialVisualizations {
feature: Feature
): BaseUIElement {
const [lon, lat] = GeoOperations.centerpointCoordinates(feature)
return new SvelteUIElement(CreateNewNote, { state, coordinate: { lon, lat } })
return new SvelteUIElement(CreateNewNote, {state, coordinate: {lon, lat}})
},
},
new CloseNoteButton(),
@ -687,7 +680,7 @@ export default class SpecialVisualizations {
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
new SvelteUIElement(AllTagsPanel, {tags, state}),
},
{
funcName: "image_carousel",
@ -1257,7 +1250,7 @@ export default class SpecialVisualizations {
},
{
funcName: "link",
docs: "Construct a link. By using the 'special' visualisation notation, translation should be easier",
docs: "Construct a link. By using the 'special' visualisation notation, translations should be easier",
args: [
{
name: "text",
@ -1326,7 +1319,7 @@ export default class SpecialVisualizations {
],
constr(state, featureTags, args) {
const [key, tr] = args
const translation = new Translation({ "*": tr })
const translation = new Translation({"*": tr})
return new VariableUiElement(
featureTags.map((tags) => {
const properties: object[] = JSON.parse(tags[key])
@ -1344,12 +1337,32 @@ export default class SpecialVisualizations {
)
},
},
{
funcName: "fediverse_link",
docs: "Converts a fediverse username or link into a clickable link",
args: [{
name: "key",
doc: "The attribute-name containing the link",
required: true
}],
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const key = argument[0]
const validator = new FediverseValidator()
return new VariableUiElement(tagSource.map(tags => tags[key]).map(fediAccount => {
fediAccount = validator.reformat(fediAccount)
const [_, username, host] = fediAccount.match(FediverseValidator.usernameAtServer)
return new Link(fediAccount, "https://" + host + "/@" + username, true)
}
))
}
}
]
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
const invalid = specialVisualizations
.map((sp, i) => ({ sp, i }))
.map((sp, i) => ({sp, i}))
.filter((sp) => sp.sp.funcName === undefined)
if (invalid.length > 0) {
throw (

View file

@ -1,57 +1,57 @@
<script lang="ts">
import { Store, UIEventSource } from "../Logic/UIEventSource"
import { Map as MlMap } from "maplibre-gl"
import MaplibreMap from "./Map/MaplibreMap.svelte"
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
import MapControlButton from "./Base/MapControlButton.svelte"
import ToSvelte from "./Base/ToSvelte.svelte"
import If from "./Base/If.svelte"
import { GeolocationControl } from "./BigComponents/GeolocationControl"
import type { Feature } from "geojson"
import SelectedElementView from "./BigComponents/SelectedElementView.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import Filterview from "./BigComponents/Filterview.svelte"
import ThemeViewState from "../Models/ThemeViewState"
import type { MapProperties } from "../Models/MapProperties"
import Geosearch from "./BigComponents/Geosearch.svelte"
import Translations from "./i18n/Translations"
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Store, UIEventSource } from "../Logic/UIEventSource";
import { Map as MlMap } from "maplibre-gl";
import MaplibreMap from "./Map/MaplibreMap.svelte";
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
import MapControlButton from "./Base/MapControlButton.svelte";
import ToSvelte from "./Base/ToSvelte.svelte";
import If from "./Base/If.svelte";
import { GeolocationControl } from "./BigComponents/GeolocationControl";
import type { Feature } from "geojson";
import SelectedElementView from "./BigComponents/SelectedElementView.svelte";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import Filterview from "./BigComponents/Filterview.svelte";
import ThemeViewState from "../Models/ThemeViewState";
import type { MapProperties } from "../Models/MapProperties";
import Geosearch from "./BigComponents/Geosearch.svelte";
import Translations from "./i18n/Translations";
import { CogIcon, EyeIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid";
import Tr from "./Base/Tr.svelte"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
import FloatOver from "./Base/FloatOver.svelte"
import PrivacyPolicy from "./BigComponents/PrivacyPolicy"
import Constants from "../Models/Constants"
import TabbedGroup from "./Base/TabbedGroup.svelte"
import UserRelatedState from "../Logic/State/UserRelatedState"
import LoginToggle from "./Base/LoginToggle.svelte"
import LoginButton from "./Base/LoginButton.svelte"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"
import ModalRight from "./Base/ModalRight.svelte"
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import { VariableUiElement } from "./Base/VariableUIElement"
import SvelteUIElement from "./Base/SvelteUIElement"
import OverlayToggle from "./BigComponents/OverlayToggle.svelte"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import Svg from "../Svg"
import { ShareScreen } from "./BigComponents/ShareScreen"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
import { AvailableRasterLayers } from "../Models/RasterLayers"
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"
import IfHidden from "./Base/IfHidden.svelte"
import { onDestroy } from "svelte"
import { OpenJosm } from "./BigComponents/OpenJosm"
import MapillaryLink from "./BigComponents/MapillaryLink.svelte"
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte"
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte"
import StateIndicator from "./BigComponents/StateIndicator.svelte"
import LanguagePicker from "./LanguagePicker"
import Locale from "./i18n/Locale"
import Tr from "./Base/Tr.svelte";
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte";
import FloatOver from "./Base/FloatOver.svelte";
import PrivacyPolicy from "./BigComponents/PrivacyPolicy";
import Constants from "../Models/Constants";
import TabbedGroup from "./Base/TabbedGroup.svelte";
import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel";
import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte";
import ModalRight from "./Base/ModalRight.svelte";
import { Utils } from "../Utils";
import Hotkeys from "./Base/Hotkeys";
import { VariableUiElement } from "./Base/VariableUIElement";
import SvelteUIElement from "./Base/SvelteUIElement";
import OverlayToggle from "./BigComponents/OverlayToggle.svelte";
import LevelSelector from "./BigComponents/LevelSelector.svelte";
import ExtraLinkButton from "./BigComponents/ExtraLinkButton";
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte";
import Svg from "../Svg";
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte";
import type { RasterLayerPolygon } from "../Models/RasterLayers";
import { AvailableRasterLayers } from "../Models/RasterLayers";
import RasterLayerOverview from "./Map/RasterLayerOverview.svelte";
import IfHidden from "./Base/IfHidden.svelte";
import { onDestroy } from "svelte";
import { OpenJosm } from "./BigComponents/OpenJosm";
import MapillaryLink from "./BigComponents/MapillaryLink.svelte";
import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte";
import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte";
import StateIndicator from "./BigComponents/StateIndicator.svelte";
import LanguagePicker from "./LanguagePicker";
import Locale from "./i18n/Locale";
import ShareScreen from "./BigComponents/ShareScreen.svelte";
export let state: ThemeViewState
let layout = state.layout
@ -314,11 +314,12 @@
<ToSvelte construct={() => new CopyrightPanel(state)} slot="content3" />
<div slot="title4">
<div slot="title4" class="flex">
<ToSvelte construct={Svg.share_svg().SetClass("w-4 h-4")} />
<Tr t={Translations.t.general.sharescreen.title} />
</div>
<div class="m-2" slot="content4">
<ToSvelte construct={() => new ShareScreen(state)} />
<ShareScreen {state}/>
</div>
</TabbedGroup>
</FloatOver>
@ -413,7 +414,7 @@
<div class="flex" slot="title2">
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
<Tr t={Translations.t.communityIndex.title}/>
<Tr t={Translations.t.communityIndex.title} />
</div>
<div class="m-2" slot="content2">
<CommunityIndexView location={state.mapProperties.location} />

View file

@ -226,16 +226,27 @@ export class Translation extends BaseUIElement {
return new Translation(this.translations, this.context)
}
FirstSentence() {
/**
* Build a new translation which only contains the first sentence of every language
* A sentence stops at either a dot (`.`) or a HTML-break ('<br/>').
* The dot or linebreak are _not_ returned.
*
* new Translation({"en": "This is a sentence. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence <br/> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence <br> This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence"
* new Translation({"en": "This is a sentence with a <b>bold</b> word. This is another sentence"}).FirstSentence().textFor("en") // "This is a sentence with a <b>bold</b> word"
* @constructor
*/
public FirstSentence(): Translation {
const tr = {}
for (const lng in this.translations) {
if (!this.translations.hasOwnProperty(lng)) {
continue
}
let txt = this.translations[lng]
txt = txt.replace(/[.<].*/, "")
txt = txt.replace(/(\.|<br\/>|<br>).*/, "")
txt = Utils.EllipsesAfter(txt, 255)
tr[lng] = txt
tr[lng] = txt.trim()
}
return new Translation(tr)

View file

@ -1,5 +1,4 @@
import colors from "./assets/colors.json"
import { HTMLElement } from "node-html-parser"
export class Utils {
/**
@ -221,6 +220,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
* Utils.Round7(12.123456789) // => 12.1234568
*/
public static Round7(i: number): number {
if (i == undefined) {
return undefined
}
return Math.round(i * 10000000) / 10000000
}
@ -324,7 +326,6 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
enumerable: false,
configurable: true,
get: () => {
console.trace("Property", name, "got requested")
init().then((r) => {
delete object[name]
object[name] = r
@ -488,7 +489,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
"\nThe value is",
v
)
v = (<HTMLElement>v.InnerConstructElement())?.textContent
v = v.InnerConstructElement()?.textContent
}
if (typeof v !== "string") {
@ -1162,7 +1163,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
public static HomepageLink(): string {
if (typeof window === "undefined") {
return "https://mapcomplete.osm.be"
return "https://mapcomplete.org"
}
const path = (
window.location.protocol +
@ -1211,6 +1212,22 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return new Date(str)
}
public static selectTextIn(node) {
if (document.body["createTextRange"]) {
const range = document.body["createTextRange"]()
range.moveToElementText(node)
range.select()
} else if (window.getSelection) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(node)
selection.removeAllRanges()
selection.addRange(range)
} else {
console.warn("Could not select text in node: Unsupported browser.")
}
}
public static sortedByLevenshteinDistance<T>(
reference: string,
ts: T[],

View file

@ -1,11 +1,11 @@
{
"contributors": [
{
"commits": 5753,
"commits": 5877,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 371,
"commits": 388,
"contributor": "Robin van der Linde"
},
{
@ -49,7 +49,7 @@
"contributor": "Ward"
},
{
"commits": 21,
"commits": 22,
"contributor": "Hosted Weblate"
},
{
@ -61,7 +61,7 @@
"contributor": "AlexanderRebai"
},
{
"commits": 19,
"commits": 20,
"contributor": "dependabot[bot]"
},
{
@ -110,11 +110,11 @@
},
{
"commits": 10,
"contributor": "LiamSimons"
"contributor": "Thibault Molleman"
},
{
"commits": 9,
"contributor": "Thibault Molleman"
"commits": 10,
"contributor": "LiamSimons"
},
{
"commits": 9,
@ -128,6 +128,10 @@
"commits": 8,
"contributor": "Mateusz Konieczny"
},
{
"commits": 7,
"contributor": "pelderson"
},
{
"commits": 7,
"contributor": "OliNau"
@ -148,10 +152,6 @@
"commits": 6,
"contributor": "danieldegroot2"
},
{
"commits": 6,
"contributor": "pelderson"
},
{
"commits": 4,
"contributor": "Nadhem"
@ -232,6 +232,14 @@
"commits": 2,
"contributor": "Stanislas Gueniffey"
},
{
"commits": 1,
"contributor": "Ciprian"
},
{
"commits": 1,
"contributor": "redfast00"
},
{
"commits": 1,
"contributor": "Daniel McDonald"

View file

@ -246,6 +246,9 @@
"es",
"pt"
],
"GR": [
"el"
],
"GT": [
"es"
],
@ -505,7 +508,9 @@
],
"PL": [
"pl",
"pl"
"be",
"pl",
"be"
],
"PS": [
"ar"

View file

@ -9,8 +9,9 @@
"fi": "suomi",
"fr": "français",
"gl": "lingua galega",
"he": "עברית",
"hu": "magyar",
"id": "bahasa Indonesia",
"id": "Bahasa Indonesia",
"it": "italiano",
"ja": "日本語",
"nb_NO": "bokmål",

File diff suppressed because it is too large Load diff

View file

@ -1,23 +1,23 @@
{
"contributors": [
{
"commits": 283,
"commits": 294,
"contributor": "kjon"
},
{
"commits": 275,
"commits": 277,
"contributor": "Pieter Vander Vennet"
},
{
"commits": 142,
"commits": 145,
"contributor": "paunofu"
},
{
"commits": 94,
"commits": 95,
"contributor": "Allan Nordhøy"
},
{
"commits": 69,
"commits": 70,
"contributor": "Robin van der Linde"
},
{
@ -36,13 +36,17 @@
"commits": 32,
"contributor": "Babos Gábor"
},
{
"commits": 31,
"contributor": "Jiří Podhorecký"
},
{
"commits": 31,
"contributor": "Supaplex"
},
{
"commits": 30,
"contributor": "Jiří Podhorecký"
"commits": 29,
"contributor": "Lucas"
},
{
"commits": 29,
@ -52,10 +56,6 @@
"commits": 25,
"contributor": "Reza Almanda"
},
{
"commits": 23,
"contributor": "Lucas"
},
{
"commits": 22,
"contributor": "Marco"
@ -124,6 +124,10 @@
"commits": 10,
"contributor": "Irina"
},
{
"commits": 9,
"contributor": "deep map"
},
{
"commits": 9,
"contributor": "Jaime Marquínez Ferrándiz"
@ -148,6 +152,10 @@
"commits": 8,
"contributor": "Vinicius"
},
{
"commits": 7,
"contributor": "NetworkedPoncho"
},
{
"commits": 7,
"contributor": "Joost Schouppe"
@ -244,6 +252,10 @@
"commits": 5,
"contributor": "Alexey Shabanov"
},
{
"commits": 4,
"contributor": "Emory Shaw"
},
{
"commits": 4,
"contributor": "André Marcelo Alvarenga"
@ -330,7 +342,7 @@
},
{
"commits": 2,
"contributor": "Emory Shaw"
"contributor": "מוימוי טרייצקי"
},
{
"commits": 2,
@ -440,10 +452,6 @@
"commits": 1,
"contributor": "Stéphane De Greef"
},
{
"commits": 1,
"contributor": "deep map"
},
{
"commits": 1,
"contributor": "Falk Rund"

View file

@ -1 +1 @@
{"languages":["ca","cs","da","de","en","eo","es","fi","fil","fr","gl","hu","id","it","ja","nb_NO","nl","pa_PK","pl","pt","pt_BR","ru","sl","sv","zgh","zh_Hans","zh_Hant"]}
{"languages":["ca","cs","da","de","en","eo","es","fi","fil","fr","gl","he","hu","id","it","ja","nb_NO","nl","pa_PK","pl","pt","pt_BR","ru","sl","sv","zgh","zh_Hans","zh_Hant"]}

12
src/land.ts Normal file
View file

@ -0,0 +1,12 @@
import {OsmConnection} from "./Logic/Osm/OsmConnection";
console.log("Authorizing...");
new OsmConnection().finishLogin(previousURL => {
const fallback = window.location.protocol+"//"+window.location.host+"/index.html"
previousURL ??= fallback
if(previousURL.indexOf("/land") > 0){
previousURL = fallback
}
console.log("Redirecting to", previousURL)
window.location.href = previousURL
})