Merge develop

This commit is contained in:
Pieter Vander Vennet 2023-03-15 14:29:53 +01:00
commit e67d740769
719 changed files with 30508 additions and 15682 deletions

54
UI/AllTagsPanel.svelte Normal file
View file

@ -0,0 +1,54 @@
<script lang="ts">
import ToSvelte from "./Base/ToSvelte.svelte"
import Table from "./Base/Table"
import { UIEventSource } from "../Logic/UIEventSource"
//Svelte props
export let tags: UIEventSource<any>
export let state: any
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
const allTags = tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return parts
})
const tagsTable = new Table(["Key", "Value"], $allTags).SetClass("zebra-table")
</script>
<section>
<ToSvelte construct={tagsTable} />
</section>
<style lang="scss">
section {
@apply border border-solid border-black rounded-2xl p-4 block;
}
</style>

View file

@ -1,47 +0,0 @@
import { VariableUiElement } from "./Base/VariableUIElement"
import { UIEventSource } from "../Logic/UIEventSource"
import Table from "./Base/Table"
export class AllTagsPanel extends VariableUiElement {
constructor(tags: UIEventSource<any>, state?) {
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ??
[])
)
super(
tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return new Table(["key", "value"], parts)
.SetStyle(
"border: 1px solid black; border-radius: 1em;padding:1em;display:block;"
)
.SetClass("zebra-table")
})
)
}
}

View file

@ -10,12 +10,10 @@ import IndexText from "./BigComponents/IndexText"
import FeaturedMessage from "./BigComponents/FeaturedMessage"
import { ImportViewerLinks } from "./BigComponents/UserInformation"
import { LoginToggle } from "./Popup/LoginButton"
import UserSurveyPanel from "./UserSurveyPanel"
export default class AllThemesGui {
setup() {
try {
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined)
const intro = new Combine([
new LanguagePicker1(Translations.t.index.title.SupportedLanguages(), "").SetClass(
@ -26,7 +24,6 @@ export default class AllThemesGui {
new Combine([
intro,
new FeaturedMessage().SetClass("mb-4 block"),
new Combine([new UserSurveyPanel()]).SetClass("flex justify-center"),
new MoreScreen(state, true),
new LoginToggle(undefined, Translations.t.index.logIn, state),
new ImportViewerLinks(state.osmConnection),
@ -37,14 +34,14 @@ export default class AllThemesGui {
])
.SetClass("block m-5 lg:w-3/4 lg:ml-40")
.SetStyle("pointer-events: all;")
.AttachTo("topleft-tools")
.AttachTo("top-left")
} catch (e) {
console.error(">>>> CRITICAL", e)
new FixedUiElement(
"Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!"
)
.SetClass("alert")
.AttachTo("centermessage")
.AttachTo("top-left")
}
}
}

View file

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

View file

@ -14,6 +14,9 @@ export class FixedUiElement extends BaseUIElement {
AsMarkdown(): string {
if (this.HasClass("code")) {
if (this.content.indexOf("\n") > 0 || this.HasClass("block")) {
return "\n```\n" + this.content + "\n```\n"
}
return "`" + this.content + "`"
}
if (this.HasClass("font-bold")) {

View file

@ -21,7 +21,7 @@ export default class LinkToWeblate extends VariableUiElement {
return undefined
}
const icon = Svg.translate_svg().SetClass(
"rounded-full border border-gray-400 inline-block w-4 h-4 m-1 weblate-link self-center"
"rounded-full inline-block w-3 h-3 ml-1 weblate-link self-center"
)
if (availableTranslations[ln] === undefined) {
icon.SetClass("bg-red-400")
@ -31,7 +31,15 @@ export default class LinkToWeblate extends VariableUiElement {
[Locale.showLinkToWeblate]
)
)
this.SetClass("enable-links hidden-on-mobile")
this.SetClass("enable-links")
const self = this
Locale.showLinkOnMobile.addCallbackAndRunD((showOnMobile) => {
if (showOnMobile) {
self.RemoveClass("hidden-on-mobile")
} else {
self.SetClass("hidden-on-mobile")
}
})
}
/**

View file

@ -3,7 +3,6 @@ import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import { UIEventSource } from "../../Logic/UIEventSource"
import { BBox } from "../../Logic/BBox"
import { deprecate } from "util"
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>

View file

@ -23,7 +23,7 @@ import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export class StrayClickHandlerImplementation {
class StrayClickHandlerImplementation {
private _lastMarker
constructor(
@ -91,6 +91,7 @@ export class StrayClickHandlerImplementation {
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>

View file

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

View file

@ -0,0 +1,82 @@
<script lang="ts">
import { onMount } from "svelte"
import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import Img from "./Img"
import Translations from "../i18n/Translations"
import { ImmutableStore } from "../../Logic/UIEventSource.js"
export let imageUrl: string | BaseUIElement = undefined
export let message: string | BaseUIElement = undefined
export let options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
extraClasses?: string
} = {}
// Website to open when clicked
let href: Store<string> = undefined
if (options?.url) {
href = typeof options?.url == "string" ? new ImmutableStore(options.url) : options.url
}
let imgElem: HTMLElement
let msgElem: HTMLElement
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11")
onMount(() => {
// Image
if (imgElem && imageUrl) {
let img: BaseUIElement
if ((imageUrl ?? "") === "") {
img = undefined
} else if (typeof imageUrl !== "string") {
img = imageUrl?.SetClass(imgClasses)
}
if (img) imgElem.replaceWith(img.ConstructElement())
}
// Message
if (msgElem && message) {
let msg = Translations.W(message)?.SetClass("block text-ellipsis no-images flex-shrink")
msgElem.replaceWith(msg.ConstructElement())
}
})
</script>
<svelte:element
this={href === undefined ? "span" : "a"}
class={(options.extraClasses ?? "") +
"flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle"}
href={$href}
target={options?.newTab ? "_blank" : ""}
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses + " bg-red border border-black"} />
{:else}
<template bind:this={imgElem} />
{/if}
{/if}
</slot>
<slot name="message">
<template bind:this={msgElem} />
</slot>
</svelte:element>
<style lang="scss">
span,
a {
@apply flex p-3 my-2 py-4 rounded-lg shrink-0;
@apply items-center w-full no-underline;
@apply bg-subtle text-black;
:global(span) {
@apply block text-ellipsis;
}
}
</style>

View file

@ -1,13 +1,11 @@
import Translations from "../i18n/Translations"
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import Link from "./Link"
import Img from "./Img"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { UIElement } from "../UIElement"
import { VariableUiElement } from "./VariableUIElement"
import Lazy from "./Lazy"
import Loading from "./Loading"
import SubtleButtonSvelte from "./SubtleButton.svelte"
import SvelteUIElement from "./SvelteUIElement"
export class SubtleButton extends UIElement {
private readonly imageUrl: string | BaseUIElement
@ -27,7 +25,7 @@ export class SubtleButton extends UIElement {
newTab?: boolean
imgSize?: "h-11 w-11" | string
extraClasses?: string
} = undefined
} = {}
) {
super()
this.imageUrl = imageUrl
@ -36,30 +34,11 @@ export class SubtleButton extends UIElement {
}
protected InnerRender(): string | BaseUIElement {
const classes =
"block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline " +
(this?.options?.extraClasses ?? "")
const message = Translations.W(this.message)?.SetClass(
"block text-ellipsis no-images flex-shrink"
)
let img
const imgClasses =
"block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
if ((this.imageUrl ?? "") === "") {
img = undefined
} else if (typeof this.imageUrl === "string") {
img = new Img(this.imageUrl)?.SetClass(imgClasses)
} else {
img = this.imageUrl?.SetClass(imgClasses)
}
const button = new Combine([img, message]).SetClass("flex items-center group w-full")
if (this.options?.url == undefined) {
this.SetClass(classes)
return button
}
return new Link(button, this.options.url, this.options.newTab ?? false).SetClass(classes)
return new SvelteUIElement(SubtleButtonSvelte, {
imageUrl: this?.imageUrl ?? undefined,
message: this?.message ?? "",
options: this?.options ?? {},
})
}
public OnClickWithLoading(

View file

@ -0,0 +1,37 @@
import BaseUIElement from "../BaseUIElement"
import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
*/
export default class SvelteUIElement<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends BaseUIElement {
private readonly _svelteComponent: {
new (args: {
target: HTMLElement
props: Props
events?: Events
slots?: Slots
}): SvelteComponentTyped<Props, Events, Slots>
}
private readonly _props: Props
constructor(svelteElement, props: Props) {
super()
this._svelteComponent = svelteElement
this._props = props
}
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("div")
new this._svelteComponent({
target: el,
props: this._props,
})
return el
}
}

18
UI/Base/ToSvelte.svelte Normal file
View file

@ -0,0 +1,18 @@
<script lang="ts">
import BaseUIElement from "../BaseUIElement.js"
import { onMount } from "svelte"
export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement
onMount(() => {
let html =
typeof construct === "function"
? construct().ConstructElement()
: construct.ConstructElement()
elem.replaceWith(html)
})
</script>
<span bind:this={elem} />

View file

@ -9,9 +9,10 @@ import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
import { MapillaryLink } from "./MapillaryLink"
import TranslatorsPanel from "./TranslatorsPanel"
import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState"
export class BackToThemeOverview extends Toggle {
constructor(
@ -40,7 +41,6 @@ export class ActionButtons extends Combine {
readonly currentBounds: Store<BBox>
readonly locationControl: Store<Loc>
readonly osmConnection: OsmConnection
readonly isTranslator: Store<boolean>
readonly featureSwitchMoreQuests: Store<boolean>
}) {
const imgSize = "h-6 w-6"
@ -77,7 +77,14 @@ export class ActionButtons extends Combine {
new OpenIdEditor(state, iconStyle),
new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle).SetClass("hidden-on-mobile"),
new TranslatorsPanel(state, iconStyle),
new SubtleButton(
Svg.translate_ui().SetStyle(iconStyle),
Translations.t.translations.activateButton
).onClick(() => {
ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode")
}),
])
this.SetClass("block w-full link-no-underline")
}

View file

@ -9,6 +9,7 @@ import { VariableUiElement } from "../Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
/**
* The bottom right attribution panel in the leaflet map
@ -58,7 +59,13 @@ export default class Attribution extends Combine {
true
)
let editWithJosm = new VariableUiElement(
const mapDataByOsm = new Link(
Translations.t.general.attribution.mapDataByOsm,
"https://openstreetmap.org/copyright",
true
)
const editWithJosm = new VariableUiElement(
userDetails.map(
(userDetails) => {
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
@ -79,7 +86,7 @@ export default class Attribution extends Combine {
[location, currentBounds]
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary])
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary, mapDataByOsm])
this.SetClass("flex")
}
}

View file

@ -108,7 +108,10 @@ class SingleLayerSelectionButton extends Toggle {
// Is the previous layer still valid? If so, we don't bother to switch
if (
previousLayer.data.feature === null ||
GeoOperations.inside(locationControl.data, previousLayer.data.feature)
GeoOperations.inside(
[locationControl.data.lon, locationControl.data.lat],
previousLayer.data.feature
)
) {
return
}

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Utils } from "../../Utils"
import global_community from "../../assets/community_index_global_resources.json"
import ContactLink from "./ContactLink.svelte"
import { GeoOperations } from "../../Logic/GeoOperations"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import type { Feature, Geometry, GeometryCollection } from "@turf/turf"
export let locationControl: Store<{ lat: number; lon: number }>
const tileToFetch: Store<string> = locationControl.mapD((l) => {
const t = Tiles.embedded_tile(l.lat, l.lon, 6)
return `https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/tile_${t.z}_${t.x}_${t.y}.geojson`
})
const t = Translations.t.communityIndex
const resources = new UIEventSource<
Feature<Geometry | GeometryCollection, { resources; nameEn: string }>[]
>([])
tileToFetch.addCallbackAndRun(async (url) => {
const data = await Utils.downloadJsonCached(url, 24 * 60 * 60)
if (data === undefined) {
return
}
resources.setData(data.features)
})
const filteredResources = resources.map(
(features) =>
features.filter((f) => {
return GeoOperations.inside([locationControl.data.lon, locationControl.data.lat], f)
}),
[locationControl]
)
</script>
<div>
<ToSvelte construct={t.intro} />
{#each $filteredResources as feature}
<ContactLink country={feature.properties} />
{/each}
<ContactLink country={{ resources: global_community, nameEn: "Global resources" }} />
</div>

View file

@ -0,0 +1,50 @@
<script lang="ts">
// A contact link indicates how a mapper can contact their local community
// The _properties_ of a community feature
import Locale from "../i18n/Locale.js"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import * as native from "../../assets/language_native.json"
import { TypedTranslation } from "../i18n/Translation"
const availableTranslationTyped: TypedTranslation<{ native: string }> =
Translations.t.communityIndex.available
const availableTranslation = availableTranslationTyped.OnEveryLanguage((s, ln) =>
s.replace("{native}", native[ln] ?? ln)
)
export let country: { resources; nameEn: string }
let resources: {
id: string
resolved: Record<string, string>
languageCodes: string[]
type: string
}[] = []
$: resources = Array.from(Object.values(country?.resources ?? {}))
const language = Locale.language
</script>
<div>
{#if country?.nameEn}
<h3>{country?.nameEn}</h3>
{/if}
{#each resources as resource}
<div class="flex link-underline items-center my-4">
<img
class="w-8 h-8 m-2"
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">
{resource.resolved.name ?? resource.resolved.url}
</a>
{resource.resolved?.description}
{#if resource.languageCodes?.indexOf($language) >= 0}
<span class="border-2 rounded-full border-lime-500 text-sm w-fit px-2">
<ToSvelte construct={() => availableTranslation.Clone()} />
</span>
{/if}
</div>
</div>
{/each}
</div>

View file

@ -2,13 +2,13 @@ import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { FixedUiElement } from "../Base/FixedUiElement"
import * as licenses from "../../assets/generated/license_info.json"
import licenses from "../../assets/generated/license_info.json"
import SmallLicense from "../../Models/smallLicense"
import { Utils } from "../../Utils"
import Link from "../Base/Link"
import { VariableUiElement } from "../Base/VariableUIElement"
import * as contributors from "../../assets/contributors.json"
import * as translators from "../../assets/translators.json"
import contributors from "../../assets/contributors.json"
import translators from "../../assets/translators.json"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Title from "../Base/Title"
@ -118,12 +118,13 @@ export default class CopyrightPanel extends Combine {
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
osmConnection: OsmConnection
isTranslator: Store<boolean>
}) {
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution)
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
CopyrightPanel.IconAttribution
)
let maintainer: BaseUIElement = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {

View file

@ -0,0 +1,38 @@
<script lang="ts">
import UserDetails from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let userDetails: UIEventSource<UserDetails>
const t = Translations.t.general.morescreen
console.log($userDetails.csCount < 50)
</script>
<div>
{#if $userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock}
<SubtleButton
options={{
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
}}
>
<span slot="message">{t.requestATheme.toString()}</span>
</SubtleButton>
{:else}
<SubtleButton
options={{
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
}}
>
<span slot="image" class="h-11 w-11 mx-4 bg-red">
<ToSvelte construct={Svg.pencil_ui()} />
</span>
<span slot="message">{t.createYourOwnTheme.toString()}</span>
</SubtleButton>
{/if}
</div>

View file

@ -227,7 +227,7 @@ export class DownloadPanel extends Toggle {
bbox,
new Set(neededLayers)
)
outer: for (const tile of featureList) {
for (const tile of featureList) {
if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) {
continue
}
@ -238,7 +238,7 @@ export class DownloadPanel extends Toggle {
}
const featureList = perLayer.get(tile.layer)
const filters = layer.appliedFilters.data
for (const feature of tile.features) {
perfeature: for (const feature of tile.features) {
if (!bbox.overlapsWith(BBox.get(feature))) {
continue
}
@ -250,7 +250,7 @@ export class DownloadPanel extends Toggle {
continue
}
if (!filter.currentFilter.matchesProperties(feature.properties)) {
continue outer
continue perfeature
}
}
}
@ -281,7 +281,7 @@ export class DownloadPanel extends Toggle {
delete feature.properties[key]
}
featureList.push(feature)
featureList.push(cleaned)
}
}

View file

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

View file

@ -279,12 +279,17 @@ export class LayerFilterPanel extends Combine {
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6")
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6")
const qp = QueryParameters.GetBooleanQueryParameter(
"filter-" + filterConfig.id,
false,
"Is filter '" + filterConfig.options[0].question.textFor("en") + " enabled?"
)
const toggle = new ClickableToggle(
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass(
"flex"
)
),
qp
)
.ToggleOnClick()
.SetClass("block m-1")
@ -315,6 +320,15 @@ export class LayerFilterPanel extends Combine {
state: i,
}))
let filterPicker: InputElement<number>
const value = QueryParameters.GetQueryParameter(
"filter-" + filterConfig.id,
"0",
"Value for filter " + filterConfig.id
).sync(
(str) => Number(str),
[],
(n) => "" + n
)
if (options.length <= 6) {
filterPicker = new RadioButton(
@ -323,6 +337,7 @@ export class LayerFilterPanel extends Combine {
new FixedInputElement(option.question.Clone().SetClass("block"), i)
),
{
value,
dontStyle: true,
}
)
@ -332,7 +347,8 @@ export class LayerFilterPanel extends Combine {
options.map((option, i) => ({
value: i,
shown: option.question.Clone(),
}))
})),
value
)
}

View file

@ -85,15 +85,6 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
tabs.push({ header: Svg.share_img, content: new ShareScreen(state) })
}
const copyright = {
header: Svg.copyright_svg(),
content: new Combine([
Translations.t.general.openStreetMapIntro.SetClass("link-underline"),
new CopyrightPanel(state),
]),
}
tabs.push(copyright)
const privacy = {
header: Svg.eye_svg(),
content: new PrivacyPolicy(),

View file

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

View file

@ -0,0 +1,47 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let search: UIEventSource<string>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] = themeOverview["default"].filter(
(layout) => layout.hideFromOverview
)
const userPreferences = state.osmConnection.preferencesHandler.preferences
const t = Translations.t.general.morescreen
$: knownThemesId = Utils.NoNull(
Object.keys($userPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
</script>
<ThemesList
{search}
{state}
{onMainScreen}
themes={knownThemes}
isCustom={false}
hideThemes={false}
>
<svelte:fragment slot="title">
<h3>{t.previouslyHiddenTitle.toString()}</h3>
<p>
{t.hiddenExplanation.Subs({
hidden_discovered: knownThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(),
})}
</p>
</svelte:fragment>
</ThemesList>

View file

@ -105,24 +105,7 @@ export default class LeftControls extends Combine {
state.featureSwitchBackgroundSelection
)
// If the welcomeMessage is disabled, the copyright is hidden (as that is where the copyright is located
const copyright = new Toggle(
undefined,
new Lazy(() => {
new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle,
() => new CopyrightPanel(state),
"copyright",
guiState.copyrightViewIsOpened
)
return new MapControlButton(Svg.copyright_svg()).onClick(() =>
guiState.copyrightViewIsOpened.setData(true)
)
}),
state.featureSwitchWelcomeMessage
)
super([currentViewAction, filterButton, downloadButton, copyright, mapSwitch])
super([currentViewAction, filterButton, downloadButton, mapSwitch])
this.SetClass("flex flex-col")
}

View file

@ -1,36 +1,25 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import Translations from "../i18n/Translations"
import * as personal from "../../assets/themes/personal/personal.json"
import Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import Title from "../Base/Title"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import themeOverview from "../../assets/generated/theme_overview.json"
import { Translation } from "../i18n/Translation"
import { TextField } from "../Input/TextField"
import FilteredCombine from "../Base/FilteredCombine"
import Locale from "../i18n/Locale"
import SvelteUIElement from "../Base/SvelteUIElement"
import ThemesList from "./ThemesList.svelte"
import HiddenThemeList from "./HiddenThemeList.svelte"
import UnofficialThemeList from "./UnofficialThemeList.svelte"
export default class MoreScreen extends Combine {
private static readonly officialThemes: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywors?: any[]
}[] = themeOverview["default"]
private static readonly officialThemes: LayoutInformation[] = themeOverview
constructor(
state: UserRelatedState & {
@ -40,13 +29,6 @@ export default class MoreScreen extends Combine {
onMainScreen: boolean = false
) {
const tr = Translations.t.general.morescreen
let themeButtonStyle = ""
let themeListStyle = ""
if (onMainScreen) {
themeButtonStyle = "h-32 min-h-32 max-h-32 text-ellipsis overflow-hidden"
themeListStyle =
"md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
}
const search = new TextField({
placeholder: tr.searchForATheme,
@ -107,38 +89,26 @@ export default class MoreScreen extends Combine {
super([
new Combine([searchBar]).SetClass("flex justify-center"),
MoreScreen.createOfficialThemesList(
new SvelteUIElement(ThemesList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createPreviouslyVistedHiddenList(
onMainScreen,
search: search.GetValue(),
themes: MoreScreen.officialThemes,
}),
new SvelteUIElement(HiddenThemeList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createUnofficialThemeList(
themeButtonStyle,
onMainScreen,
search: search.GetValue(),
}),
new SvelteUIElement(UnofficialThemeList, {
state,
themeListStyle,
search.GetValue()
),
onMainScreen,
search: search.GetValue(),
}),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10"),
])
}
private static NothingFound(search: UIEventSource<string>): BaseUIElement {
const t = Translations.t.general.morescreen
return new Combine([
new Title(t.noMatchingThemes, 5).SetClass("w-max font-bold"),
new SubtleButton(Svg.search_disable_ui(), t.noSearch, { imgSize: "h-6" })
.SetClass("h-12 w-max")
.onClick(() => search.setData("")),
]).SetClass("flex flex-col items-center w-full")
}
private static createUrlFor(
layout: { id: string; definition?: string },
isCustom: boolean,
@ -239,102 +209,6 @@ export default class MoreScreen extends Combine {
new SubtleButton(undefined, t.button, { url: "./professional.html" }),
]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg")
}
private static createUnofficialThemeList(
buttonClass: string,
state: UserRelatedState,
themeListClasses: string,
search: UIEventSource<string>
): BaseUIElement {
var currentIds: Store<string[]> = state.installedUserThemes
var stableIds = Stores.ListStabilized<string>(currentIds)
return new VariableUiElement(
stableIds.map((ids) => {
const allThemes: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
[]
for (const id of ids) {
const themeInfo = state.GetUnofficialTheme(id)
if (themeInfo === undefined) {
continue
}
const link = MoreScreen.createLinkButton(state, themeInfo, true)
if (link !== undefined) {
allThemes.push({
element: link.SetClass(buttonClass),
predicate: (s) => id.toLowerCase().indexOf(s) >= 0,
})
}
}
if (allThemes.length <= 0) {
return undefined
}
return new Combine([
Translations.t.general.customThemeIntro,
new FilteredCombine(allThemes, search, {
innerClasses: themeListClasses,
onEmpty: MoreScreen.NothingFound(search),
}),
])
})
)
}
private static createPreviouslyVistedHiddenList(
state: UserRelatedState,
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes = themeOverview["default"].filter((layout) => layout.hideFromOverview)
const hiddenTotal = hiddenThemes.length
return new Toggle(
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map((allPreferences) => {
const knownThemes: Set<string> = new Set(
Utils.NoNull(
Object.keys(allPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) =>
key.substring(prefix.length, key.length - "-enabled".length)
)
)
)
if (knownThemes.size === 0) {
return undefined
}
const knownThemeDescriptions = hiddenThemes
.filter((theme) => knownThemes.has(theme.id))
.map((theme) => ({
element: MoreScreen.createLinkButton(state, theme)?.SetClass(
buttonClass
),
predicate: MoreScreen.MatchesLayoutFunc(theme),
}))
const knownLayouts = new FilteredCombine(knownThemeDescriptions, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
return new Combine([
new Title(t.previouslyHiddenTitle),
t.hiddenExplanation.Subs({
hidden_discovered: "" + knownThemes.size,
total_hidden: "" + hiddenTotal,
}),
knownLayouts,
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static MatchesLayoutFunc(layout: {
id: string
@ -365,70 +239,4 @@ export default class MoreScreen extends Combine {
return false
}
}
private static createOfficialThemesList(
state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> },
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
let buttons: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
MoreScreen.officialThemes.map((layout) => {
if (layout === undefined) {
console.trace("Layout is undefined")
return undefined
}
if (layout.hideFromOverview) {
return undefined
}
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
if (layout.id === personal.id) {
const element = new VariableUiElement(
state.osmConnection.userDetails
.map((userdetails) => userdetails.csCount)
.map((csCount) => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
} else {
return button
}
})
)
return { element }
}
return { element: button, predicate: MoreScreen.MatchesLayoutFunc(layout) }
})
const professional = MoreScreen.CreateProffessionalSerivesButton()
const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0, { element: customGeneratorLink }, { element: professional })
return new FilteredCombine(buttons, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
}
/*
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
* */
private static createCustomGeneratorButton(state: {
osmConnection: OsmConnection
}): VariableUiElement {
const tr = Translations.t.general.morescreen
return new VariableUiElement(
state.osmConnection.userDetails.map((userDetails) => {
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
return new SubtleButton(null, tr.requestATheme.Clone(), {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
})
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false,
})
})
)
}
}

View file

@ -0,0 +1,65 @@
<script lang="ts" context="module">
export interface Theme {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywords?: any[]
}
</script>
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let search: UIEventSource<string>
const t = Translations.t.general.morescreen
</script>
<span>
<h5>{t.noMatchingThemes.toString()}</h5>
<button
on:click={() => {
search.setData("")
}}
>
<span>
<SubtleButton>
<span slot="image">
<ToSvelte construct={Svg.search_disable_ui()} />
</span>
<span slot="message">{t.noSearch.toString()}</span>
</SubtleButton>
</span>
</button>
</span>
<style lang="scss">
span {
@apply flex flex-col items-center w-full;
h5 {
@apply w-max font-bold;
}
// SubtleButton
button {
@apply h-12;
span {
@apply w-max;
:global(img) {
@apply h-6;
}
}
}
}
</style>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import Title from "../Base/Title"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
const t = Translations.t.professional.indexPage
</script>
<div>
<ToSvelte construct={new Title(t.hook, 4)} />
<span>
{t.hookMore.toString()}
</span>
<SubtleButton options={{ url: "./professional.html" }}>
<span slot="message">{t.button.toString()}</span>
</SubtleButton>
</div>
<style lang="scss">
div {
@apply flex flex-col border border-gray-300 p-2 rounded-lg;
}
</style>

View file

@ -13,12 +13,10 @@ export default class RightControls extends Combine {
state: MapState & { featurePipeline: FeaturePipeline },
geolocationHandler: GeoLocationHandler
) {
const geolocationButton = new Toggle(
const geolocationButton = Toggle.If(state.featureSwitchGeolocation, () =>
new MapControlButton(new GeolocationControl(geolocationHandler, state), {
dontStyle: true,
}).SetClass("p-1"),
undefined,
state.featureSwitchGeolocation
}).SetClass("p-1")
)
const plus = new MapControlButton(Svg.plus_svg()).onClick(() => {

View file

@ -1,7 +1,7 @@
/**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
*/
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
@ -101,6 +101,9 @@ export default class SimpleAddUI extends LoginToggle {
snapOntoWay?: OsmWay
): Promise<void> {
tags.push(new Tag(Tag.newlyCreated.key, new Date().toISOString()))
if (snapOntoWay) {
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layoutToUse?.id ?? "unkown",
changeType: "create",
@ -283,11 +286,16 @@ export default class SimpleAddUI extends LoginToggle {
const presets = layer.layerDef.presets
for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
const isSnapping = preset.preciseInput.snapToLayers?.length > 0
let icon: () => BaseUIElement = () =>
layer.layerDef.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false, {
noSize: true,
})
.GenerateLeafletStyle(
new ImmutableStore<any>(
isSnapping ? tags : { _referencing_ways: ["way/-1"], ...tags }
),
false,
{ noSize: true }
)
.html.SetClass("w-12 h-12 block relative")
const presetInfo: PresetInfo = {
layerToAddTo: layer,

View file

@ -0,0 +1,110 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import { Translation } from "../i18n/Translation"
import * as personal from "../../assets/themes/personal/personal.json"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
).toString()
$: description = new Translation(theme.shortDescription).toString()
// TODO: Improve this function
function createUrl(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined
}
const currentLocation = state?.locationControl
let path = window.location.pathname
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
path = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
}
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
}
if (isCustom) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
let hash = ""
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
return (
currentLocation?.map((currentLocation) => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
["lon", currentLocation?.lon],
]
.filter((part) => part[1] !== undefined)
.map((part) => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${hash}`
}) ?? new ImmutableStore<string>(`${linkPrefix}`)
)
}
</script>
{#if theme.id !== personal.id || $userDetails.csCount > Constants.userJourney.personalLayoutUnlock}
<div>
<SubtleButton options={{ url: createUrl(theme, isCustom, state) }}>
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
<span slot="message" class="message">
<span>
<span>{title}</span>
<span>{description}</span>
</span>
</span>
</SubtleButton>
</div>
{/if}
<style lang="scss">
div {
@apply h-32 min-h-[8rem] max-h-32 text-ellipsis overflow-hidden;
span.message {
@apply flex flex-col justify-center h-24;
& > span {
@apply flex flex-col overflow-hidden;
span:nth-child(2) {
@apply text-[#999];
}
}
}
}
</style>

View file

@ -5,15 +5,12 @@ import Toggle from "../Input/Toggle"
import { SubtleButton } from "../Base/SubtleButton"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { LoginToggle } from "../Popup/LoginButton"
import Svg from "../../Svg"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
import LoggedInUserIndicator from "../LoggedInUserIndicator"
import { ActionButtons } from "./ActionButtons"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import UserSurveyPanel from "../UserSurveyPanel"
export default class ThemeIntroductionPanel extends Combine {
constructor(
@ -27,7 +24,6 @@ export default class ThemeIntroductionPanel extends Combine {
osmConnection: OsmConnection
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
isTranslator: Store<boolean>
},
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) {
@ -70,7 +66,6 @@ export default class ThemeIntroductionPanel extends Combine {
const hasPresets = layout.layers.some((l) => l.presets?.length > 0)
super([
layout.description.Clone().SetClass("block mb-4"),
new UserSurveyPanel(),
new Combine([
t.welcomeExplanation.general,
hasPresets

View file

@ -0,0 +1,82 @@
<script lang="ts">
import NoThemeResultButton from "./NoThemeResultButton.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import Locale from "../i18n/Locale"
import CustomGeneratorButton from "./CustomGeneratorButton.svelte"
import ProfessionalServicesButton from "./ProfessionalServicesButton.svelte"
import ThemeButton from "./ThemeButton.svelte"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let search: UIEventSource<string>
export let themes: LayoutInformation[]
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let isCustom: boolean = false
export let onMainScreen: boolean = true
export let hideThemes: boolean = true
// Filter theme based on search value
$: filteredThemes = themes.filter((theme) => {
if ($search === undefined || $search === "") return true
const srch = $search.toLocaleLowerCase()
if (theme.id.toLowerCase().indexOf(srch) >= 0) {
return true
}
const entitiesToSearch = [theme.shortDescription, theme.title, ...(theme.keywords ?? [])]
for (const entity of entitiesToSearch) {
if (entity === undefined) {
continue
}
const term = entity["*"] ?? entity[Locale.language.data]
if (term?.toLowerCase()?.indexOf(search) >= 0) {
return true
}
}
return false
})
</script>
<section>
<slot name="title" />
{#if onMainScreen}
<div class="md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 gap-4">
{#if ($search === undefined || $search === "") && !isCustom && hideThemes}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{:else}
<div>
{#if ($search === undefined || $search === "") && !isCustom && hideThemes}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme (theme.id)}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{/if}
{#if filteredThemes.length == 0}
<NoThemeResultButton {search} />
{/if}
</section>
<style lang="scss">
section {
@apply flex flex-col;
}
</style>

View file

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

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import UserRelatedState from "../../Logic/State/UserRelatedState"
export let search: UIEventSource<string>
export let state: UserRelatedState & {
osmConnection: OsmConnection
locationControl?: UIEventSource<Loc>
}
export let onMainScreen: boolean = true
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
const stableIds = Stores.ListStabilized<string>(currentIds)
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
</script>
<ThemesList
{search}
{state}
{onMainScreen}
themes={customThemes}
isCustom={true}
hideThemes={false}
>
<svelte:fragment slot="title">
<!-- TODO: Change string to exclude html -->
{@html t.customThemeIntro.toString()}
</svelte:fragment>
</ThemesList>

View file

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

View file

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

View file

@ -18,7 +18,7 @@ import SimpleAddUI from "./BigComponents/SimpleAddUI"
import StrayClickHandler from "../Logic/Actors/StrayClickHandler"
import { DefaultGuiState } from "./DefaultGuiState"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
import * as home_location_json from "../assets/layers/home_location/home_location.json"
import home_location_json from "../assets/layers/home_location/home_location.json"
import NewNoteUi from "./Popup/NewNoteUi"
import Combine from "./Base/Combine"
import AddNewMarker from "./BigComponents/AddNewMarker"
@ -33,6 +33,9 @@ import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import SvelteUIElement from "./Base/SvelteUIElement"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
/**
* The default MapComplete GUI initializer
@ -236,10 +239,40 @@ export default class DefaultGUI {
const welcomeMessageMapControl = Toggle.If(state.featureSwitchWelcomeMessage, () =>
self.InitWelcomeMessage()
)
const communityIndex = Toggle.If(state.featureSwitchCommunityIndex, () => {
const communityIndexControl = new MapControlButton(Svg.community_svg())
const communityIndex = new ScrollableFullScreen(
() => Translations.t.communityIndex.title,
() => new SvelteUIElement(CommunityIndexView, { ...state }),
"community_index"
)
communityIndexControl.onClick(() => {
communityIndex.Activate()
})
return communityIndexControl
})
const testingBadge = Toggle.If(state.featureSwitchIsTesting, () =>
new FixedUiElement("TESTING").SetClass("alert m-2 border-2 border-black")
)
new Combine([welcomeMessageMapControl, userInfoMapControl, extraLink, testingBadge])
new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle,
() => new CopyrightPanel(state),
"copyright",
guiState.copyrightViewIsOpened
)
const copyright = new MapControlButton(Svg.copyright_svg()).onClick(() =>
guiState.copyrightViewIsOpened.setData(true)
)
new Combine([
welcomeMessageMapControl,
userInfoMapControl,
copyright,
communityIndex,
extraLink,
testingBadge,
])
.SetClass("flex flex-col")
.AttachTo("top-left")

View file

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

View file

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

View file

@ -22,6 +22,7 @@ import { LoginToggle } from "../Popup/LoginButton"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import Lazy from "../Base/Lazy"
import { Button } from "../Base/Button"
import ChartJs from "../Base/ChartJs"
interface NoteProperties {
id: number
@ -207,6 +208,181 @@ class MassAction extends Combine {
}
}
class Statistics extends Combine {
private static r() {
return Math.floor(Math.random() * 256)
}
private static randomColour(): string {
return "rgba(" + Statistics.r() + "," + Statistics.r() + "," + Statistics.r() + ")"
}
private static CreatePieByAuthor(closed_by: Record<string, number[]>): ChartJs {
const importers = Object.keys(closed_by)
importers.sort((a, b) => closed_by[b].at(-1) - closed_by[a].at(-1))
return new ChartJs(<any>{
type: "doughnut",
data: {
labels: importers,
datasets: [
{
label: "Closed by",
data: importers.map((k) => closed_by[k].at(-1)),
backgroundColor: importers.map((_) => Statistics.randomColour()),
},
],
},
})
}
private static CreateStatePie(noteStates: NoteState[]) {
const colors = {
imported: "#0aa323",
already_mapped: "#00bbff",
invalid: "#ff0000",
closed: "#000000",
not_found: "#ff6d00",
open: "#626262",
has_comments: "#a8a8a8",
}
const knownStates = Object.keys(colors)
const byState = knownStates.map(
(targetState) => noteStates.filter((ns) => ns.status === targetState).length
)
return new ChartJs(<any>{
type: "doughnut",
data: {
labels: knownStates.map(
(state, i) =>
state + " " + Math.floor((100 * byState[i]) / noteStates.length) + "%"
),
datasets: [
{
label: "Status by",
data: byState,
backgroundColor: knownStates.map((state) => colors[state]),
},
],
},
})
}
constructor(noteStates: NoteState[]) {
if (noteStates.length === 0) {
super([])
return
}
// We assume all notes are created at the same time
let dateOpened = new Date(noteStates[0].dateStr)
for (const noteState of noteStates) {
const openDate = new Date(noteState.dateStr)
if (openDate < dateOpened) {
dateOpened = openDate
}
}
const today = new Date()
const daysBetween = (today.getTime() - dateOpened.getTime()) / (1000 * 60 * 60 * 24)
const ranges = {
dates: [],
is_open: [],
}
const closed_by: Record<string, number[]> = {}
for (const noteState of noteStates) {
const closing_user = noteState.props.comments.at(-1).user
if (closed_by[closing_user] === undefined) {
closed_by[closing_user] = []
}
}
for (let i = -1; i < daysBetween; i++) {
const dt = new Date(dateOpened.getTime() + 24 * 60 * 60 * 1000 * i)
let open_count = 0
for (const closing_user in closed_by) {
closed_by[closing_user].push(0)
}
for (const noteState of noteStates) {
const openDate = new Date(noteState.dateStr)
if (openDate > dt) {
// Not created at this point
continue
}
if (noteState.props.closed_at === undefined) {
open_count++
} else if (
new Date(noteState.props.closed_at.substring(0, 10)).getTime() > dt.getTime()
) {
open_count++
} else {
const closing_user = noteState.props.comments.at(-1).user
const user_count = closed_by[closing_user]
user_count[user_count.length - 1] += 1
}
}
ranges.dates.push(
new Date(dateOpened.getTime() + i * 1000 * 60 * 60 * 24)
.toISOString()
.substring(0, 10)
)
ranges.is_open.push(open_count)
}
const labels = ranges.dates.map((i) => "" + i)
const data = {
labels: labels,
datasets: [
{
label: "Total open",
data: ranges.is_open,
fill: false,
borderColor: "rgb(75, 192, 192)",
tension: 0.1,
},
],
}
for (const closing_user in closed_by) {
if (closed_by[closing_user].at(-1) <= 10) {
continue
}
data.datasets.push({
label: "Closed by " + closing_user,
data: closed_by[closing_user],
fill: false,
borderColor: Statistics.randomColour(),
tension: 0.1,
})
}
super([
new ChartJs({
type: "line",
data,
options: {
scales: <any>{
yAxes: [
{
ticks: {
beginAtZero: true,
},
},
],
},
},
}),
new Combine([
Statistics.CreatePieByAuthor(closed_by),
Statistics.CreateStatePie(noteStates),
])
.SetClass("flex w-full h-32")
.SetStyle("width: 40rem"),
])
this.SetClass("block w-full h-64 border border-red")
}
}
class NoteTable extends Combine {
private static individualActions: [() => BaseUIElement, string][] = [
[Svg.not_found_svg, "This feature does not exist"],
@ -381,22 +557,24 @@ class BatchView extends Toggleable {
badges.push(toggle)
})
const fullTable = new NoteTable(noteStates, state)
const fullTable = new Combine([
new NoteTable(noteStates, state),
new Statistics(noteStates),
])
super(
new Combine([
new Title(theme + ": " + intro, 2),
new Combine(badges).SetClass("flex flex-wrap"),
]),
new VariableUiElement(
filterOn.map((filter) => {
if (filter === undefined) {
return fullTable
}
return new NoteTable(
noteStates.filter((ns) => ns.status === filter),
state
)
const notes = noteStates.filter((ns) => ns.status === filter)
return new Combine([new NoteTable(notes, state), new Statistics(notes)])
})
),
{
@ -422,10 +600,13 @@ class ImportInspector extends VariableUiElement {
url =
"https://api.openstreetmap.org/api/0.6/notes/search.json?display_name=" +
encodeURIComponent(userDetails["display_name"]) +
"&limit=10000&closed=730&sort=created_at&q=" +
encodeURIComponent(userDetails["search"] ?? "#import")
"&limit=10000&closed=730&sort=created_at&q="
if (userDetails["search"] !== "") {
url += userDetails["search"]
} else {
url += "#import"
}
}
const notes: UIEventSource<
{ error: string } | { success: { features: { properties: NoteProperties }[] } }
> = UIEventSource.FromPromiseWithErr(Utils.downloadJson(url))
@ -444,6 +625,11 @@ class ImportInspector extends VariableUiElement {
if (userDetails["uid"]) {
props = props.filter((n) => n.comments[0].uid === userDetails["uid"])
}
if (userDetails["display_name"] !== undefined) {
const display_name = <string>userDetails["display_name"]
props = props.filter((n) => n.comments[0].user === display_name)
}
const perBatch: NoteState[][] = Array.from(
ImportInspector.SplitNotesIntoBatches(props).values()
)
@ -462,6 +648,12 @@ class ImportInspector extends VariableUiElement {
]
}
contents.push(accordeon)
contents.push(
new Combine([
new Title("Statistics for all notes"),
new Statistics([].concat(...perBatch)),
])
)
const content = new Combine(contents)
return new LeftIndex(
[
@ -504,20 +696,53 @@ class ImportInspector extends VariableUiElement {
| "already_mapped"
| "not_found"
| "has_comments" = "open"
function has(keywords: string[], comment: string): boolean {
return keywords.some((keyword) => comment.toLowerCase().indexOf(keyword) >= 0)
}
if (prop.closed_at !== undefined) {
const lastComment = prop.comments[prop.comments.length - 1].text.toLowerCase()
if (lastComment.indexOf("does not exist") >= 0) {
if (has(["does not exist", "bestaat niet", "geen"], lastComment)) {
status = "not_found"
} else if (lastComment.indexOf("already mapped") >= 0) {
} else if (
has(
[
"already mapped",
"reeds",
"dubbele note",
"stond er al",
"stonden er al",
"staat er al",
"staan er al",
"stond al",
"stonden al",
"staat al",
"staan al",
],
lastComment
)
) {
status = "already_mapped"
} else if (
lastComment.indexOf("invalid") >= 0 ||
lastComment.indexOf("incorrecto") >= 0
lastComment.indexOf("incorrect") >= 0
) {
status = "invalid"
} else if (
["imported", "erbij", "toegevoegd", "added"].some(
(keyword) => lastComment.toLowerCase().indexOf(keyword) >= 0
has(
[
"imported",
"erbij",
"toegevoegd",
"added",
"gemapped",
"gemapt",
"mapped",
"done",
"openstreetmap.org/changeset",
],
lastComment
)
) {
status = "imported"
@ -559,7 +784,7 @@ class ImportViewerGui extends LoginToggle {
(ud) => {
const display_name = displayNameParam.data
const search = searchParam.data
if (display_name !== "" && search !== "") {
if (display_name !== "" || search !== "") {
return new ImportInspector({ display_name, search }, undefined)
}
return new ImportInspector(ud, state)

View file

@ -23,17 +23,22 @@ import { FlowStep } from "./FlowStep"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Title from "../Base/Title"
import CheckBoxes from "../Input/Checkboxes"
import { AllTagsPanel } from "../AllTagsPanel"
import AllTagsPanel from "../AllTagsPanel.svelte"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import { Feature, Point } from "geojson"
import DivContainer from "../Base/DivContainer"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import SvelteUIElement from "../Base/SvelteUIElement"
class PreviewPanel extends ScrollableFullScreen {
constructor(tags: UIEventSource<any>) {
super(
(_) => new FixedUiElement("Element to import"),
(_) => new Combine(["The tags are:", new AllTagsPanel(tags)]).SetClass("flex flex-col"),
(_) =>
new Combine([
"The tags are:",
new SvelteUIElement(AllTagsPanel, { tags }),
]).SetClass("flex flex-col"),
"element"
)
}

View file

@ -37,7 +37,7 @@ export default class SelectTheme
constructor(params: { features: any[]; layer: LayerConfig; bbox: BBox }) {
const t = Translations.t.importHelper.selectTheme
let options: InputElement<string>[] = AllKnownLayouts.layoutsList
let options: InputElement<string>[] = Array.from(AllKnownLayouts.allKnownLayouts.values())
.filter((th) => th.layers.some((l) => l.id === params.layer.id))
.filter((th) => th.id !== "personal")
.map(
@ -60,7 +60,7 @@ export default class SelectTheme
return []
}
// we get the layer with the correct ID via the actual theme config, as the actual theme might have different presets due to overrides
const themeConfig = AllKnownLayouts.layoutsList.find((th) => th.id === theme)
const themeConfig = AllKnownLayouts.allKnownLayouts.get(theme)
const layer = themeConfig.layers.find((l) => l.id === params.layer.id)
return layer.presets
})

View file

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

View file

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

View file

@ -52,10 +52,6 @@ export class TextFieldDef {
}
}
protectedisValid(s: string, _: (() => string) | undefined): boolean {
return true
}
public getFeedback(s: string): Translation {
const tr = Translations.t.validation[this.name]
if (tr !== undefined) {
@ -82,6 +78,9 @@ export class TextFieldDef {
}
options["textArea"] = this.name === "text"
if (this.name === "text") {
options["htmlType"] = "area"
}
const self = this
@ -258,11 +257,11 @@ class WikidataTextField extends TextFieldDef {
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
@ -295,7 +294,8 @@ class WikidataTextField extends TextFieldDef {
"square",
"plaza",
],
"nl": ["straat","plein","pad","weg",laan"]
"nl": ["straat","plein","pad","weg",laan"],
"fr":["route (de|de la|de l'| de le)"]
},
"#": "Remove streets and parks from the search results:"
@ -361,29 +361,34 @@ Another example is to search for species and trees:
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
const defaultValueCandidate = Locale.language.map((lg) => {
const prefixesUnrwapped: RegExp[] = (
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
).map((s) => new RegExp("^" + s, "i"))
const postfixesUnwrapped: RegExp[] = (
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
).map((s) => new RegExp(s + "$", "i"))
let clipped = searchFor
Locale.language
.map((lg) => {
const prefixesUnrwapped: string[] = prefixes[lg] ?? prefixes
const postfixesUnwrapped: string[] = postfixes[lg] ?? postfixes
let clipped = searchFor
for (const postfix of postfixesUnwrapped) {
if (searchFor.endsWith(postfix)) {
clipped = searchFor.substring(0, searchFor.length - postfix.length)
break
}
for (const postfix of postfixesUnwrapped) {
const match = searchFor.match(postfix)
if (match !== null) {
clipped = searchFor.substring(0, searchFor.length - match[0].length)
break
}
}
for (const prefix of prefixesUnrwapped) {
if (searchFor.startsWith(prefix)) {
clipped = searchFor.substring(prefix.length)
break
}
for (const prefix of prefixesUnrwapped) {
const match = searchFor.match(prefix)
if (match !== null) {
clipped = searchFor.substring(match[0].length)
break
}
return clipped
})
.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
}
return clipped
})
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
}
let instanceOf: number[] = Utils.NoNull(
@ -421,7 +426,7 @@ class OpeningHoursTextField extends TextFieldDef {
[
[
"prefix",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse",
"Piece of text that will always be added to the front of the generated opening hours. If the OSM-data does not start with this, it will fail to parse.",
],
[
"postfix",
@ -584,7 +589,7 @@ class StringTextField extends TextFieldDef {
class TextTextField extends TextFieldDef {
declare inputmode: "text"
constructor() {
super("text", "A longer piece of text")
super("text", "A longer piece of text. Uses an textArea instead of a textField")
}
}

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import EditableTagRendering from "./EditableTagRendering"
import QuestionBox from "./QuestionBox"
import Combine from "../Base/Combine"
@ -35,9 +35,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
if (state === undefined) {
throw "State is undefined!"
}
const showAllQuestions = state.featureSwitchShowAllQuestions.map(
(fsShow) => fsShow || state.showAllQuestionsAtOnce.data,
[state.showAllQuestionsAtOnce]
)
super(
() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state),
() => FeatureInfoBox.GenerateContent(tags, layerConfig, state),
() => FeatureInfoBox.GenerateContent(tags, layerConfig, state, showAllQuestions),
options?.hashToShow ?? tags.data.id ?? "item",
options?.isShown,
options
@ -79,21 +83,23 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
public static GenerateContent(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState
state: FeaturePipelineState,
showAllQuestions?: Store<boolean>
): BaseUIElement {
return new Toggle(
new Combine([
Svg.delete_icon_svg().SetClass("w-8 h-8"),
Translations.t.delete.isDeleted,
]).SetClass("flex justify-center font-bold items-center"),
FeatureInfoBox.GenerateMainContent(tags, layerConfig, state),
FeatureInfoBox.GenerateMainContent(tags, layerConfig, state, showAllQuestions),
tags.map((t) => t["_deleted"] == "yes")
)
}
private static GenerateMainContent(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState
state: FeaturePipelineState,
showAllQuestions?: Store<boolean>
): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>()
const t = Translations.t.general
@ -108,8 +114,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
tagRenderings: questions,
units: layerConfig.units,
showAllQuestionsAtOnce:
questionSpec?.freeform?.helperArgs["showAllQuestions"] ??
state.featureSwitchShowAllQuestions,
questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? showAllQuestions,
})
questionBoxes.set(groupName, questionBox)
}

View file

@ -35,7 +35,7 @@ import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/Crea
import { Tag } from "../../Logic/Tags/Tag"
import TagApplyButton from "./TagApplyButton"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import * as conflation_json from "../../assets/layers/conflation/conflation.json"
import conflation_json from "../../assets/layers/conflation/conflation.json"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LoginToggle } from "./LoginButton"
import { AutoAction } from "./AutoApplyButton"
@ -644,7 +644,7 @@ export class ImportPointButton extends AbstractImportButton {
},
{
name: "maproulette_id",
doc: "If given, the maproulette challenge will be marked as fixed",
doc: "The property name of the maproulette_id - this is probably `mr_taskId`. If given, the maproulette challenge will be marked as fixed. Only use this if part of a maproulette-layer.",
},
],
{ showRemovedTags: false }
@ -702,7 +702,7 @@ export class ImportPointButton extends AbstractImportButton {
Hash.hash.setData(newElementAction.newElementId)
if (note_id !== undefined) {
state.osmConnection.closeNote(note_id, "imported")
await state.osmConnection.closeNote(note_id, "imported")
originalFeatureTags.data["closed_at"] = new Date().toISOString()
originalFeatureTags.ping()
}
@ -720,7 +720,7 @@ export class ImportPointButton extends AbstractImportButton {
)
} else {
console.log("Marking maproulette task as fixed")
state.maprouletteConnection.closeTask(Number(maproulette_id))
await state.maprouletteConnection.closeTask(Number(maproulette_id))
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
originalFeatureTags.ping()
}

View file

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

View file

@ -87,27 +87,33 @@ export class NearbyImageVis implements SpecialVisualization {
const nearby = new Lazy(() => {
const towardsCenter = new CheckBox(t.onlyTowards, false)
const radiusValue =
state?.osmConnection?.GetPreference("nearby-images-radius", "300").sync(
const maxSearchRadius = 100
const stepSize = 10
const defaultValue = Math.floor(maxSearchRadius / (2 * stepSize)) * stepSize
const fromOsmPreferences = state?.osmConnection
?.GetPreference("nearby-images-radius", "" + defaultValue)
.sync(
(s) => Number(s),
[],
(i) => "" + i
) ?? new UIEventSource(300)
)
const radiusValue = new UIEventSource(fromOsmPreferences.data)
radiusValue.addCallbackAndRunD((v) => fromOsmPreferences.setData(v))
const radius = new Slider(25, 500, {
const radius = new Slider(stepSize, maxSearchRadius, {
value: radiusValue,
step: 25,
step: 10,
})
const alreadyInTheImage = AllImageProviders.LoadImagesFor(tagSource)
const options: NearbyImageOptions & { value } = {
lon,
lat,
searchRadius: 500,
searchRadius: maxSearchRadius,
shownRadius: radius.GetValue(),
value: selectedImage,
blacklist: alreadyInTheImage,
towardscenter: towardsCenter.GetValue(),
maxDaysOld: 365 * 5,
maxDaysOld: 365 * 3,
}
const slideshow = canBeEdited
? new SelectOneNearbyImage(options, state)

View file

@ -15,7 +15,7 @@ import { SubtleButton } from "../Base/SubtleButton"
import { GeoOperations } from "../../Logic/GeoOperations"
import { ElementStorage } from "../../Logic/ElementStorage"
import Lazy from "../Base/Lazy"
import P4C from "pic4carto"
export interface P4CPicture {
pictureUrl: string
date?: number
@ -175,7 +175,6 @@ export default class NearbyImages extends Lazy {
options: NearbyImageOptions,
state?: { allElements: ElementStorage }
) {
const P4C = require("../../vendor/P4C.min")
const picManager = new P4C.PicturesManager({})
const searchRadius = options.searchRadius ?? 500

View file

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

View file

@ -22,7 +22,7 @@ export default class QuestionBox extends VariableUiElement {
tagsSource: UIEventSource<any>
tagRenderings: TagRenderingConfig[]
units: Unit[]
showAllQuestionsAtOnce?: boolean | UIEventSource<boolean>
showAllQuestionsAtOnce?: boolean | Store<boolean>
}
) {
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])

View file

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

View file

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

View file

@ -85,6 +85,14 @@ export default class TagRenderingQuestion extends Combine {
),
3
)
let questionHint = undefined
if (configuration.questionhint !== undefined) {
questionHint = new SubstitutedTranslation(
configuration.questionhint,
tags,
state
).SetClass("font-bold subtle")
}
const feedback = new UIEventSource<Translation>(undefined)
const inputElement: ReadonlyInputElement<UploadableTag> = new VariableInputElement(
@ -139,22 +147,21 @@ export default class TagRenderingQuestion extends Combine {
}
super([
question,
questionHint,
inputElement,
new Combine([
new VariableUiElement(
feedback.map(
(t) =>
t
?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem")
?.SetClass("alert flex") ?? bottomTags
)
),
new Combine([new Combine([options.cancelButton]), saveButton]).SetClass(
"flex justify-end flex-wrap-reverse"
),
]).SetClass("flex mt-2 justify-between"),
new VariableUiElement(
feedback.map(
(t) =>
t
?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem")
?.SetClass("alert flex") ?? bottomTags
)
),
new Combine([options.cancelButton, saveButton]).SetClass(
"flex justify-end flex-wrap-reverse"
),
new Toggle(
Translations.t.general.testing.SetClass("alert"),
Translations.t.general.testing.SetClass("block alert"),
undefined,
state?.featureSwitchIsTesting
),

View file

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

View file

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

View file

@ -20,7 +20,7 @@ import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { NearbyImageVis } from "./Popup/NearbyImageVis"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import { AllTagsPanel } from "./AllTagsPanel"
import AllTagsPanel from "./AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
@ -30,7 +30,6 @@ import WikipediaBox from "./Wikipedia/WikipediaBox"
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
import { Translation } from "./i18n/Translation"
import Translations from "./i18n/Translations"
import MangroveReviews from "../Logic/Web/MangroveReviews"
import ReviewForm from "./Reviews/ReviewForm"
import ReviewElement from "./Reviews/ReviewElement"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
@ -53,10 +52,91 @@ import StatisticsPanel from "./BigComponents/StatisticsPanel"
import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
}
if (viz === undefined) {
return undefined
}
return new Combine([
new Title(viz.funcName, 3),
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]
})
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
).SetClass("literal-code"),
])
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
)
return new Combine([
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax", 4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
new FixedUiElement(
JSON.stringify(
{
render: {
special: {
type: "some_special_visualisation",
argname: "some_arg",
message: {
en: "some other really long message",
nl: "een boodschap in een andere taal",
},
other_arg_name: "more args",
},
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)",
},
after: {
en: "Some text to put after the element, e.g. a footer",
},
},
},
null,
" "
)
).SetClass("code"),
'In other words: use `{ "before": ..., "after": ..., "special": {"type": ..., "argname": ...argvalue...}`. The args are in the `special` block; an argvalue can be a string, a translation or another value. (Refer to class `RewriteSpecial` in case of problems)',
]).SetClass("flex flex-col"),
...helpTexts,
]).SetClass("flex flex-col")
}
private static initList(): SpecialVisualization[] {
const specialVisualizations: SpecialVisualization[] = [
new HistogramViz(),
@ -81,7 +161,8 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: (state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state),
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
},
{
funcName: "image_carousel",
@ -418,6 +499,24 @@ export default class SpecialVisualizations {
defaultValue: "id",
},
],
example:
" The following example sets the status to '2' (false positive)\n" +
"\n" +
"```json\n" +
"{\n" +
' "id": "mark_duplicate",\n' +
' "render": {\n' +
' "special": {\n' +
' "type": "maproulette_set_status",\n' +
' "message": {\n' +
' "en": "Mark as not found or false positive"\n' +
" },\n" +
' "status": "2",\n' +
' "image": "close"\n' +
" }\n" +
" }\n" +
"}\n" +
"```",
constr: (state, tags, args) => {
const isUploading = new UIEventSource(false)
const t = Translations.t.notes
@ -480,6 +579,10 @@ export default class SpecialVisualizations {
args: [],
constr(state, tagSource, argument, guistate) {
let parentId = tagSource.data.mr_challengeId
if (parentId === undefined) {
console.warn("Element ", tagSource.data.id, " has no mr_challengeId")
return undefined
}
let challenge = Stores.FromPromise(
Utils.downloadJsonCached(
`https://maproulette.org/api/v2/challenge/${parentId}`,
@ -512,7 +615,102 @@ export default class SpecialVisualizations {
})
)
},
docs: "Show details of a MapRoulette task",
docs: "Fetches the metadata of MapRoulette campaign that this task is part of and shows those details (namely `title`, `description` and `instruction`).\n\nThis reads the property `mr_challengeId` to detect the parent campaign.",
},
{
funcName: "maproulette_set_status",
docs: "Change the status of the given MapRoulette task",
args: [
{
name: "message",
doc: "A message to show to the user",
},
{
name: "image",
doc: "Image to show",
defaultValue: "confirm",
},
{
name: "message_confirm",
doc: "What to show when the task is closed, either by the user or was already closed.",
},
{
name: "status",
doc: "A statuscode to apply when the button is clicked. 1 = `close`, 2 = `false_positive`, 3 = `skip`, 4 = `deleted`, 5 = `already fixed` (on the map, e.g. for duplicates), 6 = `too hard`",
defaultValue: "1",
},
{
name: "maproulette_id",
doc: "The property name containing the maproulette id",
defaultValue: "mr_taskId",
},
],
constr: (state, tagsSource, args, guistate) => {
let [message, image, message_closed, status, maproulette_id_key] = args
if (image === "") {
image = "confirm"
}
if (Svg.All[image] !== undefined || Svg.All[image + ".svg"] !== undefined) {
if (image.endsWith(".svg")) {
image = image.substring(0, image.length - 4)
}
image = Svg[image + "_ui"]()
}
const failed = new UIEventSource(false)
const closeButton = new SubtleButton(image, message).OnClickWithLoading(
Translations.t.general.loading,
async () => {
const maproulette_id =
tagsSource.data[maproulette_id_key] ?? tagsSource.data.id
try {
await state.maprouletteConnection.closeTask(
Number(maproulette_id),
Number(status),
{
tags: `MapComplete MapComplete:${state.layoutToUse.id}`,
}
)
tagsSource.data["mr_taskStatus"] =
Maproulette.STATUS_MEANING[Number(status)]
tagsSource.data.status = status
tagsSource.ping()
} catch (e) {
console.error(e)
failed.setData(true)
}
}
)
let message_closed_element = undefined
if (message_closed !== undefined && message_closed !== "") {
message_closed_element = new FixedUiElement(message_closed)
}
return new VariableUiElement(
tagsSource
.map(
(tgs) =>
tgs["status"] ??
Maproulette.STATUS_MEANING[tgs["mr_taskStatus"]]
)
.map(Number)
.map(
(status) => {
if (failed.data) {
return new FixedUiElement(
"ERROR - could not close the MapRoulette task"
).SetClass("block alert")
}
if (status === Maproulette.STATUS_OPEN) {
return closeButton
}
return message_closed_element ?? "Closed!"
},
[failed]
)
)
},
},
{
funcName: "statistics",
@ -612,8 +810,8 @@ export default class SpecialVisualizations {
special: {
type: "multi",
key: "_doors_from_building_properties",
tagRendering: {
render: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}",
tagrendering: {
en: "The building containing this feature has a <a href='#{id}'>door</a> of width {entrance:width}",
},
},
},
@ -671,82 +869,4 @@ export default class SpecialVisualizations {
return specialVisualizations
}
public static DocumentationFor(viz: string | SpecialVisualization): BaseUIElement | undefined {
if (typeof viz === "string") {
viz = SpecialVisualizations.specialVisualizations.find((sv) => sv.funcName === viz)
}
if (viz === undefined) {
return undefined
}
return new Combine([
new Title(viz.funcName, 3),
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]
})
)
: undefined,
new Title("Example usage of " + viz.funcName, 4),
new FixedUiElement(
viz.example ??
"`{" +
viz.funcName +
"(" +
viz.args.map((arg) => arg.defaultValue).join(",") +
")}`"
).SetClass("literal-code"),
])
}
public static HelpMessage() {
const helpTexts = SpecialVisualizations.specialVisualizations.map((viz) =>
SpecialVisualizations.DocumentationFor(viz)
)
return new Combine([
new Combine([
new Title("Special tag renderings", 1),
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
new Title("Using expanded syntax", 4),
`Instead of using \`{"render": {"en": "{some_special_visualisation(some_arg, some other really long message, more args)} , "nl": "{some_special_visualisation(some_arg, een boodschap in een andere taal, more args)}}\`, one can also write`,
new FixedUiElement(
JSON.stringify(
{
render: {
special: {
type: "some_special_visualisation",
before: {
en: "Some text to prefix before the special element (e.g. a title)",
nl: "Een tekst om voor het element te zetten (bv. een titel)",
},
after: {
en: "Some text to put after the element, e.g. a footer",
},
argname: "some_arg",
message: {
en: "some other really long message",
nl: "een boodschap in een andere taal",
},
other_arg_name: "more args",
},
},
},
null,
" "
)
).SetClass("code"),
]).SetClass("flex flex-col"),
...helpTexts,
]).SetClass("flex flex-col")
}
}

View file

@ -8,17 +8,17 @@ import { Utils } from "../Utils"
import Combine from "./Base/Combine"
import { StackedRenderingChart } from "./BigComponents/TagRenderingChart"
import { LayerFilterPanel } from "./BigComponents/FilterView"
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
import MapState from "../Logic/State/MapState"
import BaseUIElement from "./BaseUIElement"
import Title from "./Base/Title"
import { FixedUiElement } from "./Base/FixedUiElement"
import List from "./Base/List"
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
import mcChanges from "../assets/generated/themes/mapcomplete-changes.json"
class StatisticsForOverviewFile extends Combine {
constructor(homeUrl: string, paths: string[]) {
paths = paths.filter((p) => !p.endsWith("file-overview.json"))
const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0]
const layer = new LayoutConfig(<any>mcChanges, true).layers[0]
const filteredLayer = MapState.InitializeFilteredLayers(
{ id: "statistics-view", layers: [layer] },
undefined

View file

@ -1,50 +0,0 @@
import Combine from "./Base/Combine"
import { FixedUiElement } from "./Base/FixedUiElement"
import { SubtleButton } from "./Base/SubtleButton"
import Svg from "../Svg"
import { LocalStorageSource } from "../Logic/Web/LocalStorageSource"
import Toggle from "./Input/Toggle"
export default class UserSurveyPanel extends Toggle {
private static readonly userSurveyHasBeenTaken = LocalStorageSource.GetParsed(
"usersurvey-has-been-taken",
false
)
constructor() {
super(
new Combine([
new FixedUiElement("Thanks for taking the survey!").SetClass("thanks px-2"),
new SubtleButton(Svg.star_svg(), "Take the user survey again", {
imgSize: "h-6 w-6",
})
.onClick(() => {
window.open(
"https://framaforms.org/mapcomplete-usage-survey-1672687708",
"_blank"
)
UserSurveyPanel.userSurveyHasBeenTaken.setData(false)
})
.SetClass("h-12"),
]),
new Combine([
new FixedUiElement("Please, fill in the user survey").SetClass("alert"),
"Hey! We'd like to get to know you better - would you mind to help out by filling out this form? Your opinion is important",
new FixedUiElement(
"We are specifically searching responses from underrepresented groups, such as non-technical people, minorities, women, people without an account, people of colour, ..."
).SetClass("font-bold"),
"Results are fully anonymous and are used to improve MapComplete. We don't ask private information. So, don't hesitate and fill it out!",
new SubtleButton(Svg.star_outline_svg(), "Take the survey").onClick(() => {
window.open(
"https://framaforms.org/mapcomplete-usage-survey-1672687708",
"_blank"
)
UserSurveyPanel.userSurveyHasBeenTaken.setData(true)
}),
]).SetClass("block border-2 border-black rounded-xl flex flex-col p-2"),
UserSurveyPanel.userSurveyHasBeenTaken
)
this.SetStyle("max-width: 40rem")
}
}

View file

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

View file

@ -2,6 +2,7 @@ import Locale from "./Locale"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import LinkToWeblate from "../Base/LinkToWeblate"
import { SvelteComponent } from "svelte"
export class Translation extends BaseUIElement {
public static forcedLanguage = undefined

View file

@ -1,13 +1,13 @@
import { FixedUiElement } from "../Base/FixedUiElement"
import { Translation, TypedTranslation } from "./Translation"
import BaseUIElement from "../BaseUIElement"
import * as known_languages from "../../assets/generated/used_languages.json"
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
import LanguageUtils from "../../Utils/LanguageUtils"
export default class Translations {
static readonly t: typeof CompiledTranslations.t & Readonly<typeof CompiledTranslations.t> =
CompiledTranslations.t
private static knownLanguages = new Set(known_languages.languages)
private static knownLanguages = LanguageUtils.usedLanguages
constructor() {
throw "Translations is static. If you want to intitialize a new translation, use the singular form"
}