forked from MapComplete/MapComplete
Merge develop
This commit is contained in:
commit
e67d740769
719 changed files with 30508 additions and 15682 deletions
54
UI/AllTagsPanel.svelte
Normal file
54
UI/AllTagsPanel.svelte
Normal 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>
|
|
@ -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")
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
82
UI/Base/SubtleButton.svelte
Normal file
82
UI/Base/SubtleButton.svelte
Normal 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>
|
|
@ -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(
|
||||
|
|
37
UI/Base/SvelteUIElement.ts
Normal file
37
UI/Base/SvelteUIElement.ts
Normal 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
18
UI/Base/ToSvelte.svelte
Normal 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} />
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
45
UI/BigComponents/CommunityIndexView.svelte
Normal file
45
UI/BigComponents/CommunityIndexView.svelte
Normal 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>
|
50
UI/BigComponents/ContactLink.svelte
Normal file
50
UI/BigComponents/ContactLink.svelte
Normal 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>
|
|
@ -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 !== "") {
|
||||
|
|
38
UI/BigComponents/CustomGeneratorButton.svelte
Normal file
38
UI/BigComponents/CustomGeneratorButton.svelte
Normal 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>
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
47
UI/BigComponents/HiddenThemeList.svelte
Normal file
47
UI/BigComponents/HiddenThemeList.svelte
Normal 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>
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
65
UI/BigComponents/NoThemeResultButton.svelte
Normal file
65
UI/BigComponents/NoThemeResultButton.svelte
Normal 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>
|
24
UI/BigComponents/ProfessionalServicesButton.svelte
Normal file
24
UI/BigComponents/ProfessionalServicesButton.svelte
Normal 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>
|
|
@ -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(() => {
|
||||
|
|
|
@ -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,
|
||||
|
|
110
UI/BigComponents/ThemeButton.svelte
Normal file
110
UI/BigComponents/ThemeButton.svelte
Normal 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>
|
|
@ -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
|
||||
|
|
82
UI/BigComponents/ThemesList.svelte
Normal file
82
UI/BigComponents/ThemesList.svelte
Normal 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>
|
|
@ -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")
|
||||
}
|
||||
}
|
35
UI/BigComponents/UnofficialThemeList.svelte
Normal file
35
UI/BigComponents/UnofficialThemeList.svelte
Normal 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>
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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[]>([])
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue