refactoring

This commit is contained in:
Pieter Vander Vennet 2023-03-28 05:13:48 +02:00
parent b94a8f5745
commit 5d0fe31c41
114 changed files with 2412 additions and 2958 deletions

View file

@ -13,6 +13,7 @@ import { OpenIdEditor, OpenJosm } from "./CopyrightPanel"
import Toggle from "../Input/Toggle"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import { DefaultGuiState } from "../DefaultGuiState"
import DefaultGUI from "../DefaultGUI"
export class BackToThemeOverview extends Toggle {
constructor(
@ -42,6 +43,7 @@ export class ActionButtons extends Combine {
readonly locationControl: Store<Loc>
readonly osmConnection: OsmConnection
readonly featureSwitchMoreQuests: Store<boolean>
readonly defaultGuiState: DefaultGuiState
}) {
const imgSize = "h-6 w-6"
const iconStyle = "height: 1.5rem; width: 1.5rem"
@ -82,8 +84,8 @@ export class ActionButtons extends Combine {
Translations.t.translations.activateButton
).onClick(() => {
ScrollableFullScreen.collapse()
DefaultGuiState.state.userInfoIsOpened.setData(true)
DefaultGuiState.state.userInfoFocusedQuestion.setData("translation-mode")
state.defaultGuiState.userInfoIsOpened.setData(true)
state.defaultGuiState.userInfoFocusedQuestion.setData("translation-mode")
}),
])
this.SetClass("block w-full link-no-underline")

View file

@ -14,54 +14,53 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Title from "../Base/Title"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import Toggle from "../Input/Toggle"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import ContributorCount from "../../Logic/ContributorCount"
import Img from "../Base/Img"
import { TypedTranslation } from "../i18n/Translation"
import GeoIndexedStore from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
export class OpenIdEditor extends VariableUiElement {
constructor(
state: { readonly locationControl: Store<Loc> },
mapProperties: { location: Store<{ lon: number; lat: number }>; zoom: Store<number> },
iconStyle?: string,
objectId?: string
) {
const t = Translations.t.general.attribution
super(
state.locationControl.map((location) => {
let elementSelect = ""
if (objectId !== undefined) {
const parts = objectId.split("/")
const tp = parts[0]
if (
parts.length === 2 &&
!isNaN(Number(parts[1])) &&
(tp === "node" || tp === "way" || tp === "relation")
) {
elementSelect = "&" + tp + "=" + parts[1]
mapProperties.location.map(
(location) => {
let elementSelect = ""
if (objectId !== undefined) {
const parts = objectId.split("/")
const tp = parts[0]
if (
parts.length === 2 &&
!isNaN(Number(parts[1])) &&
(tp === "node" || tp === "way" || tp === "relation")
) {
elementSelect = "&" + tp + "=" + parts[1]
}
}
}
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
location?.zoom ?? 0
}/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
url: idLink,
newTab: true,
})
})
const idLink = `https://www.openstreetmap.org/edit?editor=id${elementSelect}#map=${
mapProperties.zoom?.data ?? 0
}/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
url: idLink,
newTab: true,
})
},
[mapProperties.zoom]
)
)
}
}
export class OpenJosm extends Combine {
constructor(
state: { osmConnection: OsmConnection; currentBounds: Store<BBox> },
iconStyle?: string
) {
constructor(osmConnection: OsmConnection, bounds: Store<BBox>, iconStyle?: string) {
const t = Translations.t.general.attribution
const josmState = new UIEventSource<string>(undefined)
@ -83,21 +82,21 @@ export class OpenJosm extends Combine {
const toggle = new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data
if (bounds === undefined) {
return undefined
const bbox = bounds.data
if (bbox === undefined) {
return
}
const top = bounds.getNorth()
const bottom = bounds.getSouth()
const right = bounds.getEast()
const left = bounds.getWest()
const top = bbox.getNorth()
const bottom = bbox.getSouth()
const right = bbox.getEast()
const left = bbox.getWest()
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
Utils.download(josmLink)
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
.catch((_) => josmState.setData("ERROR"))
}),
undefined,
state.osmConnection.userDetails.map(
osmConnection.userDetails.map(
(ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible
)
)
@ -113,14 +112,14 @@ export default class CopyrightPanel extends Combine {
private static LicenseObject = CopyrightPanel.GenerateLicenses()
constructor(state: {
layoutToUse: LayoutConfig
featurePipeline: FeaturePipeline
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
layout: LayoutConfig
bounds: Store<BBox>
osmConnection: OsmConnection
dataIsLoading: Store<boolean>
perLayer: ReadonlyMap<string, GeoIndexedStore>
}) {
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const layoutToUse = state.layout
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
CopyrightPanel.IconAttribution

View file

@ -1,7 +1,6 @@
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import State from "../../State"
import { Utils } from "../../Utils"
import Combine from "../Base/Combine"
import CheckBoxes from "../Input/Checkboxes"

View file

@ -1,16 +1,13 @@
import { Utils } from "../../Utils"
import { FixedInputElement } from "../Input/FixedInputElement"
import { RadioButton } from "../Input/RadioButton"
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle, { ClickableToggle } from "../Input/Toggle"
import Toggle from "../Input/Toggle"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
import Svg from "../../Svg"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import BackgroundSelector from "./BackgroundSelector"
import FilteredLayer from "../../Models/FilteredLayer"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
@ -18,9 +15,7 @@ import ValidatedTextField from "../Input/ValidatedTextField"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { InputElement } from "../Input/InputElement"
import { DropDown } from "../Input/DropDown"
import { FixedUiElement } from "../Base/FixedUiElement"
import BaseLayer from "../../Models/BaseLayer"
import Loc from "../../Models/Loc"
import { BackToThemeOverview } from "./ActionButtons"
@ -272,102 +267,6 @@ export class LayerFilterPanel extends Combine {
return [tr, settableFilter]
}
private static createCheckboxFilter(
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
let option = filterConfig.options[0]
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")
return [
toggle,
toggle.isEnabled.sync(
(enabled) =>
enabled
? {
currentFilter: option.osmTags,
state: "true",
}
: undefined,
[],
(f) => f !== undefined
),
]
}
private static createMultiFilter(
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
let options = filterConfig.options
const values: FilterState[] = options.map((f, i) => ({
currentFilter: f.osmTags,
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(
options.map(
(option, i) =>
new FixedInputElement(option.question.Clone().SetClass("block"), i)
),
{
value,
dontStyle: true,
}
)
} else {
filterPicker = new DropDown(
"",
options.map((option, i) => ({
value: i,
shown: option.question.Clone(),
})),
value
)
}
return [
filterPicker,
filterPicker.GetValue().sync(
(i) => values[i],
[],
(selected) => {
const v = selected?.state
if (v === undefined || typeof v === "string") {
return undefined
}
return v
}
),
]
}
private static createFilter(
state: {},
filterConfig: FilterConfig
@ -376,12 +275,6 @@ export class LayerFilterPanel extends Combine {
return LayerFilterPanel.createFilterWithFields(state, filterConfig)
}
if (filterConfig.options.length === 1) {
return LayerFilterPanel.createCheckboxFilter(filterConfig)
}
const filter = LayerFilterPanel.createMultiFilter(filterConfig)
filter[0].SetClass("pl-2")
return filter
return undefined
}
}

View file

@ -0,0 +1,79 @@
<script lang="ts">/**
* The FilterView shows the various options to enable/disable a single layer.
*/
import type FilteredLayer from "../../Models/FilteredLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Checkbox from "../Base/Checkbox.svelte";
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import type { Writable } from "svelte/store";
import If from "../Base/If.svelte";
import Dropdown from "../Base/Dropdown.svelte";
import { onDestroy } from "svelte";
export let filteredLayer: FilteredLayer;
export let zoomlevel: number;
let layer: LayerConfig = filteredLayer.layerDef;
let isDisplayed: boolean = filteredLayer.isDisplayed.data;
onDestroy(filteredLayer.isDisplayed.addCallbackAndRunD(d => {
isDisplayed = d;
return false
}));
/**
* Gets a UIEventSource as boolean for the given option, to be used with a checkbox
*/
function getBooleanStateFor(option: FilterConfig): Writable<boolean> {
const state = filteredLayer.appliedFilters.get(option.id);
return state.sync(f => f === 0, [], (b) => b ? 0 : undefined);
}
/**
* Gets a UIEventSource as number for the given option, to be used with a dropdown or radiobutton
*/
function getStateFor(option: FilterConfig): Writable<number> {
return filteredLayer.appliedFilters.get(option.id);
}
</script>
{#if filteredLayer.layerDef.name}
<div>
<label class="flex gap-1">
<Checkbox selected={filteredLayer.isDisplayed} />
<If condition={filteredLayer.isDisplayed}>
<ToSvelte construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6")}></ToSvelte>
<ToSvelte slot="else" construct={() => layer.defaultIcon()?.SetClass("block h-6 w-6 opacity-50")}></ToSvelte>
</If>
{filteredLayer.layerDef.name}
</label>
<If condition={filteredLayer.isDisplayed}>
<div id="subfilters" class="flex flex-col gap-y-1 mb-4 ml-4">
{#each filteredLayer.layerDef.filters as filter}
<div>
<!-- There are three (and a half) modes of filters: a single checkbox, a radio button/dropdown or with fields -->
{#if filter.options.length === 1 && filter.options[0].fields.length === 0}
<label>
<Checkbox selected={getBooleanStateFor(filter)} />
{filter.options[0].question}
</label>
{/if}
{#if filter.options.length > 1}
<Dropdown value={getStateFor(filter)}>
{#each filter.options as option, i}
<option value={i}>
{ option.question}
</option>
{/each}
</Dropdown>
{/if}
</div>
{/each}
</div>
</If>
</div>
{/if}

View file

@ -1,8 +1,7 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import { BBox } from "../../Logic/BBox"
import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
@ -94,14 +93,13 @@ export class GeolocationControl extends VariableUiElement {
return
}
if (geolocationState.currentGPSLocation.data === undefined) {
// A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
if (currentLocation === undefined) {
// No location is known yet, not much we can do
lastClick.setData(new Date())
return
}
// A location _is_ known! Let's move to this location
const currentLocation = geolocationState.currentGPSLocation.data
const inBounds = state.bounds.data.contains([
currentLocation.longitude,
currentLocation.latitude,

View file

@ -0,0 +1,94 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import type { Feature } from "geojson";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import Svg from "../../Svg.js";
import Translations from "../i18n/Translations";
import Loading from "../Base/Loading.svelte";
import Hotkeys from "../Base/Hotkeys";
import { Geocoding } from "../../Logic/Osm/Geocoding";
import { BBox } from "../../Logic/BBox";
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore";
Translations.t;
export let bounds: UIEventSource<BBox>
export let layout: LayoutConfig;
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
export let selectedElement: UIEventSource<Feature>;
export let selectedLayer: UIEventSource<LayerConfig>;
let searchContents: string = undefined;
let isRunning: boolean = false;
let inputElement: HTMLInputElement;
let feedback: string = undefined
Hotkeys.RegisterHotkey(
{ ctrl: "F" },
Translations.t.hotkeyDocumentation.selectSearch,
() => {
inputElement?.focus()
inputElement?.select()
}
)
async function performSearch() {
try {
isRunning = true;
searchContents = searchContents?.trim() ?? ""
if (searchContents === "") {
return
}
const result = await Geocoding.Search(searchContents, bounds.data)
if (result.length == 0) {
feedback = Translations.t.search.nothing.txt
return
}
const poi = result[0]
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(new BBox([[lon0, lat0], [lon1, lat1]]).pad(0.01))
const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer.values())
for (const layer of layers) {
const found = layer.features.data.find(f => f.properties.id === id)
selectedElement.setData(found)
selectedLayer.setData(layer.layer.layerDef)
}
}catch (e) {
console.error(e)
feedback = Translations.t.search.error.txt
} finally {
isRunning = false;
}
}
</script>
<div class="flex normal-background rounded-full pl-2">
<form>
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else if feedback !== undefined}
<div class="alert" on:click={() => feedback = undefined}>
{feedback}
</div>
{:else }
<input
bind:this={inputElement}
on:keypress={keypr => keypr.key === "Enter" ? performSearch() : undefined}
bind:value={searchContents}
placeholder={Translations.t.general.search.search}>
{/if}
</form>
<div class="w-6 h-6" on:click={performSearch}>
<ToSvelte construct={Svg.search_ui}></ToSvelte>
</div>
</div>

View file

@ -1,12 +1,6 @@
import Combine from "../Base/Combine"
import Toggle from "../Input/Toggle"
import MapControlButton from "../MapControlButton"
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"
import Svg from "../../Svg"
import MapState from "../../Logic/State/MapState"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import LevelSelector from "./LevelSelector"
import { GeolocationControl } from "./GeolocationControl"
export default class RightControls extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {

View file

@ -0,0 +1,75 @@
<script lang="ts">
import type { Feature } from "geojson";
import { Store, UIEventSource } from "../../Logic/UIEventSource";
import TagRenderingAnswer from "../Popup/TagRenderingAnswer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import ToSvelte from "../Base/ToSvelte.svelte";
import { VariableUiElement } from "../Base/VariableUIElement.js";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import { onDestroy } from "svelte";
export let selectedElement: UIEventSource<Feature>;
export let layer: UIEventSource<LayerConfig>;
export let tags: Store<UIEventSource<Record<string, string>>>;
let _tags: UIEventSource<Record<string, string>>;
onDestroy(tags.subscribe(tags => {
_tags = tags;
return false
}));
export let specialVisState: SpecialVisualizationState;
/**
* const title = new TagRenderingAnswer(
* tags,
* layerConfig.title ?? new TagRenderingConfig("POI"),
* state
* ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl")
* const titleIcons = new Combine(
* layerConfig.titleIcons.map((icon) => {
* return new TagRenderingAnswer(
* tags,
* icon,
* state,
* "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon"
* )
* })
* ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
*
* return new Combine([
* new Combine([title, titleIcons]).SetClass(
* "flex flex-col sm:flex-row flex-grow justify-between"
* ),
* ])
*/
</script>
<div>
<div on:click={() =>selectedElement.setData(undefined)}>close</div>
<div class="flex flex-col sm:flex-row flex-grow justify-between">
<!-- Title element-->
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, layer.data.title, specialVisState), [layer]))}></ToSvelte>
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
{#each $layer.titleIcons as titleIconConfig (titleIconConfig.id)}
<div class="w-8 h-8">
<ToSvelte
construct={() => new VariableUiElement(tags.mapD(tags => new TagRenderingAnswer(tags, titleIconConfig, specialVisState)))}></ToSvelte>
</div>
{/each}
</div>
</div>
<ul>
{#each Object.keys($_tags) as key}
<li><b>{key}</b>=<b>{$_tags[key]}</b></li>
{/each}
</ul>
</div>

View file

@ -4,20 +4,14 @@ import Title from "../Base/Title"
import TagRenderingChart from "./TagRenderingChart"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmFeature } from "../../Models/OsmFeature"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { FeatureSourceForLayer } from "../../Logic/FeatureSource/FeatureSource"
import BaseUIElement from "../BaseUIElement"
export default class StatisticsPanel extends VariableUiElement {
constructor(
elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>,
state: {
layoutToUse: LayoutConfig
}
) {
export default class StatisticsForLayerPanel extends VariableUiElement {
constructor(elementsInview: FeatureSourceForLayer) {
const layer = elementsInview.layer.layerDef
super(
elementsInview.stabilized(1000).map(
elementsInview.features.stabilized(1000).map(
(features) => {
if (features === undefined) {
return new Loading("Loading data")
@ -25,40 +19,33 @@ export default class StatisticsPanel extends VariableUiElement {
if (features.length === 0) {
return "No elements in view"
}
const els = []
for (const layer of state.layoutToUse.layers) {
if (layer.name === undefined) {
continue
}
const featuresForLayer = features
.filter((f) => f.layer === layer)
.map((f) => f.element)
if (featuresForLayer.length === 0) {
continue
}
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
const layerStats = []
for (const tagRendering of layer?.tagRenderings ?? []) {
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
chartclasses: "w-full",
chartstyle: "height: 60rem",
includeTitle: false,
})
const title = new Title(
tagRendering.question?.Clone() ?? tagRendering.id,
4
).SetClass("mt-8")
if (!chart.HasClass("hidden")) {
layerStats.push(
new Combine([title, chart]).SetClass(
"flex flex-col w-full lg:w-1/3"
)
)
}
}
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
const els: BaseUIElement[] = []
const featuresForLayer = features
if (featuresForLayer.length === 0) {
return
}
els.push(new Title(layer.name.Clone(), 1).SetClass("mt-8"))
const layerStats = []
for (const tagRendering of layer?.tagRenderings ?? []) {
const chart = new TagRenderingChart(featuresForLayer, tagRendering, {
chartclasses: "w-full",
chartstyle: "height: 60rem",
includeTitle: false,
})
const title = new Title(
tagRendering.question?.Clone() ?? tagRendering.id,
4
).SetClass("mt-8")
if (!chart.HasClass("hidden")) {
layerStats.push(
new Combine([title, chart]).SetClass(
"flex flex-col w-full lg:w-1/3"
)
)
}
}
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
return new Combine(els)
},
[Locale.language]

View file

@ -11,6 +11,7 @@ import LoggedInUserIndicator from "../LoggedInUserIndicator"
import { ActionButtons } from "./ActionButtons"
import { BBox } from "../../Logic/BBox"
import Loc from "../../Models/Loc"
import { DefaultGuiState } from "../DefaultGuiState"
export default class ThemeIntroductionPanel extends Combine {
constructor(
@ -24,6 +25,7 @@ export default class ThemeIntroductionPanel extends Combine {
osmConnection: OsmConnection
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
defaultGuiState: DefaultGuiState
},
guistate?: { userInfoIsOpened: UIEventSource<boolean> }
) {

View file

@ -17,7 +17,7 @@ export default class UploadTraceToOsmUI extends LoginToggle {
constructor(
trace: (title: string) => string,
state: {
layoutToUse: LayoutConfig
layout: LayoutConfig
osmConnection: OsmConnection
readonly featureSwitchUserbadge: Store<boolean>
},