Reformat all files with prettier

This commit is contained in:
Pieter Vander Vennet 2022-09-08 21:40:48 +02:00
parent e22d189376
commit b541d3eab4
382 changed files with 50893 additions and 35566 deletions

View file

@ -1,64 +1,75 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import FilteredLayer from "../../Models/FilteredLayer";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import Svg from "../../Svg";
import { UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import FilteredLayer from "../../Models/FilteredLayer"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import Svg from "../../Svg"
/**
* The icon with the 'plus'-sign and the preset icons spinning
*
*
*/
export default class AddNewMarker extends Combine {
constructor(filteredLayers: UIEventSource<FilteredLayer[]>) {
const icons = new VariableUiElement(filteredLayers.map(filteredLayers => {
const icons = []
let last = undefined;
for (const filteredLayer of filteredLayers) {
const layer = filteredLayer.layerDef;
if(layer.name === undefined && !filteredLayer.isDisplayed.data){
continue
}
for (const preset of filteredLayer.layerDef.presets) {
const tags = TagUtils.KVtoProperties(preset.tags)
const icon = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;");
icons.push(icon)
if (last === undefined) {
last = layer.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;");
const icons = new VariableUiElement(
filteredLayers.map((filteredLayers) => {
const icons = []
let last = undefined
for (const filteredLayer of filteredLayers) {
const layer = filteredLayer.layerDef
if (layer.name === undefined && !filteredLayer.isDisplayed.data) {
continue
}
for (const preset of filteredLayer.layerDef.presets) {
const tags = TagUtils.KVtoProperties(preset.tags)
const icon = layer.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
icons.push(icon)
if (last === undefined) {
last = layer.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.html.SetClass("block relative")
.SetStyle("width: 42px; height: 42px;")
}
}
}
}
if (icons.length === 0) {
return undefined
}
if (icons.length === 1) {
return icons[0]
}
icons.push(last)
const elem = new Combine(icons).SetClass("flex")
elem.SetClass("slide min-w-min").SetStyle("animation: slide " + icons.length + "s linear infinite;")
return elem;
}))
const label = Translations.t.general.add.addNewMapLabel.Clone()
.SetClass("block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap")
if (icons.length === 0) {
return undefined
}
if (icons.length === 1) {
return icons[0]
}
icons.push(last)
const elem = new Combine(icons).SetClass("flex")
elem.SetClass("slide min-w-min").SetStyle(
"animation: slide " + icons.length + "s linear infinite;"
)
return elem
})
)
const label = Translations.t.general.add.addNewMapLabel
.Clone()
.SetClass(
"block center absolute text-sm min-w-min pl-1 pr-1 bg-gray-400 rounded-3xl text-white opacity-65 whitespace-nowrap"
)
.SetStyle("top: 65px; transform: translateX(-50%)")
super([
new Combine([
Svg.add_pin_svg().SetClass("absolute").SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"),
Svg.add_pin_svg()
.SetClass("absolute")
.SetStyle("width: 50px; filter: drop-shadow(grey 0 0 10px"),
new Combine([icons])
.SetStyle("width: 50px")
.SetClass("absolute p-1 rounded-full overflow-hidden"),
Svg.addSmall_svg().SetClass("absolute animate-pulse").SetStyle("width: 30px; left: 30px; top: 35px;")
Svg.addSmall_svg()
.SetClass("absolute animate-pulse")
.SetStyle("width: 30px; left: 30px; top: 35px;"),
]).SetClass("absolute"),
new Combine([label]).SetStyle("position: absolute; left: 50%")
new Combine([label]).SetStyle("position: absolute; left: 50%"),
])
this.SetClass("block relative");
this.SetClass("block relative")
}
}
}

View file

@ -1,88 +1,86 @@
import Combine from "../Base/Combine";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import {DownloadPanel} from "./DownloadPanel";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import ExportPDF from "../ExportPDF";
import FilteredLayer from "../../Models/FilteredLayer";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {BBox} from "../../Logic/BBox";
import BaseLayer from "../../Models/BaseLayer";
import Loc from "../../Models/Loc";
import Combine from "../Base/Combine"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Translations from "../i18n/Translations"
import { UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
import { DownloadPanel } from "./DownloadPanel"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import ExportPDF from "../ExportPDF"
import FilteredLayer from "../../Models/FilteredLayer"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import BaseLayer from "../../Models/BaseLayer"
import Loc from "../../Models/Loc"
interface DownloadState {
interface DownloadState {
filteredLayers: UIEventSource<FilteredLayer[]>
featurePipeline: FeaturePipeline,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>,
backgroundLayer:UIEventSource<BaseLayer>,
locationControl: UIEventSource<Loc>,
featureSwitchExportAsPdf: UIEventSource<boolean>,
featureSwitchEnableExport: UIEventSource<boolean>,
featurePipeline: FeaturePipeline
layoutToUse: LayoutConfig
currentBounds: UIEventSource<BBox>
backgroundLayer: UIEventSource<BaseLayer>
locationControl: UIEventSource<Loc>
featureSwitchExportAsPdf: UIEventSource<boolean>
featureSwitchEnableExport: UIEventSource<boolean>
}
export default class AllDownloads extends ScrollableFullScreen {
constructor(isShown: UIEventSource<boolean>,state: {
filteredLayers: UIEventSource<FilteredLayer[]>
featurePipeline: FeaturePipeline,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>,
backgroundLayer:UIEventSource<BaseLayer>,
locationControl: UIEventSource<Loc>,
featureSwitchExportAsPdf: UIEventSource<boolean>,
featureSwitchEnableExport: UIEventSource<boolean>,
}) {
super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown);
constructor(
isShown: UIEventSource<boolean>,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>
featurePipeline: FeaturePipeline
layoutToUse: LayoutConfig
currentBounds: UIEventSource<BBox>
backgroundLayer: UIEventSource<BaseLayer>
locationControl: UIEventSource<Loc>
featureSwitchExportAsPdf: UIEventSource<boolean>
featureSwitchEnableExport: UIEventSource<boolean>
}
) {
super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown)
}
private static GenTitle(): BaseUIElement {
return Translations.t.general.download.title
.Clone()
.SetClass("text-2xl break-words font-bold p-2");
.SetClass("text-2xl break-words font-bold p-2")
}
private static GeneratePanel(state: DownloadState): BaseUIElement {
const isExporting = new UIEventSource(false, "Pdf-is-exporting")
const generatePdf = () => {
isExporting.setData(true)
new ExportPDF(
{
freeDivId: "belowmap",
background: state.backgroundLayer,
location: state.locationControl,
features: state.featurePipeline,
layout: state.layoutToUse,
}).isRunning.addCallbackAndRun(isRunning => isExporting.setData(isRunning))
new ExportPDF({
freeDivId: "belowmap",
background: state.backgroundLayer,
location: state.locationControl,
features: state.featurePipeline,
layout: state.layoutToUse,
}).isRunning.addCallbackAndRun((isRunning) => isExporting.setData(isRunning))
}
const loading = Svg.loading_svg().SetClass("animate-rotate");
const loading = Svg.loading_svg().SetClass("animate-rotate")
const dloadTrans = Translations.t.general.download
const icon = new Toggle(loading, Svg.floppy_ui(), isExporting);
const icon = new Toggle(loading, Svg.floppy_ui(), isExporting)
const text = new Toggle(
dloadTrans.exporting.Clone(),
new Combine([
dloadTrans.downloadAsPdf.Clone().SetClass("font-bold"),
dloadTrans.downloadAsPdfHelper.Clone()]
).SetClass("flex flex-col")
dloadTrans.downloadAsPdfHelper.Clone(),
])
.SetClass("flex flex-col")
.onClick(() => {
generatePdf()
}),
isExporting);
isExporting
)
const pdf = new Toggle(
new SubtleButton(
icon,
text),
new SubtleButton(icon, text),
undefined,
state.featureSwitchExportAsPdf
@ -94,6 +92,6 @@ export default class AllDownloads extends ScrollableFullScreen {
state.featureSwitchEnableExport
)
return new Combine([pdf, exportPanel]).SetClass("flex flex-col");
return new Combine([pdf, exportPanel]).SetClass("flex flex-col")
}
}

View file

@ -1,64 +1,85 @@
import Link from "../Base/Link";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import {UIEventSource} from "../../Logic/UIEventSource";
import UserDetails from "../../Logic/Osm/OsmConnection";
import Constants from "../../Models/Constants";
import Loc from "../../Models/Loc";
import {VariableUiElement} from "../Base/VariableUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {BBox} from "../../Logic/BBox";
import {Utils} from "../../Utils";
import Link from "../Base/Link"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { UIEventSource } from "../../Logic/UIEventSource"
import UserDetails from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import Loc from "../../Models/Loc"
import { VariableUiElement } from "../Base/VariableUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import { Utils } from "../../Utils"
/**
* The bottom right attribution panel in the leaflet map
*/
export default class Attribution extends Combine {
constructor(
location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>
) {
const mapComplete = new Link(
`Mapcomplete ${Constants.vNumber}`,
"https://github.com/pietervdvn/MapComplete",
true
)
const reportBug = new Link(
Svg.bug_ui().SetClass("small-image"),
"https://github.com/pietervdvn/MapComplete/issues",
true
)
constructor(location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>) {
const layoutId = layoutToUse?.id
const stats = new Link(
Svg.statistics_ui().SetClass("small-image"),
Utils.OsmChaLinkFor(31, layoutId),
true
)
const mapComplete = new Link(`Mapcomplete ${Constants.vNumber}`, 'https://github.com/pietervdvn/MapComplete', true);
const reportBug = new Link(Svg.bug_ui().SetClass("small-image"), "https://github.com/pietervdvn/MapComplete/issues", true);
const layoutId = layoutToUse?.id;
const stats = new Link(Svg.statistics_ui().SetClass("small-image"), Utils.OsmChaLinkFor(31, layoutId), true)
const idLink = location.map(location => `https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${location?.lat ?? 0}/${location?.lon ?? 0}`)
const idLink = location.map(
(location) =>
`https://www.openstreetmap.org/edit?editor=id#map=${location?.zoom ?? 0}/${
location?.lat ?? 0
}/${location?.lon ?? 0}`
)
const editHere = new Link(Svg.pencil_ui().SetClass("small-image"), idLink, true)
const mapillaryLink = location.map(location => `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`)
const mapillary = new Link(Svg.mapillary_black_ui().SetClass("small-image"), mapillaryLink, true);
const mapillaryLink = location.map(
(location) =>
`https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${
location?.lon ?? 0
}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
)
const mapillary = new Link(
Svg.mapillary_black_ui().SetClass("small-image"),
mapillaryLink,
true
)
let editWithJosm = new VariableUiElement(
userDetails.map(userDetails => {
userDetails.map(
(userDetails) => {
if (userDetails.csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
return undefined;
return undefined
}
const bounds: any = currentBounds.data;
const bounds: any = currentBounds.data
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth();
const bottom = bounds.getSouth();
const right = bounds.getEast();
const left = bounds.getWest();
const top = bounds.getNorth()
const bottom = bounds.getSouth()
const right = bounds.getEast()
const left = bounds.getWest()
const josmLink = `http://127.0.0.1:8111/load_and_zoom?left=${left}&right=${right}&top=${top}&bottom=${bottom}`
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true);
return new Link(Svg.josm_logo_ui().SetClass("small-image"), josmLink, true)
},
[location, currentBounds]
)
)
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary]);
super([mapComplete, reportBug, stats, editHere, editWithJosm, mapillary])
this.SetClass("flex")
}
}
}

View file

@ -1,19 +1,16 @@
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import BaseUIElement from "../BaseUIElement";
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
export default class BackToIndex extends SubtleButton {
constructor(message?: string | BaseUIElement) {
super(
Svg.back_svg().SetStyle("height: 1.5rem;"),
message ?? Translations.t.general.backToMapcomplete,
{
url: "index.html"
url: "index.html",
}
)
}
}
}

View file

@ -1,15 +1,14 @@
import Combine from "../Base/Combine";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import Svg from "../../Svg";
import Toggle from "../Input/Toggle";
import BaseLayer from "../../Models/BaseLayer";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import BaseUIElement from "../BaseUIElement";
import {GeoOperations} from "../../Logic/GeoOperations";
import Combine from "../Base/Combine"
import { UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import Svg from "../../Svg"
import Toggle from "../Input/Toggle"
import BaseLayer from "../../Models/BaseLayer"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import BaseUIElement from "../BaseUIElement"
import { GeoOperations } from "../../Logic/GeoOperations"
class SingleLayerSelectionButton extends Toggle {
public readonly activate: () => void
/**
@ -24,38 +23,39 @@ class SingleLayerSelectionButton extends Toggle {
constructor(
locationControl: UIEventSource<Loc>,
options: {
currentBackground: UIEventSource<BaseLayer>,
preferredType: string,
preferredLayer?: BaseLayer,
currentBackground: UIEventSource<BaseLayer>
preferredType: string
preferredLayer?: BaseLayer
notAvailable?: () => void
}) {
}
) {
const prefered = options.preferredType
const previousLayer = new UIEventSource(options.preferredLayer)
const unselected = SingleLayerSelectionButton.getIconFor(prefered)
.SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible")
const unselected = SingleLayerSelectionButton.getIconFor(prefered).SetClass(
"rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-invisible"
)
const selected = SingleLayerSelectionButton.getIconFor(prefered)
.SetClass("rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch")
const selected = SingleLayerSelectionButton.getIconFor(prefered).SetClass(
"rounded-lg p-1 h-12 w-12 overflow-hidden subtle-background border-attention-catch"
)
const available = AvailableBaseLayers
.SelectBestLayerAccordingTo(locationControl, new UIEventSource<string | string[]>(options.preferredType))
const available = AvailableBaseLayers.SelectBestLayerAccordingTo(
locationControl,
new UIEventSource<string | string[]>(options.preferredType)
)
let toggle: BaseUIElement = new Toggle(
selected,
unselected,
options.currentBackground.map(bg => bg.category === options.preferredType)
options.currentBackground.map((bg) => bg.category === options.preferredType)
)
super(
toggle,
undefined,
available.map(av => av.category === options.preferredType)
);
available.map((av) => av.category === options.preferredType)
)
/**
* Checks that the previous layer is still usable on the current location.
@ -85,27 +85,29 @@ class SingleLayerSelectionButton extends Toggle {
options.currentBackground.setData(previousLayer.data)
})
options.currentBackground.addCallbackAndRunD(background => {
options.currentBackground.addCallbackAndRunD((background) => {
if (background.category === options.preferredType) {
previousLayer.setData(background)
}
})
available.addCallbackD(availableLayer => {
available.addCallbackD((availableLayer) => {
// Called whenever a better layer is available
if (previousLayer.data === undefined) {
// PreviousLayer is unset -> we definitively weren't using this category -> no need to switch
return;
return
}
if (options.currentBackground.data?.id !== previousLayer.data?.id) {
// The previously used layer doesn't match the current layer -> no need to switch
return;
return
}
// 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)) {
if (
previousLayer.data.feature === null ||
GeoOperations.inside(locationControl.data, previousLayer.data.feature)
) {
return
}
@ -134,13 +136,12 @@ class SingleLayerSelectionButton extends Toggle {
// Fallback to OSM carto
options.currentBackground.setData(AvailableBaseLayers.osmCarto)
}
return;
return
}
previousLayer.setData(previousLayer.data ?? available.data)
options.currentBackground.setData(previousLayer.data)
}
}
private static getIconFor(type: string) {
@ -158,7 +159,6 @@ class SingleLayerSelectionButton extends Toggle {
}
export default class BackgroundMapSwitch extends Combine {
/**
* Three buttons to easily switch map layers between OSM, aerial and some map.
* @param state
@ -167,14 +167,13 @@ export default class BackgroundMapSwitch extends Combine {
*/
constructor(
state: {
locationControl: UIEventSource<Loc>,
locationControl: UIEventSource<Loc>
backgroundLayer: UIEventSource<BaseLayer>
},
currentBackground: UIEventSource<BaseLayer>,
options?:{
preferredCategory?: string,
options?: {
preferredCategory?: string
allowedCategories?: ("osmbasedmap" | "photo" | "map")[]
}
) {
const allowedCategories = options?.allowedCategories ?? ["osmbasedmap", "photo", "map"]
@ -188,14 +187,12 @@ export default class BackgroundMapSwitch extends Combine {
preferredLayer = previousLayer
}
const button = new SingleLayerSelectionButton(
state.locationControl,
{
preferredType: category,
preferredLayer: preferredLayer,
currentBackground: currentBackground,
notAvailable: activatePrevious
})
const button = new SingleLayerSelectionButton(state.locationControl, {
preferredType: category,
preferredLayer: preferredLayer,
currentBackground: currentBackground,
notAvailable: activatePrevious,
})
// Fall back to the first option: OSM
activatePrevious = activatePrevious ?? button.activate
if (category === options?.preferredCategory) {
@ -209,5 +206,4 @@ export default class BackgroundMapSwitch extends Combine {
super(buttons)
this.SetClass("flex")
}
}
}

View file

@ -1,41 +1,41 @@
import {DropDown} from "../Input/DropDown";
import Translations from "../i18n/Translations";
import State from "../../State";
import BaseLayer from "../../Models/BaseLayer";
import {VariableUiElement} from "../Base/VariableUIElement";
import {Store} from "../../Logic/UIEventSource";
import { DropDown } from "../Input/DropDown"
import Translations from "../i18n/Translations"
import State from "../../State"
import BaseLayer from "../../Models/BaseLayer"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Store } from "../../Logic/UIEventSource"
export default class BackgroundSelector extends VariableUiElement {
constructor(state: {availableBackgroundLayers?:Store<BaseLayer[]>} ) {
const available = state.availableBackgroundLayers?.map(available => {
if(available === undefined){
constructor(state: { availableBackgroundLayers?: Store<BaseLayer[]> }) {
const available = state.availableBackgroundLayers?.map((available) => {
if (available === undefined) {
return undefined
}
const baseLayers: { value: BaseLayer, shown: string }[] = [];
for (const i in available) {
if (!available.hasOwnProperty(i)) {
continue;
}
const layer: BaseLayer = available[i];
baseLayers.push({value: layer, shown: layer.name ?? "id:" + layer.id});
const baseLayers: { value: BaseLayer; shown: string }[] = []
for (const i in available) {
if (!available.hasOwnProperty(i)) {
continue
}
return baseLayers
const layer: BaseLayer = available[i]
baseLayers.push({ value: layer, shown: layer.name ?? "id:" + layer.id })
}
)
return baseLayers
})
super(
available?.map(baseLayers => {
if (baseLayers === undefined || baseLayers.length <= 1) {
return undefined;
}
return new DropDown(Translations.t.general.backgroundMap.Clone(), baseLayers, State.state.backgroundLayer, {
select_class: 'bg-indigo-100 p-1 rounded hover:bg-indigo-200 w-full'
})
available?.map((baseLayers) => {
if (baseLayers === undefined || baseLayers.length <= 1) {
return undefined
}
)
return new DropDown(
Translations.t.general.backgroundMap.Clone(),
baseLayers,
State.state.backgroundLayer,
{
select_class: "bg-indigo-100 p-1 rounded hover:bg-indigo-200 w-full",
}
)
})
)
}
}
}

View file

@ -1,109 +1,128 @@
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {FixedUiElement} from "../Base/FixedUiElement";
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 SmallLicense from "../../Models/smallLicense";
import {Utils} from "../../Utils";
import Link from "../Base/Link";
import {VariableUiElement} from "../Base/VariableUIElement";
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 BaseUIElement from "../BaseUIElement";
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 TranslatorsPanel from "./TranslatorsPanel";
import {MapillaryLink} from "./MapillaryLink";
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs";
import BaseUIElement from "../BaseUIElement"
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 TranslatorsPanel from "./TranslatorsPanel"
import { MapillaryLink } from "./MapillaryLink"
import FullWelcomePaneWithTabs from "./FullWelcomePaneWithTabs"
export class OpenIdEditor extends VariableUiElement {
constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string, objectId?: string) {
constructor(
state: { locationControl: UIEventSource<Loc> },
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]
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]
}
}
}
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=${
location?.zoom ?? 0
}/${location?.lat ?? 0}/${location?.lon ?? 0}`
return new SubtleButton(Svg.pencil_ui().SetStyle(iconStyle), t.editId, {
url: idLink,
newTab: true,
})
})
)
}
}
export class OpenJosm extends Combine {
constructor(state: { osmConnection: OsmConnection, currentBounds: Store<BBox>, }, iconStyle?: string) {
constructor(
state: { osmConnection: OsmConnection; currentBounds: Store<BBox> },
iconStyle?: string
) {
const t = Translations.t.general.attribution
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
josmState.stabilized(15000).addCallbackD(_ => josmState.setData(undefined))
josmState.stabilized(15000).addCallbackD((_) => josmState.setData(undefined))
const stateIndication = new VariableUiElement(josmState.map(state => {
if (state === undefined) {
return undefined
}
state = state.toUpperCase()
if (state === "OK") {
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
}));
const stateIndication = new VariableUiElement(
josmState.map((state) => {
if (state === undefined) {
return undefined
}
state = state.toUpperCase()
if (state === "OK") {
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
})
)
const toggle = new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data;
const bounds: any = state.currentBounds.data
if (bounds === undefined) {
return undefined
}
const top = bounds.getNorth();
const bottom = bounds.getSouth();
const right = bounds.getEast();
const left = bounds.getWest();
const top = bounds.getNorth()
const bottom = bounds.getSouth()
const right = bounds.getEast()
const left = bounds.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(ud => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible))
super([stateIndication, toggle]);
Utils.download(josmLink)
.then((answer) => josmState.setData(answer.replace(/\n/g, "").trim()))
.catch((_) => josmState.setData("ERROR"))
}),
undefined,
state.osmConnection.userDetails.map(
(ud) => ud.loggedIn && ud.csCount >= Constants.userJourney.historyLinkVisible
)
)
super([stateIndication, toggle])
}
}
/**
* The attribution panel shown on mobile
*/
export default class CopyrightPanel extends Combine {
private static LicenseObject = CopyrightPanel.GenerateLicenses();
private static LicenseObject = CopyrightPanel.GenerateLicenses()
constructor(state: {
layoutToUse: LayoutConfig,
featurePipeline: FeaturePipeline,
currentBounds: Store<BBox>,
locationControl: UIEventSource<Loc>,
osmConnection: OsmConnection,
layoutToUse: LayoutConfig
featurePipeline: FeaturePipeline
currentBounds: Store<BBox>
locationControl: UIEventSource<Loc>
osmConnection: OsmConnection
isTranslator: Store<boolean>
}) {
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const imgSize = "h-6 w-6"
@ -112,120 +131,134 @@ export default class CopyrightPanel extends Combine {
new SubtleButton(Svg.liberapay_ui(), t.donate, {
url: "https://liberapay.com/pietervdvn/",
newTab: true,
imgSize
imgSize,
}),
new SubtleButton(Svg.bug_ui(), t.openIssueTracker, {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
imgSize
imgSize,
}),
new SubtleButton(Svg.statistics_ui(), t.openOsmcha.Subs({theme: state.layoutToUse.title}), {
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
newTab: true,
imgSize
}),
new SubtleButton(Svg.mastodon_ui(),
new Combine([t.followOnMastodon.SetClass("font-bold"), t.followBridge]).SetClass("flex flex-col"),
new SubtleButton(
Svg.statistics_ui(),
t.openOsmcha.Subs({ theme: state.layoutToUse.title }),
{
url:"https://en.osm.town/web/notifications",
newTab: true,
imgSize
}),
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
newTab: true,
imgSize,
}
),
new SubtleButton(
Svg.mastodon_ui(),
new Combine([t.followOnMastodon.SetClass("font-bold"), t.followBridge]).SetClass(
"flex flex-col"
),
{
url: "https://en.osm.town/web/notifications",
newTab: true,
imgSize,
}
),
new SubtleButton(Svg.twitter_ui(), t.followOnTwitter, {
url:"https://twitter.com/mapcomplete",
url: "https://twitter.com/mapcomplete",
newTab: true,
imgSize
imgSize,
}),
new OpenIdEditor(state, iconStyle),
new MapillaryLink(state, iconStyle),
new OpenJosm(state, iconStyle),
new TranslatorsPanel(state, iconStyle)
new TranslatorsPanel(state, iconStyle),
]
const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution)
let maintainer: BaseUIElement = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {
maintainer = t.themeBy.Subs({author: layoutToUse.credits})
maintainer = t.themeBy.Subs({ author: layoutToUse.credits })
}
const contributions = new ContributorCount(state).Contributors
const dataContributors = new VariableUiElement(contributions.map(contributions => {
const dataContributors = new VariableUiElement(
contributions.map((contributions) => {
if (contributions === undefined) {
return ""
}
const sorted = Array.from(contributions, ([name, value]) => ({
name,
value
})).filter(x => x.name !== undefined && x.name !== "undefined");
value,
})).filter((x) => x.name !== undefined && x.name !== "undefined")
if (sorted.length === 0) {
return "";
return ""
}
sorted.sort((a, b) => b.value - a.value);
let hiddenCount = 0;
sorted.sort((a, b) => b.value - a.value)
let hiddenCount = 0
if (sorted.length > 10) {
hiddenCount = sorted.length - 10
sorted.splice(10, sorted.length - 10)
}
const links = sorted.map(kv => `<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>`)
const links = sorted.map(
(kv) =>
`<a href="https://openstreetmap.org/user/${kv.name}" target="_blank">${kv.name}</a>`
)
const contribs = links.join(", ")
if (hiddenCount <= 0) {
return t.mapContributionsBy.Subs({
contributors: contribs
contributors: contribs,
})
} else {
return t.mapContributionsByAndHidden.Subs({
contributors: contribs,
hiddenCount: hiddenCount
});
hiddenCount: hiddenCount,
})
}
})
)
}))
super([
new Title(t.attributionTitle),
t.attributionContent,
maintainer,
dataContributors,
CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy),
CopyrightPanel.CodeContributors(translators, t.translatedBy),
new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"),
new Combine(actionButtons).SetClass("block w-full link-no-underline"),
new Title(t.iconAttribution.title, 3),
...iconAttributions
].map(e => e?.SetClass("mt-4")));
super(
[
new Title(t.attributionTitle),
t.attributionContent,
maintainer,
dataContributors,
CopyrightPanel.CodeContributors(contributors, t.codeContributionsBy),
CopyrightPanel.CodeContributors(translators, t.translatedBy),
new FixedUiElement("MapComplete " + Constants.vNumber).SetClass("font-bold"),
new Combine(actionButtons).SetClass("block w-full link-no-underline"),
new Title(t.iconAttribution.title, 3),
...iconAttributions,
].map((e) => e?.SetClass("mt-4"))
)
this.SetClass("flex flex-col link-underline overflow-hidden")
this.SetStyle("max-width:100%; width: 40rem; margin-left: 0.75rem; margin-right: 0.5rem")
}
private static CodeContributors(contributors, translation: TypedTranslation<{contributors, hiddenCount}>): BaseUIElement {
const total = contributors.contributors.length;
private static CodeContributors(
contributors,
translation: TypedTranslation<{ contributors; hiddenCount }>
): BaseUIElement {
const total = contributors.contributors.length
let filtered = [...contributors.contributors]
filtered.splice(10, total - 10);
filtered.splice(10, total - 10)
let contribsStr = filtered.map(c => c.contributor).join(", ")
let contribsStr = filtered.map((c) => c.contributor).join(", ")
if (contribsStr === "") {
// Hmm, something went wrong loading the contributors list. Lets show nothing
return undefined;
return undefined
}
return translation.Subs({
contributors: contribsStr,
hiddenCount: total - 10
});
hiddenCount: total - 10,
})
}
private static IconAttribution(iconPath: string): BaseUIElement {
if (iconPath.startsWith("http")) {
try {
iconPath = "." + new URL(iconPath).pathname;
iconPath = "." + new URL(iconPath).pathname
} catch (e) {
console.warn(e)
}
@ -233,10 +266,10 @@ export default class CopyrightPanel extends Combine {
const license: SmallLicense = CopyrightPanel.LicenseObject[iconPath]
if (license == undefined) {
return undefined;
return undefined
}
if (license.license.indexOf("trivial") >= 0) {
return undefined;
return undefined
}
const sources = Utils.NoNull(Utils.NoEmpty(license.sources))
@ -246,25 +279,29 @@ export default class CopyrightPanel extends Combine {
new Combine([
new FixedUiElement(license.authors.join("; ")).SetClass("font-bold"),
license.license,
new Combine([...sources.map(lnk => {
let sourceLinkContent = lnk;
try {
sourceLinkContent = new URL(lnk).hostname
} catch {
console.error("Not a valid URL:", lnk)
}
return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2");
})]).SetClass("flex flex-wrap")
]).SetClass("flex flex-col").SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;")
new Combine([
...sources.map((lnk) => {
let sourceLinkContent = lnk
try {
sourceLinkContent = new URL(lnk).hostname
} catch {
console.error("Not a valid URL:", lnk)
}
return new Link(sourceLinkContent, lnk, true).SetClass("mr-2 mb-2")
}),
]).SetClass("flex flex-wrap"),
])
.SetClass("flex flex-col")
.SetStyle("width: calc(100% - 50px - 0.5em); min-width: 12rem;"),
]).SetClass("flex flex-wrap border-b border-gray-300 m-2 border-box")
}
private static GenerateLicenses() {
const allLicenses = {}
for (const key in licenses) {
const license: SmallLicense = licenses[key];
const license: SmallLicense = licenses[key]
allLicenses[license.path] = license
}
return allLicenses;
return allLicenses
}
}
}

View file

@ -1,98 +1,105 @@
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";
import {GeoOperations} from "../../Logic/GeoOperations";
import Toggle from "../Input/Toggle";
import Title from "../Base/Title";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {UIEventSource} from "../../Logic/UIEventSource";
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {BBox} from "../../Logic/BBox";
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
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"
import { GeoOperations } from "../../Logic/GeoOperations"
import Toggle from "../Input/Toggle"
import Title from "../Base/Title"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { UIEventSource } from "../../Logic/UIEventSource"
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { BBox } from "../../Logic/BBox"
import FilteredLayer, { FilterState } from "../../Models/FilteredLayer"
import geojson2svg from "geojson2svg"
import Constants from "../../Models/Constants";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import Constants from "../../Models/Constants"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
export class DownloadPanel extends Toggle {
constructor(state: {
filteredLayers: UIEventSource<FilteredLayer[]>
featurePipeline: FeaturePipeline,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>,
featurePipeline: FeaturePipeline
layoutToUse: LayoutConfig
currentBounds: UIEventSource<BBox>
}) {
const t = Translations.t.general.download
const name = State.state.layoutToUse.id;
const name = State.state.layoutToUse.id
const includeMetaToggle = new CheckBoxes([t.includeMetaData])
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0)
const buttonGeoJson = new SubtleButton(
Svg.floppy_ui(),
new Combine([
t.downloadGeojson.SetClass("font-bold"),
t.downloadGeoJsonHelper,
]).SetClass("flex flex-col")
).OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
Utils.offerContentsAsDownloadableFile(
JSON.stringify(geojson, null, " "),
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`,
{
mimetype: "application/vnd.geo+json",
}
)
})
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
new Combine([t.downloadGeojson.SetClass("font-bold"),
t.downloadGeoJsonHelper]).SetClass("flex flex-col"))
.OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
mimetype: "application/vnd.geo+json"
});
const buttonCSV = new SubtleButton(
Svg.floppy_ui(),
new Combine([t.downloadCSV.SetClass("font-bold"), t.downloadCSVHelper]).SetClass(
"flex flex-col"
)
).OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
const csv = GeoOperations.toCSV(geojson.features)
Utils.offerContentsAsDownloadableFile(
csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`,
{
mimetype: "text/csv",
}
)
})
const buttonSvg = new SubtleButton(
Svg.floppy_ui(),
new Combine([t.downloadAsSvg.SetClass("font-bold"), t.downloadAsSvgHelper]).SetClass(
"flex flex-col"
)
).OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data)
const leafletdiv = document.getElementById("leafletDiv")
const csv = DownloadPanel.asSvg(geojson, {
layers: state.filteredLayers.data.map((l) => l.layerDef),
mapExtent: state.currentBounds.data,
width: leafletdiv.offsetWidth,
height: leafletdiv.offsetHeight,
})
Utils.offerContentsAsDownloadableFile(
csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`,
{
mimetype: "image/svg+xml",
}
)
})
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadCSV.SetClass("font-bold"),
t.downloadCSVHelper]).SetClass("flex flex-col"))
.OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
const csv = GeoOperations.toCSV(geojson.features)
const downloadButtons = new Combine([
new Title(t.title),
buttonGeoJson,
buttonCSV,
buttonSvg,
includeMetaToggle,
t.licenseInfo.SetClass("link-underline"),
]).SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
Utils.offerContentsAsDownloadableFile(csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, {
mimetype: "text/csv"
});
})
const buttonSvg = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadAsSvg.SetClass("font-bold"),
t.downloadAsSvgHelper]).SetClass("flex flex-col"))
.OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data)
const leafletdiv = document.getElementById("leafletDiv")
const csv = DownloadPanel.asSvg(geojson,
{
layers: state.filteredLayers.data.map(l => l.layerDef),
mapExtent: state.currentBounds.data,
width: leafletdiv.offsetWidth,
height: leafletdiv.offsetHeight
})
Utils.offerContentsAsDownloadableFile(csv,
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, {
mimetype: "image/svg+xml"
});
})
const downloadButtons = new Combine(
[new Title(t.title),
buttonGeoJson,
buttonCSV,
buttonSvg,
includeMetaToggle,
t.licenseInfo.SetClass("link-underline")])
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
super(
downloadButtons,
t.noDataLoaded,
state.featurePipeline.somethingLoaded)
super(downloadButtons, t.noDataLoaded, state.featurePipeline.somethingLoaded)
}
/**
@ -112,20 +119,21 @@ export class DownloadPanel extends Toggle {
* const perLayer = new Map<string, any[]>([["testlayer", [feature]]])
* DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => `<svg width="1000px" height="1000px" viewBox="0 0 1000 1000"> <g id="testlayer" inkscape:groupmode="layer" inkscape:label="testlayer"> <path d="M0,27.77777777777778 1000,472.22222222222223" style="fill:none;stroke-width:1" stroke="#ff0000"/> </g></svg>`
*/
public static asSvg(perLayer: Map<string, any[]>,
options?:
{
layers?: LayerConfig[],
width?: 1000 | number,
height?: 1000 | number,
mapExtent?: BBox
unit?: "px" | "mm" | string
}) {
public static asSvg(
perLayer: Map<string, any[]>,
options?: {
layers?: LayerConfig[]
width?: 1000 | number
height?: 1000 | number
mapExtent?: BBox
unit?: "px" | "mm" | string
}
) {
options = options ?? {}
const w = options.width ?? 1000
const h = options.height ?? 1000
const unit = options.unit ?? "px"
const mapExtent = {left: -180, bottom: -90, right: 180, top: 90}
const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 }
if (options.mapExtent !== undefined) {
const bbox = options.mapExtent
mapExtent.left = bbox.minLon
@ -134,51 +142,50 @@ export class DownloadPanel extends Toggle {
mapExtent.top = bbox.maxLat
}
const elements: string [] = []
const elements: string[] = []
for (const layer of Array.from(perLayer.keys())) {
const features = perLayer.get(layer)
if(features.length === 0){
if (features.length === 0) {
continue
}
const layerDef = options?.layers?.find(l => l.id === layer)
const layerDef = options?.layers?.find((l) => l.id === layer)
const rendering = layerDef?.lineRendering[0]
const converter = geojson2svg({
viewportSize: {width: w, height: h},
const converter = geojson2svg({
viewportSize: { width: w, height: h },
mapExtent,
output: 'svg',
attributes:[
output: "svg",
attributes: [
{
property: "style",
type:'static',
value: "fill:none;stroke-width:1"
type: "static",
value: "fill:none;stroke-width:1",
},
{
property: 'properties.stroke',
type:'dynamic',
key: 'stroke'
}
]
});
property: "properties.stroke",
type: "dynamic",
key: "stroke",
},
],
})
for (const feature of features) {
const stroke = rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000"
const color = Utils.colorAsHex( Utils.color(stroke))
const stroke =
rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000"
const color = Utils.colorAsHex(Utils.color(stroke))
feature.properties.stroke = color
}
const groupPaths: string[] = converter.convert({type: "FeatureCollection", features})
const group = ` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` +
groupPaths.map(p => " " + p).join("\n")
+ "\n </g>"
const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features })
const group =
` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` +
groupPaths.map((p) => " " + p).join("\n") +
"\n </g>"
elements.push(group)
}
const header = `<svg width="${w}${unit}" height="${h}${unit}" viewBox="0 0 ${w} ${h}">`
return header + "\n" + elements.join("\n") + "\n</svg>"
}
@ -189,48 +196,53 @@ export class DownloadPanel extends Toggle {
* @param includeMetaData
* @private
*/
private static getCleanGeoJson(state: {
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>,
filteredLayers: UIEventSource<FilteredLayer[]>
}, includeMetaData: boolean) {
private static getCleanGeoJson(
state: {
featurePipeline: FeaturePipeline
currentBounds: UIEventSource<BBox>
filteredLayers: UIEventSource<FilteredLayer[]>
},
includeMetaData: boolean
) {
const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData)
const features = [].concat(...Array.from(perLayer.values()))
return {
type: "FeatureCollection",
features
features,
}
}
private static getCleanGeoJsonPerLayer(state: {
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>,
filteredLayers: UIEventSource<FilteredLayer[]>
}, includeMetaData: boolean): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ {
const perLayer = new Map<string, any[]>();
const neededLayers = state.filteredLayers.data.map(l => l.layerDef.id)
private static getCleanGeoJsonPerLayer(
state: {
featurePipeline: FeaturePipeline
currentBounds: UIEventSource<BBox>
filteredLayers: UIEventSource<FilteredLayer[]>
},
includeMetaData: boolean
): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ {
const perLayer = new Map<string, any[]>()
const neededLayers = state.filteredLayers.data.map((l) => l.layerDef.id)
const bbox = state.currentBounds.data
const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox, new Set(neededLayers));
outer : for (const tile of featureList) {
if(Constants.priviliged_layers.indexOf(tile.layer) >= 0){
const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(
bbox,
new Set(neededLayers)
)
outer: for (const tile of featureList) {
if (Constants.priviliged_layers.indexOf(tile.layer) >= 0) {
continue
}
const layer = state.filteredLayers.data.find(fl => fl.layerDef.id === tile.layer)
const layer = state.filteredLayers.data.find((fl) => fl.layerDef.id === tile.layer)
if (!perLayer.has(tile.layer)) {
perLayer.set(tile.layer, [])
}
const featureList = perLayer.get(tile.layer)
const filters = layer.appliedFilters.data
for (const feature of tile.features) {
if (!bbox.overlapsWith(BBox.get(feature))) {
continue
}
if (filters !== undefined) {
for (let key of Array.from(filters.keys())) {
const filter: FilterState = filters.get(key)
@ -238,21 +250,21 @@ export class DownloadPanel extends Toggle {
continue
}
if (!filter.currentFilter.matchesProperties(feature.properties)) {
continue outer;
continue outer
}
}
}
const cleaned = {
type: feature.type,
geometry: {...feature.geometry},
properties: {...feature.properties}
geometry: { ...feature.geometry },
properties: { ...feature.properties },
}
if (!includeMetaData) {
for (const key in cleaned.properties) {
if (key === "_lon" || key === "_lat") {
continue;
continue
}
if (key.startsWith("_")) {
delete feature.properties[key]
@ -260,7 +272,11 @@ export class DownloadPanel extends Toggle {
}
}
const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys))
const datedKeys = [].concat(
SimpleMetaTagger.metatags
.filter((tagging) => tagging.includesDates)
.map((tagging) => tagging.keys)
)
for (const key of datedKeys) {
delete feature.properties[key]
}
@ -270,6 +286,5 @@ export class DownloadPanel extends Toggle {
}
return perLayer
}
}
}

View file

@ -1,37 +1,44 @@
import {UIElement} from "../UIElement";
import BaseUIElement from "../BaseUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig";
import Img from "../Base/Img";
import {SubtleButton} from "../Base/SubtleButton";
import Toggle from "../Input/Toggle";
import Loc from "../../Models/Loc";
import Locale from "../i18n/Locale";
import {Utils} from "../../Utils";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import {Translation} from "../i18n/Translation";
import { UIElement } from "../UIElement"
import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
import Img from "../Base/Img"
import { SubtleButton } from "../Base/SubtleButton"
import Toggle from "../Input/Toggle"
import Loc from "../../Models/Loc"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
export default class ExtraLinkButton extends UIElement {
private readonly _config: ExtraLinkConfig;
private readonly _config: ExtraLinkConfig
private readonly state: {
layoutToUse: { id: string, title: Translation };
featureSwitchWelcomeMessage: UIEventSource<boolean>, locationControl: UIEventSource<Loc>
};
layoutToUse: { id: string; title: Translation }
featureSwitchWelcomeMessage: UIEventSource<boolean>
locationControl: UIEventSource<Loc>
}
constructor(state: { featureSwitchWelcomeMessage: UIEventSource<boolean>, locationControl: UIEventSource<Loc>, layoutToUse: { id: string, title: Translation } },
config: ExtraLinkConfig) {
super();
this.state = state;
this._config = config;
constructor(
state: {
featureSwitchWelcomeMessage: UIEventSource<boolean>
locationControl: UIEventSource<Loc>
layoutToUse: { id: string; title: Translation }
},
config: ExtraLinkConfig
) {
super()
this.state = state
this._config = config
}
protected InnerRender(): BaseUIElement {
if (this._config === undefined) {
return undefined;
return undefined
}
const c = this._config;
const c = this._config
const isIframe = window !== window.top
@ -46,17 +53,16 @@ export default class ExtraLinkButton extends UIElement {
let link: BaseUIElement
const theme = this.state.layoutToUse?.id ?? ""
const basepath = window.location.host
const href = this.state.locationControl.map(loc => {
const href = this.state.locationControl.map((loc) => {
const subs = {
...loc,
theme: theme,
basepath,
language: Locale.language.data
language: Locale.language.data,
}
return Utils.SubstituteKeys(c.href, subs)
})
let img: BaseUIElement = Svg.pop_out_ui()
if (c.icon !== undefined) {
img = new Img(c.icon).SetClass("h-6")
@ -64,14 +70,16 @@ export default class ExtraLinkButton extends UIElement {
let text: Translation
if (c.text === undefined) {
text = Translations.t.general.screenToSmall.Subs({theme: this.state.layoutToUse.title})
text = Translations.t.general.screenToSmall.Subs({
theme: this.state.layoutToUse.title,
})
} else {
text = c.text.Clone()
}
link = new SubtleButton(img, text, {
url: href,
newTab: c.newTab
newTab: c.newTab,
})
if (c.requirements.has("no-welcome-message")) {
@ -82,7 +90,6 @@ export default class ExtraLinkButton extends UIElement {
link = new Toggle(link, undefined, this.state.featureSwitchWelcomeMessage)
}
return link;
return link
}
}
}

View file

@ -1,18 +1,16 @@
import Combine from "../Base/Combine";
import Combine from "../Base/Combine"
import * as welcome_messages from "../../assets/welcome_message.json"
import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import MoreScreen from "./MoreScreen";
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import MoreScreen from "./MoreScreen"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
export default class FeaturedMessage extends Combine {
constructor() {
const now = new Date()
let welcome_message = undefined;
let welcome_message = undefined
for (const wm of FeaturedMessage.WelcomeMessages()) {
if (wm.start_date >= now) {
continue
@ -24,19 +22,29 @@ export default class FeaturedMessage extends Combine {
if (welcome_message !== undefined) {
console.warn("Multiple applicable messages today:", welcome_message.featured_theme)
}
welcome_message = wm;
welcome_message = wm
}
welcome_message = welcome_message ?? undefined
super([FeaturedMessage.CreateFeaturedBox(welcome_message)]);
super([FeaturedMessage.CreateFeaturedBox(welcome_message)])
}
public static WelcomeMessages(): { start_date: Date, end_date: Date, message: string, featured_theme?: string }[] {
const all_messages: { start_date: Date, end_date: Date, message: string, featured_theme?: string }[] = []
public static WelcomeMessages(): {
start_date: Date
end_date: Date
message: string
featured_theme?: string
}[] {
const all_messages: {
start_date: Date
end_date: Date
message: string
featured_theme?: string
}[] = []
const themesById = new Map<string, { id: string, title: any, shortDescription: any }>();
const themesById = new Map<string, { id: string; title: any; shortDescription: any }>()
for (const theme of themeOverview["default"]) {
themesById.set(theme.id, theme);
themesById.set(theme.id, theme)
}
for (const i in welcome_messages) {
@ -62,32 +70,36 @@ export default class FeaturedMessage extends Combine {
start_date: new Date(wm.start_date),
end_date: new Date(wm.end_date),
message: wm.message,
featured_theme: wm.featured_theme
featured_theme: wm.featured_theme,
})
}
return all_messages
}
public static CreateFeaturedBox(welcome_message: { message: string, featured_theme?: string }): BaseUIElement {
public static CreateFeaturedBox(welcome_message: {
message: string
featured_theme?: string
}): BaseUIElement {
const els: BaseUIElement[] = []
if (welcome_message === undefined) {
return undefined;
return undefined
}
const title = new Title(Translations.t.index.featuredThemeTitle.Clone())
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["default"].filter(th => th.id === welcome_message.featured_theme)[0];
els.push(MoreScreen.createLinkButton({}, theme)
.SetClass("m-4 self-center md:w-160")
.SetStyle("height: min-content;"))
els.push(
MoreScreen.createLinkButton({}, theme)
.SetClass("m-4 self-center md:w-160")
.SetStyle("height: min-content;")
)
}
return new Combine(els).SetClass("border-2 border-grey-400 rounded-xl flex flex-col md:flex-row");
return new Combine(els).SetClass(
"border-2 border-grey-400 rounded-xl flex flex-col md:flex-row"
)
}
}
}

View file

@ -1,37 +1,39 @@
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 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 FilterConfig from "../../Models/ThemeConfig/FilterConfig";
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
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 { 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 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 FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
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"
export default class FilterView extends VariableUiElement {
constructor(filteredLayer: Store<FilteredLayer[]>,
tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[],
state: {
availableBackgroundLayers?: Store<BaseLayer[]>,
featureSwitchBackgroundSelection?: UIEventSource<boolean>,
featureSwitchIsDebugging?: UIEventSource<boolean>,
locationControl?: UIEventSource<Loc>
}) {
constructor(
filteredLayer: Store<FilteredLayer[]>,
tileLayers: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }[],
state: {
availableBackgroundLayers?: Store<BaseLayer[]>
featureSwitchBackgroundSelection?: UIEventSource<boolean>
featureSwitchIsDebugging?: UIEventSource<boolean>
locationControl?: UIEventSource<Loc>
}
) {
const backgroundSelector = new Toggle(
new BackgroundSelector(state),
undefined,
@ -39,147 +41,143 @@ export default class FilterView extends VariableUiElement {
)
super(
filteredLayer.map((filteredLayers) => {
// Create the views which toggle layers (and filters them) ...
let elements = filteredLayers
?.map(l => FilterView.createOneFilteredLayerElement(l, state)?.SetClass("filter-panel"))
?.filter(l => l !== undefined)
elements[0].SetClass("first-filter-panel")
// Create the views which toggle layers (and filters them) ...
let elements = filteredLayers
?.map((l) =>
FilterView.createOneFilteredLayerElement(l, state)?.SetClass("filter-panel")
)
?.filter((l) => l !== undefined)
elements[0].SetClass("first-filter-panel")
// ... create views for non-interactive layers ...
elements = elements.concat(tileLayers.map(tl => FilterView.createOverlayToggle(state, tl)))
// ... and add the dropdown to select a different background
return elements.concat(backgroundSelector);
}
)
);
// ... create views for non-interactive layers ...
elements = elements.concat(
tileLayers.map((tl) => FilterView.createOverlayToggle(state, tl))
)
// ... and add the dropdown to select a different background
return elements.concat(backgroundSelector)
})
)
}
private static createOverlayToggle(state: { locationControl?: UIEventSource<Loc> }, config: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }) {
private static createOverlayToggle(
state: { locationControl?: UIEventSource<Loc> },
config: { config: TilesourceConfig; isDisplayed: UIEventSource<boolean> }
) {
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;"
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;";
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle)
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle)
const name: Translation = config.config.name
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle);
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(
iconStyle
);
const name: Translation = config.config.name;
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2")
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-2");
const zoomStatus = new Toggle(
undefined,
Translations.t.general.layerSelection.zoomInToSeeThisLayer
.SetClass("alert")
.SetStyle("display: block ruby;width:min-content;"),
state.locationControl?.map((location) => location.zoom >= config.config.minzoom) ??
new ImmutableStore(false)
)
const zoomStatus =
new Toggle(
undefined,
Translations.t.general.layerSelection.zoomInToSeeThisLayer
.SetClass("alert")
.SetStyle("display: block ruby;width:min-content;"),
state.locationControl?.map(location => location.zoom >= config.config.minzoom) ?? new ImmutableStore(false)
)
const style =
"display:flex;align-items:center;padding:0.5rem 0;";
const style = "display:flex;align-items:center;padding:0.5rem 0;"
const layerChecked = new Combine([icon, styledNameChecked, zoomStatus])
.SetStyle(style)
.onClick(() => config.isDisplayed.setData(false));
.onClick(() => config.isDisplayed.setData(false))
const layerNotChecked = new Combine([iconUnselected, styledNameUnChecked])
.SetStyle(style)
.onClick(() => config.isDisplayed.setData(true));
.onClick(() => config.isDisplayed.setData(true))
return new Toggle(
layerChecked,
layerNotChecked,
config.isDisplayed
);
return new Toggle(layerChecked, layerNotChecked, config.isDisplayed)
}
private static createOneFilteredLayerElement(filteredLayer: FilteredLayer,
state: { featureSwitchIsDebugging?: Store<boolean>, locationControl?: Store<Loc> }) {
private static createOneFilteredLayerElement(
filteredLayer: FilteredLayer,
state: { featureSwitchIsDebugging?: Store<boolean>; locationControl?: Store<Loc> }
) {
if (filteredLayer.layerDef.name === undefined) {
// Name is not defined: we hide this one
return new Toggle(
new FixedUiElement(filteredLayer?.layerDef?.id).SetClass("block"),
undefined,
state?.featureSwitchIsDebugging ?? new ImmutableStore(false)
);
)
}
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;";
const iconStyle = "width:1.5rem;height:1.5rem;margin-left:1.25rem;flex-shrink: 0;"
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle);
const icon = new Combine([Svg.checkbox_filled]).SetStyle(iconStyle)
const layer = filteredLayer.layerDef
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(
iconStyle
);
const iconUnselected = new Combine([Svg.checkbox_empty]).SetStyle(iconStyle)
const name: Translation = filteredLayer.layerDef.name.Clone()
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3");
const styledNameChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3")
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3");
const styledNameUnChecked = name.Clone().SetStyle("font-size:large").SetClass("ml-3")
const zoomStatus =
new Toggle(
undefined,
Translations.t.general.layerSelection.zoomInToSeeThisLayer
.SetClass("alert")
.SetStyle("display: block ruby;width:min-content;"),
state?.locationControl?.map(location => location.zoom >= filteredLayer.layerDef.minzoom) ?? new ImmutableStore(false)
)
const zoomStatus = new Toggle(
undefined,
Translations.t.general.layerSelection.zoomInToSeeThisLayer
.SetClass("alert")
.SetStyle("display: block ruby;width:min-content;"),
state?.locationControl?.map(
(location) => location.zoom >= filteredLayer.layerDef.minzoom
) ?? new ImmutableStore(false)
)
const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0";
const toggleClasses = "layer-toggle flex flex-wrap items-center pt-2 pb-2 px-0"
const layerIcon = layer.defaultIcon()?.SetClass("flex-shrink-0 w-8 h-8 ml-2")
const layerIconUnchecked = layer.defaultIcon()?.SetClass("flex-shrink-0 opacity-50 w-8 h-8 ml-2")
const layerIconUnchecked = layer
.defaultIcon()
?.SetClass("flex-shrink-0 opacity-50 w-8 h-8 ml-2")
const layerChecked = new Combine([icon, layerIcon, styledNameChecked, zoomStatus])
.SetClass(toggleClasses)
.onClick(() => filteredLayer.isDisplayed.setData(false));
.onClick(() => filteredLayer.isDisplayed.setData(false))
const layerNotChecked = new Combine([iconUnselected, layerIconUnchecked, styledNameUnChecked])
const layerNotChecked = new Combine([
iconUnselected,
layerIconUnchecked,
styledNameUnChecked,
])
.SetClass(toggleClasses)
.onClick(() => filteredLayer.isDisplayed.setData(true));
.onClick(() => filteredLayer.isDisplayed.setData(true))
const filterPanel: BaseUIElement = new LayerFilterPanel(state, filteredLayer)
return new Toggle(
new Combine([layerChecked, filterPanel]),
layerNotChecked,
filteredLayer.isDisplayed
);
)
}
}
export class LayerFilterPanel extends Combine {
public constructor(state: any, flayer: FilteredLayer) {
const layer = flayer.layerDef
if (layer.filters.length === 0) {
super([])
return undefined;
return undefined
}
const toShow: BaseUIElement [] = []
const toShow: BaseUIElement[] = []
for (const filter of layer.filters) {
const [ui, actualTags] = LayerFilterPanel.createFilter(state, filter)
ui.SetClass("mt-1")
toShow.push(ui)
actualTags.addCallbackAndRun(tagsToFilterFor => {
actualTags.addCallbackAndRun((tagsToFilterFor) => {
flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
flayer.appliedFilters.ping()
})
flayer.appliedFilters.map(dict => dict.get(filter.id))
.addCallbackAndRun(filters => actualTags.setData(filters))
flayer.appliedFilters
.map((dict) => dict.get(filter.id))
.addCallbackAndRun((filters) => actualTags.setData(filters))
}
super(toShow)
@ -187,37 +185,53 @@ export class LayerFilterPanel extends Combine {
}
// Filter which uses one or more textfields
private static createFilterWithFields(state: any, filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
private static createFilterWithFields(
state: any,
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
const filter = filterConfig.options[0]
const mappings = new Map<string, BaseUIElement>()
let allValid: Store<boolean> = new ImmutableStore(true)
var allFields: InputElement<string>[] = []
const properties = new UIEventSource<any>({})
for (const {name, type} of filter.fields) {
const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id)
const field = ValidatedTextField.ForType(type).ConstructInputElement({
value
}).SetClass("inline-block")
for (const { name, type } of filter.fields) {
const value = QueryParameters.GetQueryParameter(
"filter-" + filterConfig.id + "-" + name,
"",
"Value for filter " + filterConfig.id
)
const field = ValidatedTextField.ForType(type)
.ConstructInputElement({
value,
})
.SetClass("inline-block")
mappings.set(name, field)
const stable = value.stabilized(250)
stable.addCallbackAndRunD(v => {
properties.data[name] = v.toLowerCase();
stable.addCallbackAndRunD((v) => {
properties.data[name] = v.toLowerCase()
properties.ping()
})
allFields.push(field)
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
allValid = allValid.map(
(previous) => previous && field.IsValid(stable.data) && stable.data !== "",
[stable]
)
}
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), state, mappings)
const trigger: Store<FilterState> = allValid.map(isValid => {
if (!isValid) {
return undefined
}
const props = properties.data
// Replace all the field occurences in the tags...
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec,
v => {
const tr = new SubstitutedTranslation(
filter.question,
new UIEventSource<any>({ id: filterConfig.id }),
state,
mappings
)
const trigger: Store<FilterState> = allValid.map(
(isValid) => {
if (!isValid) {
return undefined
}
const props = properties.data
// Replace all the field occurences in the tags...
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, (v) => {
if (typeof v !== "string") {
return v
}
@ -227,57 +241,72 @@ export class LayerFilterPanel extends Combine {
}
return v
})
const tagsFilter = TagUtils.Tag(tagsSpec)
return {
currentFilter: tagsFilter,
state: JSON.stringify(props),
}
)
const tagsFilter = TagUtils.Tag(tagsSpec)
return {
currentFilter: tagsFilter,
state: JSON.stringify(props)
}
}, [properties])
},
[properties]
)
const settableFilter = new UIEventSource<FilterState>(undefined)
trigger.addCallbackAndRun(state => settableFilter.setData(state))
settableFilter.addCallback(state => {
trigger.addCallbackAndRun((state) => settableFilter.setData(state))
settableFilter.addCallback((state) => {
if (state === undefined) {
// still initializing
return
}
if (state.currentFilter === undefined) {
allFields.forEach(f => f.GetValue().setData(undefined));
allFields.forEach((f) => f.GetValue().setData(undefined))
}
})
return [tr, settableFilter];
return [tr, settableFilter]
}
private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
let option = filterConfig.options[0];
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 icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6")
const iconUnselected = Svg.checkbox_empty_svg().SetClass("block mr-2 w-6")
const toggle = new ClickableToggle(
new Combine([icon, option.question.Clone().SetClass("block")]).SetClass("flex"),
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass("flex")
new Combine([iconUnselected, option.question.Clone().SetClass("block")]).SetClass(
"flex"
)
)
.ToggleOnClick()
.SetClass("block m-1")
return [toggle, toggle.isEnabled.sync(enabled => enabled ? {
currentFilter: option.osmTags,
state: "true"
} : undefined, [],
f => f !== undefined)
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;
private static createMultiFilter(
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
let options = filterConfig.options
const values: FilterState[] = options.map((f, i) => ({
currentFilter: f.osmTags, state: i
currentFilter: f.osmTags,
state: i,
}))
let filterPicker: InputElement<number>
@ -288,36 +317,43 @@ export class LayerFilterPanel extends Combine {
new FixedInputElement(option.question.Clone().SetClass("block"), i)
),
{
dontStyle: true
dontStyle: true,
}
);
)
} else {
filterPicker = new DropDown("", options.map((option, i) => ({
value: i, shown: option.question.Clone()
})))
filterPicker = new DropDown(
"",
options.map((option, i) => ({
value: i,
shown: option.question.Clone(),
}))
)
}
return [filterPicker,
return [
filterPicker,
filterPicker.GetValue().sync(
i => values[i],
(i) => values[i],
[],
selected => {
(selected) => {
const v = selected?.state
if (v === undefined || typeof v === "string") {
return undefined
}
return v
}
)]
),
]
}
private static createFilter(state: {}, filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
private static createFilter(
state: {},
filterConfig: FilterConfig
): [BaseUIElement, UIEventSource<FilterState>] {
if (filterConfig.options[0].fields.length > 0) {
return LayerFilterPanel.createFilterWithFields(state, filterConfig)
}
if (filterConfig.options.length === 1) {
return LayerFilterPanel.createCheckboxFilter(filterConfig)
}

View file

@ -1,43 +1,44 @@
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
import ShareScreen from "./ShareScreen";
import MoreScreen from "./MoreScreen";
import Constants from "../../Models/Constants";
import Combine from "../Base/Combine";
import {TabbedComponent} from "../Base/TabbedComponent";
import {UIEventSource} from "../../Logic/UIEventSource";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Loc from "../../Models/Loc";
import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
import CopyrightPanel from "./CopyrightPanel";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import PrivacyPolicy from "./PrivacyPolicy";
import ThemeIntroductionPanel from "./ThemeIntroductionPanel"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import ShareScreen from "./ShareScreen"
import MoreScreen from "./MoreScreen"
import Constants from "../../Models/Constants"
import Combine from "../Base/Combine"
import { TabbedComponent } from "../Base/TabbedComponent"
import { UIEventSource } from "../../Logic/UIEventSource"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import BaseUIElement from "../BaseUIElement"
import Toggle from "../Input/Toggle"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Utils } from "../../Utils"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import PrivacyPolicy from "./PrivacyPolicy"
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
public static MoreThemesTabIndex = 1
public static MoreThemesTabIndex = 1;
constructor(isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number>,
state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>,
featurePipeline: FeaturePipeline,
backgroundLayer: UIEventSource<BaseLayer>,
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState) {
const layoutToUse = state.layoutToUse;
constructor(
isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number>,
state: {
layoutToUse: LayoutConfig
osmConnection: OsmConnection
featureSwitchShareScreen: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>
locationControl: UIEventSource<Loc>
featurePipeline: FeaturePipeline
backgroundLayer: UIEventSource<BaseLayer>
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState
) {
const layoutToUse = state.layoutToUse
super(
() => layoutToUse.title.Clone(),
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown),
@ -46,83 +47,99 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
)
}
private static ConstructBaseTabs(state: { layoutToUse: LayoutConfig; osmConnection: OsmConnection; featureSwitchShareScreen: UIEventSource<boolean>; featureSwitchMoreQuests: UIEventSource<boolean>; featurePipeline: FeaturePipeline; locationControl: UIEventSource<Loc>; backgroundLayer: UIEventSource<BaseLayer>; filteredLayers: UIEventSource<FilteredLayer[]> } & UserRelatedState,
isShown: UIEventSource<boolean>, currentTab: UIEventSource<number>):
{ header: string | BaseUIElement; content: BaseUIElement }[] {
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
{header: `<img src='${state.layoutToUse.icon}'>`, content: new ThemeIntroductionPanel(isShown, currentTab, state)},
private static ConstructBaseTabs(
state: {
layoutToUse: LayoutConfig
osmConnection: OsmConnection
featureSwitchShareScreen: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>
featurePipeline: FeaturePipeline
locationControl: UIEventSource<Loc>
backgroundLayer: UIEventSource<BaseLayer>
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState,
isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number>
): { header: string | BaseUIElement; content: BaseUIElement }[] {
const tabs: { header: string | BaseUIElement; content: BaseUIElement }[] = [
{
header: `<img src='${state.layoutToUse.icon}'>`,
content: new ThemeIntroductionPanel(isShown, currentTab, state),
},
]
if (state.featureSwitchMoreQuests.data) {
tabs.push({
header: Svg.add_img,
content:
new Combine([
Translations.t.general.morescreen.intro,
new MoreScreen(state)
]).SetClass("flex flex-col")
});
content: new Combine([
Translations.t.general.morescreen.intro,
new MoreScreen(state),
]).SetClass("flex flex-col"),
})
}
if (state.featureSwitchShareScreen.data) {
tabs.push({header: Svg.share_img, content: new ShareScreen(state)});
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)
]
)
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()
content: new PrivacyPolicy(),
}
tabs.push(privacy)
return tabs;
return tabs
}
private static GenerateContents(state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
featurePipeline: FeaturePipeline,
locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) {
private static GenerateContents(
state: {
layoutToUse: LayoutConfig
osmConnection: OsmConnection
featureSwitchShareScreen: UIEventSource<boolean>
featureSwitchMoreQuests: UIEventSource<boolean>
featurePipeline: FeaturePipeline
locationControl: UIEventSource<Loc>
backgroundLayer: UIEventSource<BaseLayer>
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState,
currentTab: UIEventSource<number>,
isShown: UIEventSource<boolean>
) {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab)]
const tabsWithAboutMc = [
...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown, currentTab),
]
tabsWithAboutMc.push({
header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete
.Subs({"osmcha_link": Utils.OsmChaLinkFor(7)}), "<br/>Version " + Constants.vNumber])
.SetClass("link-underline")
}
);
header: Svg.help,
content: new Combine([
Translations.t.general.aboutMapcomplete.Subs({
osmcha_link: Utils.OsmChaLinkFor(7),
}),
"<br/>Version " + Constants.vNumber,
]).SetClass("link-underline"),
})
tabs.forEach(c => c.content.SetClass("p-4"))
tabsWithAboutMc.forEach(c => c.content.SetClass("p-4"))
tabs.forEach((c) => c.content.SetClass("p-4"))
tabsWithAboutMc.forEach((c) => c.content.SetClass("p-4"))
return new Toggle(
new TabbedComponent(tabsWithAboutMc, currentTab),
new TabbedComponent(tabs, currentTab),
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
userdetails.loggedIn &&
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock)
state.osmConnection.userDetails.map(
(userdetails: UserDetails) =>
userdetails.loggedIn &&
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock
)
)
}
}
}

View file

@ -1,14 +1,13 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import Table from "../Base/Table";
import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Utils} from "../../Utils";
import BaseUIElement from "../BaseUIElement";
import Svg from "../../Svg";
import { VariableUiElement } from "../Base/VariableUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Table from "../Base/Table"
import Combine from "../Base/Combine"
import { FixedUiElement } from "../Base/FixedUiElement"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import Svg from "../../Svg"
export default class Histogram<T> extends VariableUiElement {
private static defaultPalette = [
"#ff5858",
"#ffad48",
@ -16,29 +15,35 @@ export default class Histogram<T> extends VariableUiElement {
"#56bd56",
"#63a9ff",
"#9d62d9",
"#fa61fa"
"#fa61fa",
]
constructor(values: Store<string[]>,
title: string | BaseUIElement,
countTitle: string | BaseUIElement,
options?: {
assignColor?: (t0: string) => string,
sortMode?: "name" | "name-rev" | "count" | "count-rev"
}
constructor(
values: Store<string[]>,
title: string | BaseUIElement,
countTitle: string | BaseUIElement,
options?: {
assignColor?: (t0: string) => string
sortMode?: "name" | "name-rev" | "count" | "count-rev"
}
) {
const sortMode = new UIEventSource<"name" | "name-rev" | "count" | "count-rev">(options?.sortMode ?? "name")
const sortName = new VariableUiElement(sortMode.map(m => {
switch (m) {
case "name":
return Svg.up_svg()
case "name-rev":
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
default:
return Svg.circle_svg()
}
}))
const titleHeader = new Combine([sortName.SetClass("w-4 mr-2"), title]).SetClass("flex")
const sortMode = new UIEventSource<"name" | "name-rev" | "count" | "count-rev">(
options?.sortMode ?? "name"
)
const sortName = new VariableUiElement(
sortMode.map((m) => {
switch (m) {
case "name":
return Svg.up_svg()
case "name-rev":
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
default:
return Svg.circle_svg()
}
})
)
const titleHeader = new Combine([sortName.SetClass("w-4 mr-2"), title])
.SetClass("flex")
.onClick(() => {
if (sortMode.data === "name") {
sortMode.setData("name-rev")
@ -47,91 +52,103 @@ export default class Histogram<T> extends VariableUiElement {
}
})
const sortCount = new VariableUiElement(sortMode.map(m => {
switch (m) {
case "count":
return Svg.up_svg()
case "count-rev":
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
default:
return Svg.circle_svg()
}
}))
const countHeader = new Combine([sortCount.SetClass("w-4 mr-2"), countTitle]).SetClass("flex").onClick(() => {
if (sortMode.data === "count-rev") {
sortMode.setData("count")
} else {
sortMode.setData("count-rev")
}
})
const header = [
titleHeader,
countHeader
]
super(values.map(values => {
if (values === undefined) {
return undefined;
}
values = Utils.NoNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0;
counts.set(value, c + 1);
}
const keys = Array.from(counts.keys());
switch (sortMode.data) {
case "name":
keys.sort()
break;
case "name-rev":
keys.sort().reverse(/*Copy of array, inplace reverse if fine*/)
break;
case "count":
keys.sort((k0, k1) => counts.get(k0) - counts.get(k1))
break;
case "count-rev":
keys.sort((k0, k1) => counts.get(k1) - counts.get(k0))
break;
}
const max = Math.max(...Array.from(counts.values()))
const fallbackColor = (keyValue: string) => {
const index = keys.indexOf(keyValue)
return Histogram.defaultPalette[index % Histogram.defaultPalette.length]
};
let actualAssignColor = undefined;
if (options?.assignColor === undefined) {
actualAssignColor = fallbackColor;
} else {
actualAssignColor = (keyValue: string) => {
return options.assignColor(keyValue) ?? fallbackColor(keyValue)
const sortCount = new VariableUiElement(
sortMode.map((m) => {
switch (m) {
case "count":
return Svg.up_svg()
case "count-rev":
return Svg.up_svg().SetStyle("transform: rotate(180deg)")
default:
return Svg.circle_svg()
}
}
})
)
return new Table(
header,
keys.map(key => [
key,
new Combine([
new Combine([new FixedUiElement("" + counts.get(key)).SetClass("font-bold rounded-full block")])
.SetClass("flex justify-center rounded border border-black")
.SetStyle(`background: ${actualAssignColor(key)}; width: ${100 * counts.get(key) / max}%`)
]).SetClass("block w-full")
]),{
contentStyle:keys.map(_ => ["width: 20%"])
const countHeader = new Combine([sortCount.SetClass("w-4 mr-2"), countTitle])
.SetClass("flex")
.onClick(() => {
if (sortMode.data === "count-rev") {
sortMode.setData("count")
} else {
sortMode.setData("count-rev")
}
).SetClass("w-full zebra-table");
}, [sortMode]));
})
const header = [titleHeader, countHeader]
super(
values.map(
(values) => {
if (values === undefined) {
return undefined
}
values = Utils.NoNull(values)
const counts = new Map<string, number>()
for (const value of values) {
const c = counts.get(value) ?? 0
counts.set(value, c + 1)
}
const keys = Array.from(counts.keys())
switch (sortMode.data) {
case "name":
keys.sort()
break
case "name-rev":
keys.sort().reverse(/*Copy of array, inplace reverse if fine*/)
break
case "count":
keys.sort((k0, k1) => counts.get(k0) - counts.get(k1))
break
case "count-rev":
keys.sort((k0, k1) => counts.get(k1) - counts.get(k0))
break
}
const max = Math.max(...Array.from(counts.values()))
const fallbackColor = (keyValue: string) => {
const index = keys.indexOf(keyValue)
return Histogram.defaultPalette[index % Histogram.defaultPalette.length]
}
let actualAssignColor = undefined
if (options?.assignColor === undefined) {
actualAssignColor = fallbackColor
} else {
actualAssignColor = (keyValue: string) => {
return options.assignColor(keyValue) ?? fallbackColor(keyValue)
}
}
return new Table(
header,
keys.map((key) => [
key,
new Combine([
new Combine([
new FixedUiElement("" + counts.get(key)).SetClass(
"font-bold rounded-full block"
),
])
.SetClass("flex justify-center rounded border border-black")
.SetStyle(
`background: ${actualAssignColor(key)}; width: ${
(100 * counts.get(key)) / max
}%`
),
]).SetClass("block w-full"),
]),
{
contentStyle: keys.map((_) => ["width: 20%"]),
}
).SetClass("w-full zebra-table")
},
[sortMode]
)
)
}
}
}

View file

@ -1,27 +1,29 @@
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import { FixedUiElement } from "../Base/FixedUiElement"
export default class IndexText extends Combine {
constructor() {
super([
new FixedUiElement(`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`)
.SetClass("flex-none m-3"),
new FixedUiElement(
`<img class="w-12 h-12 sm:h-24 sm:w-24" src="./assets/svg/logo.svg" alt="MapComplete Logo">`
).SetClass("flex-none m-3"),
new Combine([
Translations.t.index.title
.SetClass("text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline"),
Translations.t.index.title.SetClass(
"text-2xl tracking-tight font-extrabold text-gray-900 sm:text-5xl md:text-6xl block text-gray-800 xl:inline"
),
Translations.t.index.intro.SetClass(
"mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"),
"mt-3 text-base font-semibold text-gray-500 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"
),
Translations.t.index.pickTheme.SetClass("mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0")
Translations.t.index.pickTheme.SetClass(
"mt-3 text-base text-green-600 sm:mt-5 sm:text-lg sm:max-w-xl sm:mx-auto md:mt-5 md:text-xl lg:mx-0"
),
]).SetClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4"),
])
]).SetClass("flex flex-col sm:text-center lg:text-left m-1 mt-2 md:m-2 md:mt-4")
]);
this.SetClass("flex flex-row");
this.SetClass("flex flex-row")
}
}
}

View file

@ -1,93 +1,102 @@
import Combine from "../Base/Combine";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations";
import Toggle from "../Input/Toggle";
import MapControlButton from "../MapControlButton";
import Svg from "../../Svg";
import AllDownloads from "./AllDownloads";
import FilterView from "./FilterView";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import BackgroundMapSwitch from "./BackgroundMapSwitch";
import Lazy from "../Base/Lazy";
import {VariableUiElement} from "../Base/VariableUIElement";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import CopyrightPanel from "./CopyrightPanel";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import Combine from "../Base/Combine"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Translations from "../i18n/Translations"
import Toggle from "../Input/Toggle"
import MapControlButton from "../MapControlButton"
import Svg from "../../Svg"
import AllDownloads from "./AllDownloads"
import FilterView from "./FilterView"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import BackgroundMapSwitch from "./BackgroundMapSwitch"
import Lazy from "../Base/Lazy"
import { VariableUiElement } from "../Base/VariableUIElement"
import FeatureInfoBox from "../Popup/FeatureInfoBox"
import CopyrightPanel from "./CopyrightPanel"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
export default class LeftControls extends Combine {
constructor(state: FeaturePipelineState,
guiState: {
currentViewControlIsOpened: UIEventSource<boolean>;
downloadControlIsOpened: UIEventSource<boolean>,
filterViewIsOpened: UIEventSource<boolean>,
copyrightViewIsOpened: UIEventSource<boolean>
}) {
constructor(
state: FeaturePipelineState,
guiState: {
currentViewControlIsOpened: UIEventSource<boolean>
downloadControlIsOpened: UIEventSource<boolean>
filterViewIsOpened: UIEventSource<boolean>
copyrightViewIsOpened: UIEventSource<boolean>
}
) {
const currentViewFL = state.currentView?.layer
const currentViewAction = new Toggle(
new Lazy(() => {
const feature: Store<any> = state.currentView.features.map(ffs => ffs[0]?.feature)
const icon = new VariableUiElement(feature.map(feature => {
const defaultIcon = Svg.checkbox_empty_svg()
if (feature === undefined) {
return defaultIcon;
}
const tags = {...feature.properties, button: "yes"}
const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon(new UIEventSource(tags));
if (elem === undefined) {
return defaultIcon
}
return elem
})).SetClass("inline-block w-full h-full")
const featureBox = new VariableUiElement(feature.map(feature => {
if (feature === undefined) {
return undefined
}
return new Lazy(() => {
const tagsSource = state.allElements.getEventSourceById(feature.properties.id)
return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, {
hashToShow: "currentview",
isShown: guiState.currentViewControlIsOpened
})
.SetClass("md:floating-element-width")
const feature: Store<any> = state.currentView.features.map((ffs) => ffs[0]?.feature)
const icon = new VariableUiElement(
feature.map((feature) => {
const defaultIcon = Svg.checkbox_empty_svg()
if (feature === undefined) {
return defaultIcon
}
const tags = { ...feature.properties, button: "yes" }
const elem = currentViewFL.layerDef.mapRendering[0]?.GetSimpleIcon(
new UIEventSource(tags)
)
if (elem === undefined) {
return defaultIcon
}
return elem
})
})).SetStyle("width: 40rem").SetClass("block")
).SetClass("inline-block w-full h-full")
const featureBox = new VariableUiElement(
feature.map((feature) => {
if (feature === undefined) {
return undefined
}
return new Lazy(() => {
const tagsSource = state.allElements.getEventSourceById(
feature.properties.id
)
return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, state, {
hashToShow: "currentview",
isShown: guiState.currentViewControlIsOpened,
}).SetClass("md:floating-element-width")
})
})
)
.SetStyle("width: 40rem")
.SetClass("block")
return new Toggle(
featureBox,
new MapControlButton(icon),
guiState.currentViewControlIsOpened
)
}).onClick(() => {
guiState.currentViewControlIsOpened.setData(true)
}),
undefined,
new UIEventSource<boolean>(currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null)
new UIEventSource<boolean>(
currentViewFL !== undefined && currentViewFL?.layerDef?.tagRenderings !== null
)
)
const toggledDownload = new Toggle(
new AllDownloads(
guiState.downloadControlIsOpened,
state
).SetClass("block p-1 rounded-full md:floating-element-width"),
new MapControlButton(Svg.download_svg())
.onClick(() => guiState.downloadControlIsOpened.setData(true)),
new AllDownloads(guiState.downloadControlIsOpened, state).SetClass(
"block p-1 rounded-full md:floating-element-width"
),
new MapControlButton(Svg.download_svg()).onClick(() =>
guiState.downloadControlIsOpened.setData(true)
),
guiState.downloadControlIsOpened
)
const downloadButtonn = new Toggle(
toggledDownload,
undefined,
state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data,
[state.featureSwitchExportAsPdf])
);
state.featureSwitchEnableExport.map(
(downloadEnabled) => downloadEnabled || state.featureSwitchExportAsPdf.data,
[state.featureSwitchExportAsPdf]
)
)
const toggledFilter = new Toggle(
new ScrollableFullScreen(
@ -99,16 +108,13 @@ export default class LeftControls extends Combine {
"filters",
guiState.filterViewIsOpened
).SetClass("rounded-lg md:floating-element-width"),
new MapControlButton(Svg.layers_svg())
.onClick(() => guiState.filterViewIsOpened.setData(true)),
new MapControlButton(Svg.layers_svg()).onClick(() =>
guiState.filterViewIsOpened.setData(true)
),
guiState.filterViewIsOpened
)
const filterButton = new Toggle(
toggledFilter,
undefined,
state.featureSwitchFilter
);
const filterButton = new Toggle(toggledFilter, undefined, state.featureSwitchFilter)
const mapSwitch = new Toggle(
new BackgroundMapSwitch(state, state.backgroundLayer),
@ -119,32 +125,26 @@ export default class LeftControls extends Combine {
// 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 Toggle(
new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle,
() => new CopyrightPanel(state),
"copyright",
new Lazy(
() =>
new Toggle(
new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle,
() => new CopyrightPanel(state),
"copyright",
guiState.copyrightViewIsOpened
),
new MapControlButton(Svg.copyright_svg()).onClick(() =>
guiState.copyrightViewIsOpened.setData(true)
),
guiState.copyrightViewIsOpened
),
new MapControlButton(Svg.copyright_svg()).onClick(() => guiState.copyrightViewIsOpened.setData(true)),
guiState.copyrightViewIsOpened
)
)
),
state.featureSwitchWelcomeMessage
)
super([
currentViewAction,
filterButton,
downloadButtonn,
copyright,
mapSwitch
])
super([currentViewAction, filterButton, downloadButtonn, copyright, mapSwitch])
this.SetClass("flex flex-col")
}
}
}

View file

@ -1,36 +1,36 @@
import FloorLevelInputElement from "../Input/FloorLevelInputElement";
import MapState, {GlobalFilter} from "../../Logic/State/MapState";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {RegexTag} from "../../Logic/Tags/RegexTag";
import {Or} from "../../Logic/Tags/Or";
import {Tag} from "../../Logic/Tags/Tag";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import {OsmFeature} from "../../Models/OsmFeature";
import {BBox} from "../../Logic/BBox";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {Store} from "../../Logic/UIEventSource";
import FloorLevelInputElement from "../Input/FloorLevelInputElement"
import MapState, { GlobalFilter } from "../../Logic/State/MapState"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { RegexTag } from "../../Logic/Tags/RegexTag"
import { Or } from "../../Logic/Tags/Or"
import { Tag } from "../../Logic/Tags/Tag"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import { OsmFeature } from "../../Models/OsmFeature"
import { BBox } from "../../Logic/BBox"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { Store } from "../../Logic/UIEventSource"
/***
* The element responsible for the level input element and picking the right level, showing and hiding at the right time, ...
*/
export default class LevelSelector extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const levelsInView : Store< Record<string, number>> = state.currentBounds.map(bbox => {
const levelsInView: Store<Record<string, number>> = state.currentBounds.map((bbox) => {
if (bbox === undefined) {
return {}
}
const allElementsUnfiltered: OsmFeature[] = [].concat(...state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map(ff => ff.features))
const allElements = allElementsUnfiltered.filter(f => BBox.get(f).overlapsWith(bbox))
const allLevelsRaw: string[] = allElements.map(f => f.properties["level"])
const levels : Record<string, number> = {"0": 0}
const allElementsUnfiltered: OsmFeature[] = [].concat(
...state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox).map((ff) => ff.features)
)
const allElements = allElementsUnfiltered.filter((f) => BBox.get(f).overlapsWith(bbox))
const allLevelsRaw: string[] = allElements.map((f) => f.properties["level"])
const levels: Record<string, number> = { "0": 0 }
for (const levelDescription of allLevelsRaw) {
if(levelDescription === undefined){
levels["0"] ++
if (levelDescription === undefined) {
levels["0"]++
}
for (const level of TagUtils.LevelsParser(levelDescription)) {
levels[level] = (levels[level] ?? 0) + 1
@ -46,61 +46,66 @@ export default class LevelSelector extends Combine {
filter: {
currentFilter: undefined,
state: undefined,
},
id: "level",
onNewPoint: undefined
onNewPoint: undefined,
})
const isShown = levelsInView.map(levelsInView => {
const isShown = levelsInView.map(
(levelsInView) => {
if (state.locationControl.data.zoom <= 16) {
return false;
return false
}
if (Object.keys(levelsInView).length == 1) {
return false;
return false
}
return true;
return true
},
[state.locationControl])
[state.locationControl]
)
function setLevelFilter() {
console.log("Updating levels filter to ", levelSelect.GetValue().data, " is shown:", isShown.data)
const filter: GlobalFilter = state.globalFilters.data.find(gf => gf.id === "level")
console.log(
"Updating levels filter to ",
levelSelect.GetValue().data,
" is shown:",
isShown.data
)
const filter: GlobalFilter = state.globalFilters.data.find((gf) => gf.id === "level")
if (!isShown.data) {
filter.filter = {
state: "*",
currentFilter: undefined,
}
filter.onNewPoint = undefined
state.globalFilters.ping();
state.globalFilters.ping()
return
}
const l = levelSelect.GetValue().data
if(l === undefined){
if (l === undefined) {
return
}
let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)"));
let neededLevel: TagsFilter = new RegexTag("level", new RegExp("(^|;)" + l + "(;|$)"))
if (l === "0") {
neededLevel = new Or([neededLevel, new Tag("level", "")])
}
filter.filter = {
state: l,
currentFilter: neededLevel
currentFilter: neededLevel,
}
const t = Translations.t.general.levelSelection
filter.onNewPoint = {
confirmAddNew: t.confirmLevel.PartialSubs({level: l}),
safetyCheck: t.addNewOnLevel.Subs({level: l}),
tags: [new Tag("level", l)]
confirmAddNew: t.confirmLevel.PartialSubs({ level: l }),
safetyCheck: t.addNewOnLevel.Subs({ level: l }),
tags: [new Tag("level", l)],
}
state.globalFilters.ping();
return;
state.globalFilters.ping()
return
}
isShown.addCallbackAndRun(shown => {
isShown.addCallbackAndRun((shown) => {
console.log("Is level selector shown?", shown)
setLevelFilter()
if (shown) {
@ -110,31 +115,36 @@ export default class LevelSelector extends Combine {
}
})
levelsInView.addCallbackAndRun(levels => {
if(!isShown.data){
levelsInView.addCallbackAndRun((levels) => {
if (!isShown.data) {
return
}
const value = levelSelect.GetValue()
if (!(levels[value.data] === undefined || levels[value.data] === 0)) {
return;
return
}
// Nothing in view. Lets switch to a different level (the level with the most features)
let mostElements = 0
let mostElementsLevel = undefined
for (const level in levels) {
const count = levels[level]
if(mostElementsLevel === undefined || mostElements < count){
if (mostElementsLevel === undefined || mostElements < count) {
mostElementsLevel = level
mostElements = count
}
}
console.log("Force switching to a different level:", mostElementsLevel,"as it has",mostElements,"elements on that floor",levels,"(old level: "+value.data+")")
value.setData(mostElementsLevel )
console.log(
"Force switching to a different level:",
mostElementsLevel,
"as it has",
mostElements,
"elements on that floor",
levels,
"(old level: " + value.data + ")"
)
value.setData(mostElementsLevel)
})
levelSelect.GetValue().addCallback(_ => setLevelFilter())
levelSelect.GetValue().addCallback((_) => setLevelFilter())
super([levelSelect])
}
}
}

View file

@ -1,32 +1,33 @@
import {DropDown} from "../Input/DropDown";
import Translations from "../i18n/Translations";
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Translation} from "../i18n/Translation";
import { DropDown } from "../Input/DropDown"
import Translations from "../i18n/Translations"
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Translation } from "../i18n/Translation"
export default class LicensePicker extends DropDown<string> {
private static readonly cc0 = "CC0"
private static readonly ccbysa = "CC-BY-SA 4.0"
private static readonly ccby = "CC-BY 4.0"
constructor(state: { osmConnection: OsmConnection }) {
super(Translations.t.image.willBePublished.Clone(),
super(
Translations.t.image.willBePublished.Clone(),
[
{value: LicensePicker.cc0, shown: Translations.t.image.cco.Clone()},
{value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone()},
{value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone()}
{ value: LicensePicker.cc0, shown: Translations.t.image.cco.Clone() },
{ value: LicensePicker.ccbysa, shown: Translations.t.image.ccbs.Clone() },
{ value: LicensePicker.ccby, shown: Translations.t.image.ccb.Clone() },
],
state?.osmConnection?.GetPreference("pictures-license") ?? new UIEventSource<string>("CC0"),
state?.osmConnection?.GetPreference("pictures-license") ??
new UIEventSource<string>("CC0"),
{
select_class:"w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200"
select_class: "w-min bg-indigo-100 p-1 rounded hover:bg-indigo-200",
}
)
this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left");
this.SetClass("flex flex-col sm:flex-row").SetStyle("float:left")
}
public static LicenseExplanations(): Map<string, Translation> {
let dict = new Map<string, Translation>();
let dict = new Map<string, Translation>()
dict.set(LicensePicker.cc0, Translations.t.image.ccoExplanation)
dict.set(LicensePicker.ccby, Translations.t.image.ccbExplanation)
@ -34,5 +35,4 @@ export default class LicensePicker extends DropDown<string> {
return dict
}
}
}

View file

@ -1,24 +1,29 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import Translations from "../i18n/Translations";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import { VariableUiElement } from "../Base/VariableUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import Translations from "../i18n/Translations"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
export class MapillaryLink extends VariableUiElement {
constructor(state: { locationControl: UIEventSource<Loc> }, iconStyle?: string) {
const t = Translations.t.general.attribution
super(state.locationControl.map(location => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${location?.lat ?? 0}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle),
new Combine([
t.openMapillary.SetClass("font-bold"),
t.mapillaryHelp]), {
url: mapillaryLink,
newTab: true
}).SetClass("flex flex-col link-no-underline")
}))
super(
state.locationControl.map((location) => {
const mapillaryLink = `https://www.mapillary.com/app/?focus=map&lat=${
location?.lat ?? 0
}&lng=${location?.lon ?? 0}&z=${Math.max((location?.zoom ?? 2) - 1, 1)}`
return new SubtleButton(
Svg.mapillary_black_ui().SetStyle(iconStyle),
new Combine([t.openMapillary.SetClass("font-bold"), t.mapillaryHelp]),
{
url: mapillaryLink,
newTab: true,
}
).SetClass("flex flex-col link-no-underline")
})
)
}
}
}

View file

@ -1,82 +1,90 @@
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 { 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 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 Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, Stores, 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 {Translation} from "../i18n/Translation";
import {TextField} from "../Input/TextField";
import FilteredCombine from "../Base/FilteredCombine";
import Locale from "../i18n/Locale";
import { Translation } from "../i18n/Translation"
import { TextField } from "../Input/TextField"
import FilteredCombine from "../Base/FilteredCombine"
import Locale from "../i18n/Locale"
export default class MoreScreen extends Combine {
private static readonly officialThemes: {
id: string,
icon: string,
title: any,
shortDescription: any,
definition?: any,
mustHaveLanguage?: boolean,
hideFromOverview: boolean,
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywors?: any[]
}[] = themeOverview["default"];
constructor(state: UserRelatedState & {
locationControl?: UIEventSource<Loc>,
layoutToUse?: LayoutConfig
}, onMainScreen: boolean = false) {
const tr = Translations.t.general.morescreen;
}[] = themeOverview["default"]
constructor(
state: UserRelatedState & {
locationControl?: UIEventSource<Loc>
layoutToUse?: LayoutConfig
},
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"
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,
})
search.enterPressed.addCallbackD(searchTerm => {
search.enterPressed.addCallbackD((searchTerm) => {
searchTerm = searchTerm.toLowerCase()
if(searchTerm === "personal"){
window.location.href = MoreScreen.createUrlFor({id: "personal"}, false, state).data
if (searchTerm === "personal") {
window.location.href = MoreScreen.createUrlFor(
{ id: "personal" },
false,
state
).data
}
if(searchTerm === "bugs" || searchTerm === "issues") {
if (searchTerm === "bugs" || searchTerm === "issues") {
window.location.href = "https://github.com/pietervdvn/MapComplete/issues"
}
if(searchTerm === "source") {
if (searchTerm === "source") {
window.location.href = "https://github.com/pietervdvn/MapComplete"
}
if(searchTerm === "docs") {
if (searchTerm === "docs") {
window.location.href = "https://github.com/pietervdvn/MapComplete/tree/develop/Docs"
}
if(searchTerm === "osmcha" || searchTerm === "stats"){
if (searchTerm === "osmcha" || searchTerm === "stats") {
window.location.href = Utils.OsmChaLinkFor(7)
}
// Enter pressed -> search the first _official_ matchin theme and open it
const publicTheme = MoreScreen.officialThemes.find(th =>
th.hideFromOverview == false &&
th.id !== "personal" &&
MoreScreen.MatchesLayoutFunc(th)(searchTerm))
const publicTheme = MoreScreen.officialThemes.find(
(th) =>
th.hideFromOverview == false &&
th.id !== "personal" &&
MoreScreen.MatchesLayoutFunc(th)(searchTerm)
)
if (publicTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(publicTheme, false, state).data
}
const hiddenTheme = MoreScreen.officialThemes.find(th =>
th.id !== "personal" &&
MoreScreen.MatchesLayoutFunc(th)(searchTerm))
const hiddenTheme = MoreScreen.officialThemes.find(
(th) => th.id !== "personal" && MoreScreen.MatchesLayoutFunc(th)(searchTerm)
)
if (hiddenTheme !== undefined) {
window.location.href = MoreScreen.createUrlFor(hiddenTheme, false, state).data
}
@ -88,53 +96,71 @@ export default class MoreScreen extends Combine {
document.addEventListener("keydown", function (event) {
if (event.ctrlKey && event.code === "KeyF") {
search.focus()
event.preventDefault();
event.preventDefault()
}
});
const searchBar = new Combine([Svg.search_svg().SetClass("w-8"), search.SetClass("mr-4 w-full")])
.SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2")
})
const searchBar = new Combine([
Svg.search_svg().SetClass("w-8"),
search.SetClass("mr-4 w-full"),
]).SetClass("flex rounded-full border-2 border-black items-center my-2 w-1/2")
super([
new Combine([searchBar]).SetClass("flex justify-center"),
MoreScreen.createOfficialThemesList(state, themeButtonStyle, themeListStyle, search.GetValue()),
MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle, themeListStyle, search.GetValue()),
MoreScreen.createUnofficialThemeList(themeButtonStyle, state, themeListStyle, search.GetValue()),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10")
]);
MoreScreen.createOfficialThemesList(
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createPreviouslyVistedHiddenList(
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createUnofficialThemeList(
themeButtonStyle,
state,
themeListStyle,
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;
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(""))
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,
state?: { locationControl?: UIEventSource<{ lat, lon, zoom }>, layoutToUse?: { id } }
private static createUrlFor(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined;
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout);
return undefined;
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined;
return undefined
}
const currentLocation = state?.locationControl;
const currentLocation = state?.locationControl
let path = window.location.pathname;
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 = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
@ -154,18 +180,19 @@ export default class MoreScreen extends Combine {
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}`)
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}`)
)
}
/**
@ -174,135 +201,157 @@ export default class MoreScreen extends Combine {
*/
public static createLinkButton(
state: {
locationControl?: UIEventSource<Loc>,
locationControl?: UIEventSource<Loc>
layoutToUse?: LayoutConfig
}, layout: {
id: string,
icon: string,
title: any,
shortDescription: any,
definition?: any,
},
layout: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
}, isCustom: boolean = false
):
BaseUIElement {
},
isCustom: boolean = false
): BaseUIElement {
const url = MoreScreen.createUrlFor(layout, isCustom, state)
let content = new Combine([
new Translation(layout.title, !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined),
new Translation(
layout.title,
!isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined
),
new Translation(layout.shortDescription)?.SetClass("subtle") ?? "",
]).SetClass("overflow-hidden flex flex-col")
if(state.layoutToUse === undefined){
if (state.layoutToUse === undefined) {
// Currently on the index screen: we style the buttons equally large
content = new Combine([content]).SetClass("flex flex-col justify-center h-24")
}
return new SubtleButton(layout.icon, content, {url, newTab: false});
return new SubtleButton(layout.icon, content, { url, newTab: false })
}
public static CreateProffessionalSerivesButton() {
const t = Translations.t.professional.indexPage;
const t = Translations.t.professional.indexPage
return new Combine([
new Title(t.hook, 4),
t.hookMore,
new SubtleButton(undefined, t.button, {url: "./professional.html"}),
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 {
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 }[] = []
stableIds.map((ids) => {
const allThemes: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
[]
for (const id of ids) {
const themeInfo = state.GetUnofficialTheme(id)
if(themeInfo === undefined){
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
predicate: (s) => id.toLowerCase().indexOf(s) >= 0,
})
}
}
if (allThemes.length <= 0) {
return undefined;
return undefined
}
return new Combine([
Translations.t.general.customThemeIntro,
new FilteredCombine(allThemes, search, {
innerClasses: themeListClasses,
onEmpty: MoreScreen.NothingFound(search)
})
]);
}));
onEmpty: MoreScreen.NothingFound(search),
}),
])
})
)
}
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string, search: UIEventSource<string>): BaseUIElement {
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 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))));
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 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)
}
)
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
total_hidden: "" + hiddenTotal,
}),
knownLayouts
knownLayouts,
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static MatchesLayoutFunc(layout: {
id: string,
title: any,
shortDescription: any,
id: string
title: any
shortDescription: any
keywords?: any[]
}): ((search: string) => boolean) {
}): (search: string) => boolean {
return (search: string) => {
search = search.toLocaleLowerCase()
if (layout.id.toLowerCase().indexOf(search) >= 0) {
return true;
return true
}
const entitiesToSearch = [layout.shortDescription, layout.title, ...(layout.keywords ?? [])]
const entitiesToSearch = [
layout.shortDescription,
layout.title,
...(layout.keywords ?? []),
]
for (const entity of entitiesToSearch) {
if (entity === undefined) {
continue
@ -313,73 +362,73 @@ export default class MoreScreen extends Combine {
}
}
return false;
return false
}
}
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string, themeListStyle: string, search: UIEventSource<string>): BaseUIElement {
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) }
})
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 professional = MoreScreen.CreateProffessionalSerivesButton()
const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0,
{element: customGeneratorLink},
{element: professional});
buttons.splice(0, 0, { element: customGeneratorLink }, { element: professional })
return new FilteredCombine(buttons, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search)
});
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;
* 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 => {
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
});
newTab: true,
})
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false
});
newTab: false,
})
})
)
}
}
}

View file

@ -1,17 +1,16 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import PlantNet from "../../Logic/Web/PlantNet";
import Loading from "../Base/Loading";
import Wikidata from "../../Logic/Web/Wikidata";
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox";
import {Button} from "../Base/Button";
import Combine from "../Base/Combine";
import Title from "../Base/Title";
import WikipediaBox from "../Wikipedia/WikipediaBox";
import Translations from "../i18n/Translations";
import List from "../Base/List";
import Svg from "../../Svg";
import { VariableUiElement } from "../Base/VariableUIElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import PlantNet from "../../Logic/Web/PlantNet"
import Loading from "../Base/Loading"
import Wikidata from "../../Logic/Web/Wikidata"
import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
import { Button } from "../Base/Button"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import WikipediaBox from "../Wikipedia/WikipediaBox"
import Translations from "../i18n/Translations"
import List from "../Base/List"
import Svg from "../../Svg"
export default class PlantNetSpeciesSearch extends VariableUiElement {
/***
@ -23,99 +22,116 @@ export default class PlantNetSpeciesSearch extends VariableUiElement {
const t = Translations.t.plantDetection
super(
images
.bind(images => {
.bind((images) => {
if (images.length === 0) {
return null
}
return UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0,5)));
return UIEventSource.FromPromiseWithErr(PlantNet.query(images.slice(0, 5)))
})
.map(result => {
.map((result) => {
if (images.data.length === 0) {
return new Combine([t.takeImages, t.howTo.intro, new List(
[
t.howTo.li0,
t.howTo.li1,
t.howTo.li2,
t.howTo.li3
]
)]).SetClass("flex flex-col")
return new Combine([
t.takeImages,
t.howTo.intro,
new List([t.howTo.li0, t.howTo.li1, t.howTo.li2, t.howTo.li3]),
]).SetClass("flex flex-col")
}
if (result === undefined) {
return new Loading(t.querying.Subs(images.data))
}
if (result === undefined) {
return new Loading(t.querying.Subs(images.data))
}
if (result["error"] !== undefined) {
return t.error.Subs(<any>result).SetClass("alert")
}
console.log(result)
const success = result["success"]
const selectedSpecies = new UIEventSource<string>(undefined)
const speciesInformation = success.results
.filter(species => species.score >= 0.005)
.map(species => {
const wikidata = UIEventSource.FromPromise(Wikidata.Sparql<{ species }>(["?species", "?speciesLabel"],
["?species wdt:P846 \"" + species.gbif.id + "\""]));
if (result["error"] !== undefined) {
return t.error.Subs(<any>result).SetClass("alert")
}
console.log(result)
const success = result["success"]
const confirmButton = new Button(t.seeInfo, async() => {
await selectedSpecies.setData(wikidata.data[0].species?.value)
}).SetClass("btn")
const selectedSpecies = new UIEventSource<string>(undefined)
const speciesInformation = success.results
.filter((species) => species.score >= 0.005)
.map((species) => {
const wikidata = UIEventSource.FromPromise(
Wikidata.Sparql<{ species }>(
["?species", "?speciesLabel"],
['?species wdt:P846 "' + species.gbif.id + '"']
)
)
const match = t.matchPercentage.Subs({match: Math.round(species.score * 100)}).SetClass("font-bold")
const confirmButton = new Button(t.seeInfo, async () => {
await selectedSpecies.setData(wikidata.data[0].species?.value)
}).SetClass("btn")
const extraItems = new Combine([match, confirmButton]).SetClass("flex flex-col")
const match = t.matchPercentage
.Subs({ match: Math.round(species.score * 100) })
.SetClass("font-bold")
return new WikidataPreviewBox(wikidata.map(wd => wd == undefined ? undefined : wd[0]?.species?.value),
{
whileLoading: new Loading(
t.loadingWikidata.Subs({species: species.species.scientificNameWithoutAuthor})),
extraItems: [new Combine([extraItems])],
const extraItems = new Combine([match, confirmButton]).SetClass(
"flex flex-col"
)
imageStyle: "max-width: 8rem; width: unset; height: 8rem"
return new WikidataPreviewBox(
wikidata.map((wd) =>
wd == undefined ? undefined : wd[0]?.species?.value
),
{
whileLoading: new Loading(
t.loadingWikidata.Subs({
species: species.species.scientificNameWithoutAuthor,
})
.SetClass("border-2 border-subtle rounded-xl block mb-2")
),
extraItems: [new Combine([extraItems])],
imageStyle: "max-width: 8rem; width: unset; height: 8rem",
}
);
const plantOverview = new Combine([
new Title(t.overviewTitle),
t.overviewIntro,
t.overviewVerify.SetClass("font-bold"),
...speciesInformation]).SetClass("flex flex-col")
).SetClass("border-2 border-subtle rounded-xl block mb-2")
})
const plantOverview = new Combine([
new Title(t.overviewTitle),
t.overviewIntro,
t.overviewVerify.SetClass("font-bold"),
...speciesInformation,
]).SetClass("flex flex-col")
return new VariableUiElement(selectedSpecies.map(wikidataSpecies => {
return new VariableUiElement(
selectedSpecies.map((wikidataSpecies) => {
if (wikidataSpecies === undefined) {
return plantOverview
}
const buttons = new Combine([
new Button(
new Combine([
Svg.back_svg().SetClass("w-6 mr-1 bg-white rounded-full p-1"),
t.back]).SetClass("flex"),
new Combine([
Svg.back_svg().SetClass(
"w-6 mr-1 bg-white rounded-full p-1"
),
t.back,
]).SetClass("flex"),
() => {
selectedSpecies.setData(undefined)
}).SetClass("btn btn-secondary"),
selectedSpecies.setData(undefined)
}
).SetClass("btn btn-secondary"),
new Button(
new Combine([Svg.confirm_svg().SetClass("w-6 mr-1"), t.confirm]).SetClass("flex")
, () => {
onConfirm(wikidataSpecies)
}).SetClass("btn"),
]).SetClass("flex justify-between");
new Combine([
Svg.confirm_svg().SetClass("w-6 mr-1"),
t.confirm,
]).SetClass("flex"),
() => {
onConfirm(wikidataSpecies)
}
).SetClass("btn"),
]).SetClass("flex justify-between")
return new Combine([
new WikipediaBox([wikidataSpecies], {
firstParagraphOnly: false,
noImages: false,
addHeader: false
addHeader: false,
}).SetClass("h-96"),
buttons
buttons,
]).SetClass("flex flex-col self-end")
}))
}
))
})
)
})
)
}
}
}

View file

@ -1,6 +1,6 @@
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
export default class PrivacyPolicy extends Combine {
constructor() {
@ -19,8 +19,7 @@ export default class PrivacyPolicy extends Combine {
t.miscCookies,
new Title(t.whileYoureHere),
t.surveillance,
]);
])
this.SetClass("link-underline")
}
}
}

View file

@ -1,52 +1,42 @@
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 {Utils} from "../../Utils";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {BBox} from "../../Logic/BBox";
import {OsmFeature} from "../../Models/OsmFeature";
import LevelSelector from "./LevelSelector";
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 { Utils } from "../../Utils"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { BBox } from "../../Logic/BBox"
import { OsmFeature } from "../../Models/OsmFeature"
import LevelSelector from "./LevelSelector"
export default class RightControls extends Combine {
constructor(state: MapState & { featurePipeline: FeaturePipeline }) {
const geolocatioHandler = new GeoLocationHandler(
state
)
const geolocatioHandler = new GeoLocationHandler(state)
const geolocationButton = new Toggle(
new MapControlButton(
geolocatioHandler
, {
dontStyle: true
}
),
new MapControlButton(geolocatioHandler, {
dontStyle: true,
}),
undefined,
state.featureSwitchGeolocation
);
)
const plus = new MapControlButton(
Svg.plus_svg()
).onClick(() => {
state.locationControl.data.zoom++;
state.locationControl.ping();
});
const plus = new MapControlButton(Svg.plus_svg()).onClick(() => {
state.locationControl.data.zoom++
state.locationControl.ping()
})
const min = new MapControlButton(
Svg.min_svg()
).onClick(() => {
state.locationControl.data.zoom--;
state.locationControl.ping();
});
const min = new MapControlButton(Svg.min_svg()).onClick(() => {
state.locationControl.data.zoom--
state.locationControl.ping()
})
const levelSelector = new LevelSelector(state);
super([levelSelector, plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))
const levelSelector = new LevelSelector(state)
super(
[levelSelector, plus, min, geolocationButton].map((el) => el.SetClass("m-0.5 md:m-1"))
)
this.SetClass("flex flex-col items-center")
}
}
}

View file

@ -1,28 +1,24 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {Translation} from "../i18n/Translation";
import {VariableUiElement} from "../Base/VariableUIElement";
import Svg from "../../Svg";
import {TextField} from "../Input/TextField";
import {Geocoding} from "../../Logic/Osm/Geocoding";
import Translations from "../i18n/Translations";
import Hash from "../../Logic/Web/Hash";
import Combine from "../Base/Combine";
import Locale from "../i18n/Locale";
import { UIEventSource } from "../../Logic/UIEventSource"
import { Translation } from "../i18n/Translation"
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import { TextField } from "../Input/TextField"
import { Geocoding } from "../../Logic/Osm/Geocoding"
import Translations from "../i18n/Translations"
import Hash from "../../Logic/Web/Hash"
import Combine from "../Base/Combine"
import Locale from "../i18n/Locale"
export default class SearchAndGo extends Combine {
constructor(state: {
leafletMap: UIEventSource<any>,
selectedElement: UIEventSource<any>
}) {
const goButton = Svg.search_ui().SetClass(
"w-8 h-8 full-rounded border-black float-right"
);
constructor(state: { leafletMap: UIEventSource<any>; selectedElement: UIEventSource<any> }) {
const goButton = Svg.search_ui().SetClass("w-8 h-8 full-rounded border-black float-right")
const placeholder = new UIEventSource<Translation>(
Translations.t.general.search.search
);
const placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
const searchField = new TextField({
placeholder: placeholder.map(tr => tr.textFor(Locale.language.data), [Locale.language]),
placeholder: placeholder.map(
(tr) => tr.textFor(Locale.language.data),
[Locale.language]
),
value: new UIEventSource<string>(""),
inputStyle:
" background: transparent;\n" +
@ -32,53 +28,52 @@ export default class SearchAndGo extends Combine {
" height: 100%;\n" +
" box-sizing: border-box;\n" +
" color: var(--foreground-color);",
});
})
searchField.SetClass("relative float-left mt-0 ml-2");
searchField.SetStyle("width: calc(100% - 3em);height: 100%");
searchField.SetClass("relative float-left mt-0 ml-2")
searchField.SetStyle("width: calc(100% - 3em);height: 100%")
super([searchField, goButton]);
super([searchField, goButton])
this.SetClass("block h-8");
this.SetClass("block h-8")
this.SetStyle(
"background: var(--background-color); color: var(--foreground-color); pointer-evetns:all;"
);
)
// Triggered by 'enter' or onclick
async function runSearch() {
const searchString = searchField.GetValue().data;
const searchString = searchField.GetValue().data
if (searchString === undefined || searchString === "") {
return;
return
}
searchField.GetValue().setData("");
placeholder.setData(Translations.t.general.search.searching);
searchField.GetValue().setData("")
placeholder.setData(Translations.t.general.search.searching)
try {
const result = await Geocoding.Search(searchString)
const result = await Geocoding.Search(searchString);
console.log("Search result", result);
console.log("Search result", result)
if (result.length == 0) {
placeholder.setData(Translations.t.general.search.nothing);
return;
placeholder.setData(Translations.t.general.search.nothing)
return
}
const poi = result[0];
const bb = poi.boundingbox;
const poi = result[0]
const bb = poi.boundingbox
const bounds: [[number, number], [number, number]] = [
[bb[0], bb[2]],
[bb[1], bb[3]],
];
state.selectedElement.setData(undefined);
Hash.hash.setData(poi.osm_type + "/" + poi.osm_id);
state.leafletMap.data.fitBounds(bounds);
]
state.selectedElement.setData(undefined)
Hash.hash.setData(poi.osm_type + "/" + poi.osm_id)
state.leafletMap.data.fitBounds(bounds)
placeholder.setData(Translations.t.general.search.search)
}catch(e){
searchField.GetValue().setData("");
placeholder.setData(Translations.t.general.search.error);
} catch (e) {
searchField.GetValue().setData("")
placeholder.setData(Translations.t.general.search.error)
}
}
searchField.enterPressed.addCallback(runSearch);
goButton.onClick(runSearch);
searchField.enterPressed.addCallback(runSearch)
goButton.onClick(runSearch)
}
}

View file

@ -1,17 +1,20 @@
import BaseUIElement from "../BaseUIElement";
import BaseUIElement from "../BaseUIElement"
export default class ShareButton extends BaseUIElement {
private _embedded: BaseUIElement;
private _shareData: () => { text: string; title: string; url: string };
private _embedded: BaseUIElement
private _shareData: () => { text: string; title: string; url: string }
constructor(embedded: BaseUIElement, generateShareData: () => {
text: string,
title: string,
url: string
}) {
super();
this._embedded = embedded;
this._shareData = generateShareData;
constructor(
embedded: BaseUIElement,
generateShareData: () => {
text: string
title: string
url: string
}
) {
super()
this._embedded = embedded
this._shareData = generateShareData
this.SetClass("share-button")
}
@ -20,21 +23,21 @@ export default class ShareButton extends BaseUIElement {
e.type = "button"
e.appendChild(this._embedded.ConstructElement())
e.addEventListener('click', () => {
e.addEventListener("click", () => {
if (navigator.share) {
navigator.share(this._shareData()).then(() => {
console.log('Thanks for sharing!');
})
.catch(err => {
console.log(`Couldn't share because of`, err.message);
});
navigator
.share(this._shareData())
.then(() => {
console.log("Thanks for sharing!")
})
.catch((err) => {
console.log(`Couldn't share because of`, err.message)
})
} else {
console.log('web share not supported');
console.log("web share not supported")
}
});
})
return e;
return e
}
}
}

View file

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

View file

@ -1,147 +1,162 @@
/**
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
*/
import {UIEventSource} from "../../Logic/UIEventSource";
import Svg from "../../Svg";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import Loc from "../../Models/Loc";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {ElementStorage} from "../../Logic/ElementStorage";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
import BaseLayer from "../../Models/BaseLayer";
import Loading from "../Base/Loading";
import Hash from "../../Logic/Web/Hash";
import {GlobalFilter} from "../../Logic/State/MapState";
import { UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
import PresetConfig from "../../Models/ThemeConfig/PresetConfig"
import FilteredLayer from "../../Models/FilteredLayer"
import Loc from "../../Models/Loc"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import { ElementStorage } from "../../Logic/ElementStorage"
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
import BaseLayer from "../../Models/BaseLayer"
import Loading from "../Base/Loading"
import Hash from "../../Logic/Web/Hash"
import { GlobalFilter } from "../../Logic/State/MapState"
/*
* The SimpleAddUI is a single panel, which can have multiple states:
* - A list of presets which can be added by the user
* - A 'confirm-selection' button (or alternatively: please enable the layer)
* - A 'something is wrong - please soom in further'
* - A 'read your unread messages before adding a point'
* The SimpleAddUI is a single panel, which can have multiple states:
* - A list of presets which can be added by the user
* - A 'confirm-selection' button (or alternatively: please enable the layer)
* - A 'something is wrong - please soom in further'
* - A 'read your unread messages before adding a point'
*/
export interface PresetInfo extends PresetConfig {
name: string | BaseUIElement,
icon: () => BaseUIElement,
layerToAddTo: FilteredLayer,
name: string | BaseUIElement
icon: () => BaseUIElement
layerToAddTo: FilteredLayer
boundsFactor?: 0.25 | number
}
export default class SimpleAddUI extends Toggle {
/**
*
*
* @param isShown
* @param resetScrollSignal
* @param filterViewIsOpened
* @param state
* @param takeLocationFrom: defaults to state.lastClickLocation. Take this location to add the new point around
*/
constructor(isShown: UIEventSource<boolean>,
resetScrollSignal: UIEventSource<void>,
filterViewIsOpened: UIEventSource<boolean>,
state: {
featureSwitchIsTesting: UIEventSource<boolean>,
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
changes: Changes,
allElements: ElementStorage,
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
featurePipeline: FeaturePipeline,
selectedElement: UIEventSource<any>,
locationControl: UIEventSource<Loc>,
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
backgroundLayer: UIEventSource<BaseLayer>,
globalFilters: UIEventSource<GlobalFilter[]>
},
takeLocationFrom?: UIEventSource<{lat: number, lon: number}>
constructor(
isShown: UIEventSource<boolean>,
resetScrollSignal: UIEventSource<void>,
filterViewIsOpened: UIEventSource<boolean>,
state: {
featureSwitchIsTesting: UIEventSource<boolean>
layoutToUse: LayoutConfig
osmConnection: OsmConnection
changes: Changes
allElements: ElementStorage
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
featurePipeline: FeaturePipeline
selectedElement: UIEventSource<any>
locationControl: UIEventSource<Loc>
filteredLayers: UIEventSource<FilteredLayer[]>
featureSwitchFilter: UIEventSource<boolean>
backgroundLayer: UIEventSource<BaseLayer>
globalFilters: UIEventSource<GlobalFilter[]>
},
takeLocationFrom?: UIEventSource<{ lat: number; lon: number }>
) {
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
.onClick(() => state.osmConnection.AttemptLogin());
const loginButton = new SubtleButton(
Svg.osm_logo_ui(),
Translations.t.general.add.pleaseLogin.Clone()
).onClick(() => state.osmConnection.AttemptLogin())
const readYourMessages = new Combine([
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
new SubtleButton(Svg.envelope_ui(),
Translations.t.general.goToInbox, {url: "https://www.openstreetmap.org/messages/inbox", newTab: false})
]);
new SubtleButton(Svg.envelope_ui(), Translations.t.general.goToInbox, {
url: "https://www.openstreetmap.org/messages/inbox",
newTab: false,
}),
])
takeLocationFrom = takeLocationFrom ?? state.LastClickLocation
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
selectedPreset.addCallback(_ => {
resetScrollSignal.ping();
const selectedPreset = new UIEventSource<PresetInfo>(undefined)
selectedPreset.addCallback((_) => {
resetScrollSignal.ping()
})
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
takeLocationFrom.addCallback(_ => selectedPreset.setData(undefined))
isShown.addCallback((_) => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined))
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) : Promise<void>{
async function createNewPoint(
tags: any[],
location: { lat: number; lon: number },
snapOntoWay?: OsmWay
): Promise<void> {
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layoutToUse?.id ?? "unkown",
changeType: "create",
snapOnto: snapOntoWay
snapOnto: snapOntoWay,
})
await state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
isShown.setData(false)
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
state.selectedElement.setData(
state.allElements.ContainingFeatures.get(newElementAction.newElementId)
)
Hash.hash.setData(newElementAction.newElementId)
}
const addUi = new VariableUiElement(
selectedPreset.map(preset => {
if (preset === undefined) {
return presetsOverview
}
function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId?: string) {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD(way => {
createNewPoint(tags, location, <OsmWay>way)
return true;
})
}
}
function cancel() {
selectedPreset.setData(undefined)
}
const message = Translations.t.general.add.addNew.Subs({category: preset.name}, preset.name["context"]);
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message,
takeLocationFrom.data,
confirm,
cancel,
() => {
isShown.setData(false)
})
selectedPreset.map((preset) => {
if (preset === undefined) {
return presetsOverview
}
))
function confirm(
tags: any[],
location: { lat: number; lon: number },
snapOntoWayId?: string
) {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD((way) => {
createNewPoint(tags, location, <OsmWay>way)
return true
})
}
}
function cancel() {
selectedPreset.setData(undefined)
}
const message = Translations.t.general.add.addNew.Subs(
{ category: preset.name },
preset.name["context"]
)
return new ConfirmLocationOfPoint(
state,
filterViewIsOpened,
preset,
message,
takeLocationFrom.data,
confirm,
cancel,
() => {
isShown.setData(false)
}
)
})
)
super(
new Toggle(
@ -152,114 +167,136 @@ export default class SimpleAddUI extends Toggle {
state.featurePipeline.runningQuery
),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
state.locationControl.map(
(loc) => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints
)
),
readYourMessages,
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
userdetails.unreadMessages == 0)
state.osmConnection.userDetails.map(
(userdetails: UserDetails) =>
userdetails.csCount >=
Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
userdetails.unreadMessages == 0
)
),
loginButton,
state.osmConnection.isLoggedIn
)
}
public static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
const csCount = osmConnection.userDetails.data.csCount;
public static CreateTagInfoFor(
preset: PresetInfo,
osmConnection: OsmConnection,
optionallyLinkToWiki = true
) {
const csCount = osmConnection.userDetails.data.csCount
return new Toggle(
Translations.t.general.add.presetInfo.Subs({
tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"),
}).SetStyle("word-break: break-all"),
Translations.t.general.add.presetInfo
.Subs({
tags: preset.tags
.map((t) =>
t.asHumanString(
optionallyLinkToWiki &&
csCount > Constants.userJourney.tagsVisibleAndWikiLinked,
true
)
)
.join("&"),
})
.SetStyle("word-break: break-all"),
undefined,
osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
);
osmConnection.userDetails.map(
(userdetails) => userdetails.csCount >= Constants.userJourney.tagsVisibleAt
)
)
}
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
state: {
featureSwitchIsTesting: UIEventSource<boolean>;
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
osmConnection: OsmConnection
}): BaseUIElement {
private static CreateAllPresetsPanel(
selectedPreset: UIEventSource<PresetInfo>,
state: {
featureSwitchIsTesting: UIEventSource<boolean>
filteredLayers: UIEventSource<FilteredLayer[]>
featureSwitchFilter: UIEventSource<boolean>
osmConnection: OsmConnection
}
): BaseUIElement {
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
let intro: BaseUIElement = Translations.t.general.add.intro;
let intro: BaseUIElement = Translations.t.general.add.intro
let testMode: BaseUIElement = new Toggle(Translations.t.general.testing.SetClass("alert"),
let testMode: BaseUIElement = new Toggle(
Translations.t.general.testing.SetClass("alert"),
undefined,
state.featureSwitchIsTesting);
state.featureSwitchIsTesting
)
return new Combine([intro, testMode, presetButtons]).SetClass("flex flex-col")
}
private static CreatePresetSelectButton(preset: PresetInfo) {
const title = Translations.t.general.add.addNew.Subs({
category: preset.name
}, preset.name["context"])
const title = Translations.t.general.add.addNew.Subs(
{
category: preset.name,
},
preset.name["context"]
)
return new SubtleButton(
preset.icon(),
new Combine([
title.SetClass("font-bold"),
preset.description?.FirstSentence()
preset.description?.FirstSentence(),
]).SetClass("flex flex-col")
)
}
/*
* Generates the list with all the buttons.*/
* Generates the list with all the buttons.*/
private static CreatePresetButtons(
state: {
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
filteredLayers: UIEventSource<FilteredLayer[]>
featureSwitchFilter: UIEventSource<boolean>
osmConnection: OsmConnection
},
selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
const allButtons = [];
selectedPreset: UIEventSource<PresetInfo>
): BaseUIElement {
const allButtons = []
for (const layer of state.filteredLayers.data) {
if (layer.isDisplayed.data === false) {
// The layer is not displayed...
if(!state.featureSwitchFilter.data){
if (!state.featureSwitchFilter.data) {
// ...and we cannot enable the layer control -> we skip, as these presets can never be shown anyway
continue;
continue
}
if (layer.layerDef.name === undefined) {
// this layer can never be toggled on in any case, so we skip the presets
continue;
continue
}
}
const presets = layer.layerDef.presets;
const presets = layer.layerDef.presets
for (const preset of presets) {
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
let icon: () => BaseUIElement = () => layer.layerDef.mapRendering[0].GenerateLeafletStyle(new UIEventSource<any>(tags), false).html
.SetClass("w-12 h-12 block relative");
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
let icon: () => BaseUIElement = () =>
layer.layerDef.mapRendering[0]
.GenerateLeafletStyle(new UIEventSource<any>(tags), false)
.html.SetClass("w-12 h-12 block relative")
const presetInfo: PresetInfo = {
layerToAddTo: layer,
name: preset.title,
title: preset.title,
icon: icon,
preciseInput: preset.preciseInput,
...preset
...preset,
}
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo)
button.onClick(() => {
selectedPreset.setData(presetInfo)
})
allButtons.push(button);
allButtons.push(button)
}
}
return new Combine(allButtons).SetClass("flex flex-col");
return new Combine(allButtons).SetClass("flex flex-col")
}
}
}

View file

@ -1,51 +1,68 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
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 { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
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"
export default class StatisticsPanel extends VariableUiElement {
constructor(elementsInview: UIEventSource<{ element: OsmFeature, layer: LayerConfig }[]>, state: {
layoutToUse: LayoutConfig
}) {
super(elementsInview.stabilized(1000).map(features => {
if (features === undefined) {
return new Loading("Loading data")
}
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"))
constructor(
elementsInview: UIEventSource<{ element: OsmFeature; layer: LayerConfig }[]>,
state: {
layoutToUse: LayoutConfig
}
) {
super(
elementsInview.stabilized(1000).map(
(features) => {
if (features === undefined) {
return new Loading("Loading data")
}
}
els.push(new Combine(layerStats).SetClass("flex flex-wrap"))
}
return new Combine(els)
}, [Locale.language]));
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"))
}
return new Combine(els)
},
[Locale.language]
)
)
}
}
}

View file

@ -1,62 +1,66 @@
import ChartJs from "../Base/ChartJs";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {ChartConfiguration} from 'chart.js';
import Combine from "../Base/Combine";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {Utils} from "../../Utils";
import {OsmFeature} from "../../Models/OsmFeature";
import ChartJs from "../Base/ChartJs"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { ChartConfiguration } from "chart.js"
import Combine from "../Base/Combine"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { Utils } from "../../Utils"
import { OsmFeature } from "../../Models/OsmFeature"
export interface TagRenderingChartOptions {
groupToOtherCutoff?: 3 | number,
groupToOtherCutoff?: 3 | number
sort?: boolean
}
export class StackedRenderingChart extends ChartJs {
constructor(tr: TagRenderingConfig, features: (OsmFeature & { properties: { date: string } })[], options?: {
period: "day" | "month",
groupToOtherCutoff?: 3 | number
}) {
const {labels, data} = TagRenderingChart.extractDataAndLabels(tr, features, {
constructor(
tr: TagRenderingConfig,
features: (OsmFeature & { properties: { date: string } })[],
options?: {
period: "day" | "month"
groupToOtherCutoff?: 3 | number
}
) {
const { labels, data } = TagRenderingChart.extractDataAndLabels(tr, features, {
sort: true,
groupToOtherCutoff: options?.groupToOtherCutoff
groupToOtherCutoff: options?.groupToOtherCutoff,
})
if (labels === undefined || data === undefined) {
console.error("Could not extract data and labels for ", tr, " with features", features)
throw ("No labels or data given...")
throw "No labels or data given..."
}
// labels: ["cyclofix", "buurtnatuur", ...]; data : [ ["cyclofix-changeset", "cyclofix-changeset", ...], ["buurtnatuur-cs", "buurtnatuur-cs"], ... ]
for (let i = labels.length; i >= 0; i--) {
if (data[i]?.length != 0) {
continue
}
data.splice(i, 1)
labels.splice(i, 1)
}
const datasets: { label: string /*themename*/, data: number[]/*counts per day*/, backgroundColor: string }[] = []
const datasets: {
label: string /*themename*/
data: number[] /*counts per day*/
backgroundColor: string
}[] = []
const allDays = StackedRenderingChart.getAllDays(features)
let trimmedDays = allDays.map(d => d.substr(0, 10))
let trimmedDays = allDays.map((d) => d.substr(0, 10))
if (options?.period === "month") {
trimmedDays = trimmedDays.map(d => d.substr(0, 7))
trimmedDays = trimmedDays.map((d) => d.substr(0, 7))
}
trimmedDays = Utils.Dedup(trimmedDays)
for (let i = 0; i < labels.length; i++) {
const label = labels[i];
const label = labels[i]
const changesetsForTheme = data[i]
const perDay: Record<string, OsmFeature[]> = {}
for (const changeset of changesetsForTheme) {
const csDate = new Date(changeset.properties.date)
Utils.SetMidnight(csDate)
let str = csDate.toISOString();
let str = csDate.toISOString()
str = str.substr(0, 10)
if (options?.period === "month") {
str = str.substr(0, 7);
str = str.substr(0, 7)
}
if (perDay[str] === undefined) {
perDay[str] = [changeset]
@ -67,10 +71,11 @@ export class StackedRenderingChart extends ChartJs {
const countsPerDay: number[] = []
for (let i = 0; i < trimmedDays.length; i++) {
const day = trimmedDays[i];
const day = trimmedDays[i]
countsPerDay[i] = perDay[day]?.length ?? 0
}
let backgroundColor = TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length]
let backgroundColor =
TagRenderingChart.borderColors[i % TagRenderingChart.borderColors.length]
if (label === "Unknown") {
backgroundColor = TagRenderingChart.unkownBorderColor
}
@ -80,47 +85,44 @@ export class StackedRenderingChart extends ChartJs {
datasets.push({
data: countsPerDay,
backgroundColor,
label
label,
})
}
const perDayData = {
labels: trimmedDays,
datasets
datasets,
}
const config = <ChartConfiguration>{
type: 'bar',
type: "bar",
data: perDayData,
options: {
responsive: true,
legend: {
display: false
display: false,
},
scales: {
x: {
stacked: true,
},
y: {
stacked: true
}
}
}
stacked: true,
},
},
},
}
super(config)
}
public static getAllDays(features: (OsmFeature & { properties: { date: string } })[]): string[] {
public static getAllDays(
features: (OsmFeature & { properties: { date: string } })[]
): string[] {
let earliest: Date = undefined
let latest: Date = undefined;
let allDates = new Set<string>();
let latest: Date = undefined
let allDates = new Set<string>()
features.forEach((value, key) => {
const d = new Date(value.properties.date);
const d = new Date(value.properties.date)
Utils.SetMidnight(d)
if (earliest === undefined) {
@ -147,60 +149,72 @@ export class StackedRenderingChart extends ChartJs {
}
export default class TagRenderingChart extends Combine {
public static readonly unkownColor = "rgba(128, 128, 128, 0.2)"
public static readonly unkownBorderColor = "rgba(128, 128, 128, 0.2)"
public static readonly unkownColor = 'rgba(128, 128, 128, 0.2)'
public static readonly unkownBorderColor = 'rgba(128, 128, 128, 0.2)'
public static readonly otherColor = 'rgba(128, 128, 128, 0.2)'
public static readonly otherBorderColor = 'rgba(128, 128, 255)'
public static readonly notApplicableColor = 'rgba(128, 128, 128, 0.2)'
public static readonly notApplicableBorderColor = 'rgba(255, 0, 0)'
public static readonly otherColor = "rgba(128, 128, 128, 0.2)"
public static readonly otherBorderColor = "rgba(128, 128, 255)"
public static readonly notApplicableColor = "rgba(128, 128, 128, 0.2)"
public static readonly notApplicableBorderColor = "rgba(255, 0, 0)"
public static readonly backgroundColors = [
'rgba(255, 99, 132, 0.2)',
'rgba(54, 162, 235, 0.2)',
'rgba(255, 206, 86, 0.2)',
'rgba(75, 192, 192, 0.2)',
'rgba(153, 102, 255, 0.2)',
'rgba(255, 159, 64, 0.2)'
"rgba(255, 99, 132, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(255, 206, 86, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(153, 102, 255, 0.2)",
"rgba(255, 159, 64, 0.2)",
]
public static readonly borderColors = [
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)',
'rgba(255, 206, 86, 1)',
'rgba(75, 192, 192, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)'
"rgba(255, 99, 132, 1)",
"rgba(54, 162, 235, 1)",
"rgba(255, 206, 86, 1)",
"rgba(75, 192, 192, 1)",
"rgba(153, 102, 255, 1)",
"rgba(255, 159, 64, 1)",
]
/**
* Creates a chart about this tagRendering for the given data
*/
constructor(features: { properties: Record<string, string> }[], tagRendering: TagRenderingConfig, options?: TagRenderingChartOptions & {
chartclasses?: string,
chartstyle?: string,
includeTitle?: boolean,
chartType?: "pie" | "bar" | "doughnut"
}) {
constructor(
features: { properties: Record<string, string> }[],
tagRendering: TagRenderingConfig,
options?: TagRenderingChartOptions & {
chartclasses?: string
chartstyle?: string
includeTitle?: boolean
chartType?: "pie" | "bar" | "doughnut"
}
) {
if (tagRendering.mappings?.length === 0 && tagRendering.freeform?.key === undefined) {
super([])
this.SetClass("hidden")
return;
return
}
const {labels, data} = TagRenderingChart.extractDataAndLabels(tagRendering, features, options)
const { labels, data } = TagRenderingChart.extractDataAndLabels(
tagRendering,
features,
options
)
if (labels === undefined || data === undefined) {
super([])
this.SetClass("hidden")
return
}
const borderColor = [TagRenderingChart.unkownBorderColor, TagRenderingChart.otherBorderColor, TagRenderingChart.notApplicableBorderColor]
const backgroundColor = [TagRenderingChart.unkownColor, TagRenderingChart.otherColor, TagRenderingChart.notApplicableColor]
const borderColor = [
TagRenderingChart.unkownBorderColor,
TagRenderingChart.otherBorderColor,
TagRenderingChart.notApplicableBorderColor,
]
const backgroundColor = [
TagRenderingChart.unkownColor,
TagRenderingChart.otherColor,
TagRenderingChart.notApplicableColor,
]
while (borderColor.length < data.length) {
borderColor.push(...TagRenderingChart.borderColors)
@ -216,80 +230,87 @@ export default class TagRenderingChart extends Combine {
}
}
let barchartMode = tagRendering.multiAnswer;
let barchartMode = tagRendering.multiAnswer
if (labels.length > 9) {
barchartMode = true;
barchartMode = true
}
const config = <ChartConfiguration>{
type: options.chartType ?? (barchartMode ? 'bar' : 'doughnut'),
type: options.chartType ?? (barchartMode ? "bar" : "doughnut"),
data: {
labels,
datasets: [{
data: data.map(l => l.length),
backgroundColor,
borderColor,
borderWidth: 1,
label: undefined
}]
datasets: [
{
data: data.map((l) => l.length),
backgroundColor,
borderColor,
borderWidth: 1,
label: undefined,
},
],
},
options: {
plugins: {
legend: {
display: !barchartMode
}
}
}
display: !barchartMode,
},
},
},
}
const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32");
const chart = new ChartJs(config).SetClass(options?.chartclasses ?? "w-32 h-32")
if (options.chartstyle !== undefined) {
chart.SetStyle(options.chartstyle)
}
super([
options?.includeTitle ? (tagRendering.question.Clone() ?? tagRendering.id) : undefined,
chart])
options?.includeTitle ? tagRendering.question.Clone() ?? tagRendering.id : undefined,
chart,
])
this.SetClass("block")
}
public static extractDataAndLabels<T extends { properties: Record<string, string> }>(tagRendering: TagRenderingConfig, features: T[], options?: TagRenderingChartOptions): { labels: string[], data: T[][] } {
public static extractDataAndLabels<T extends { properties: Record<string, string> }>(
tagRendering: TagRenderingConfig,
features: T[],
options?: TagRenderingChartOptions
): { labels: string[]; data: T[][] } {
const mappings = tagRendering.mappings ?? []
options = options ?? {}
let unknownCount: T[] = [];
const categoryCounts: T[][] = mappings.map(_ => [])
let unknownCount: T[] = []
const categoryCounts: T[][] = mappings.map((_) => [])
const otherCounts: Record<string, T[]> = {}
let notApplicable: T[] = [];
let notApplicable: T[] = []
for (const feature of features) {
const props = feature.properties
if (tagRendering.condition !== undefined && !tagRendering.condition.matchesProperties(props)) {
notApplicable.push(feature);
continue;
if (
tagRendering.condition !== undefined &&
!tagRendering.condition.matchesProperties(props)
) {
notApplicable.push(feature)
continue
}
if (!tagRendering.IsKnown(props)) {
unknownCount.push(feature);
continue;
unknownCount.push(feature)
continue
}
let foundMatchingMapping = false;
let foundMatchingMapping = false
if (!tagRendering.multiAnswer) {
for (let i = 0; i < mappings.length; i++) {
const mapping = mappings[i];
const mapping = mappings[i]
if (mapping.if.matchesProperties(props)) {
categoryCounts[i].push(feature)
foundMatchingMapping = true
break;
break
}
}
} else {
for (let i = 0; i < mappings.length; i++) {
const mapping = mappings[i];
const mapping = mappings[i]
if (TagUtils.MatchesMultiAnswer(mapping.if, props)) {
categoryCounts[i].push(feature)
foundMatchingMapping = true
@ -297,9 +318,12 @@ export default class TagRenderingChart extends Combine {
}
}
if (!foundMatchingMapping) {
if (tagRendering.freeform?.key !== undefined && props[tagRendering.freeform.key] !== undefined) {
if (
tagRendering.freeform?.key !== undefined &&
props[tagRendering.freeform.key] !== undefined
) {
const otherValue = props[tagRendering.freeform.key]
otherCounts[otherValue] = (otherCounts[otherValue] ?? [])
otherCounts[otherValue] = otherCounts[otherValue] ?? []
otherCounts[otherValue].push(feature)
} else {
unknownCount.push(feature)
@ -309,15 +333,15 @@ export default class TagRenderingChart extends Combine {
if (unknownCount.length + notApplicable.length === features.length) {
console.log("Returning no label nor data: all features are unkown or notApplicable")
return {labels: undefined, data: undefined}
return { labels: undefined, data: undefined }
}
let otherGrouped: T[] = [];
let otherGrouped: T[] = []
const otherLabels: string[] = []
const otherData: T[][] = []
const sortedOtherCounts: [string, T[]][] = []
for (const v in otherCounts) {
sortedOtherCounts.push([v, otherCounts[v]]);
sortedOtherCounts.push([v, otherCounts[v]])
}
if (options?.sort) {
sortedOtherCounts.sort((a, b) => b[1].length - a[1].length)
@ -327,15 +351,25 @@ export default class TagRenderingChart extends Combine {
otherLabels.push(v)
otherData.push(otherCounts[v])
} else {
otherGrouped.push(...count);
otherGrouped.push(...count)
}
}
const labels = [
"Unknown",
"Other",
"Not applicable",
...(mappings?.map((m) => m.then.txt) ?? []),
...otherLabels,
]
const data: T[][] = [
unknownCount,
otherGrouped,
notApplicable,
...categoryCounts,
...otherData,
]
const labels = ["Unknown", "Other", "Not applicable", ...mappings?.map(m => m.then.txt) ?? [], ...otherLabels]
const data: T[][] = [unknownCount, otherGrouped, notApplicable, ...categoryCounts, ...otherData]
return {labels, data}
return { labels, data }
}
}
}

View file

@ -1,18 +1,27 @@
import Combine from "../Base/Combine";
import LanguagePicker from "../LanguagePicker";
import Translations from "../i18n/Translations";
import Toggle from "../Input/Toggle";
import {SubtleButton} from "../Base/SubtleButton";
import {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 Combine from "../Base/Combine"
import LanguagePicker from "../LanguagePicker"
import Translations from "../i18n/Translations"
import Toggle from "../Input/Toggle"
import { SubtleButton } from "../Base/SubtleButton"
import { 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"
export default class ThemeIntroductionPanel extends Combine {
constructor(isShown: UIEventSource<boolean>, currentTab: UIEventSource<number>, state: { featureSwitchMoreQuests: UIEventSource<boolean>; featureSwitchAddNew: UIEventSource<boolean>; featureSwitchUserbadge: UIEventSource<boolean>; layoutToUse: LayoutConfig; osmConnection: OsmConnection }) {
constructor(
isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number>,
state: {
featureSwitchMoreQuests: UIEventSource<boolean>
featureSwitchAddNew: UIEventSource<boolean>
featureSwitchUserbadge: UIEventSource<boolean>
layoutToUse: LayoutConfig
osmConnection: OsmConnection
}
) {
const t = Translations.t.general
const layout = state.layoutToUse
@ -21,48 +30,56 @@ export default class ThemeIntroductionPanel extends Combine {
const toTheMap = new SubtleButton(
undefined,
t.openTheMap.Clone().SetClass("text-xl font-bold w-full text-center")
).onClick(() => {
isShown.setData(false)
}).SetClass("only-on-mobile")
)
.onClick(() => {
isShown.setData(false)
})
.SetClass("only-on-mobile")
const loginStatus =
new Toggle(
new LoginToggle(
undefined,
new Combine([Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold")]
).SetClass("flex flex-col"),
state
),
const loginStatus = new Toggle(
new LoginToggle(
undefined,
state.featureSwitchUserbadge
)
new Combine([
Translations.t.general.loginWithOpenStreetMap.SetClass("text-xl font-bold"),
Translations.t.general.loginOnlyNeededToEdit.Clone().SetClass("font-bold"),
]).SetClass("flex flex-col"),
state
),
undefined,
state.featureSwitchUserbadge
)
const hasPresets = layout.layers.some(l => l.presets?.length > 0)
const hasPresets = layout.layers.some((l) => l.presets?.length > 0)
super([
layout.description.Clone().SetClass("blcok mb-4"),
new Combine([
t.welcomeExplanation.general,
hasPresets ? Toggle.If( state.featureSwitchAddNew, () => t.welcomeExplanation.addNew) : undefined,
hasPresets
? Toggle.If(state.featureSwitchAddNew, () => t.welcomeExplanation.addNew)
: undefined,
]).SetClass("flex flex-col mt-2"),
toTheMap,
loginStatus.SetClass("block"),
layout.descriptionTail?.Clone().SetClass("block mt-4"),
languagePicker?.SetClass("block mt-4"),
Toggle.If(state.featureSwitchMoreQuests,
() => new Combine([
Toggle.If(state.featureSwitchMoreQuests, () =>
new Combine([
t.welcomeExplanation.browseOtherThemesIntro,
new SubtleButton(Svg.add_ui().SetClass("h-6"),t.welcomeExplanation.browseMoreMaps )
.onClick(() => currentTab.setData(FullWelcomePaneWithTabs.MoreThemesTabIndex))
.SetClass("h-12")
]).SetClass("flex flex-col mt-6")),
...layout.CustomCodeSnippets()
new SubtleButton(
Svg.add_ui().SetClass("h-6"),
t.welcomeExplanation.browseMoreMaps
)
.onClick(() =>
currentTab.setData(FullWelcomePaneWithTabs.MoreThemesTabIndex)
)
.SetClass("h-12"),
]).SetClass("flex flex-col mt-6")
),
...layout.CustomCodeSnippets(),
])
this.SetClass("link-underline")

View file

@ -1,28 +1,29 @@
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, UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
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, UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import * as native_languages from "../../assets/language_native.json"
import * as used_languages from "../../assets/generated/used_languages.json"
import BaseUIElement from "../BaseUIElement";
import BaseUIElement from "../BaseUIElement"
class TranslatorsPanelContent extends Combine {
constructor(layout: LayoutConfig, isTranslator: Store<boolean>) {
const t = Translations.t.translations
const {completeness, untranslated, total} = TranslatorsPanel.MissingTranslationsFor(layout)
const { completeness, untranslated, total } =
TranslatorsPanel.MissingTranslationsFor(layout)
const seed = t.completeness
for (const ln of Array.from(completeness.keys())) {
@ -36,127 +37,164 @@ class TranslatorsPanelContent extends Combine {
const completenessTr = {}
const completenessPercentage = {}
seed.SupportedLanguages().forEach(ln => {
seed.SupportedLanguages().forEach((ln) => {
completenessTr[ln] = "" + (completeness.get(ln) ?? 0)
completenessPercentage[ln] = "" + Math.round(100 * (completeness.get(ln) ?? 0) / total)
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"))
.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]))
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)
console.log(
"Getting untranslated string for",
language,
"raw:",
missingKeys,
"hasMissingTheme:",
hasMissingTheme,
"missingLayers:",
missingLayers
)
return [
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))
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,
total,
theme: layout.title,
percentage: new Translation(completenessPercentage),
translated: new Translation(completenessTr),
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng)
language: seed.OnEveryLanguage((_, lng) => native_languages[lng] ?? lng),
})
super([
new Title(
Translations.t.translations.activateButton,
),
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 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")
)
}))
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) {
constructor(
state: { layoutToUse: LayoutConfig; isTranslator: Store<boolean> },
iconStyle?: string
) {
const t = Translations.t.translations
super(
new Lazy(() => new TranslatorsPanelContent(state.layoutToUse, state.isTranslator)
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)),
new SubtleButton(Svg.translate_ui().SetStyle(iconStyle), t.activateButton).onClick(() =>
Locale.showLinkToWeblate.setData(true)
),
Locale.showLinkToWeblate
)
this.SetClass("hidden-on-mobile")
}
public static MissingTranslationsFor(layout: LayoutConfig): { completeness: Map<string, number>, untranslated: Map<string, string[]>, total: number } {
public static MissingTranslationsFor(layout: LayoutConfig): {
completeness: Map<string, number>
untranslated: Map<string, string[]>
total: number
} {
let total = 0
const completeness = new Map<string, number>()
const untranslated = new Map<string, string[]>()
Utils.WalkObject(layout, (o) => {
const translation = <Translation><any>o;
if (translation.translations["*"] !== undefined) {
return
}
if (translation.context === undefined || translation.context.indexOf(":") < 0) {
// no source given - lets ignore
return
}
total ++
used_languages.languages.forEach(ln => {
const trans = translation.translations
if (trans["*"] !== undefined) {
return;
Utils.WalkObject(
layout,
(o) => {
const translation = <Translation>(<any>o)
if (translation.translations["*"] !== undefined) {
return
}
if (trans[ln] === undefined) {
if (!untranslated.has(ln)) {
untranslated.set(ln, [])
}
untranslated.get(ln).push(translation.context)
}else{
completeness.set(ln, 1 + (completeness.get(ln) ?? 0))
if (translation.context === undefined || translation.context.indexOf(":") < 0) {
// no source given - lets ignore
return
}
})
}, o => {
if (o === undefined || o === null) {
return false;
}
return o instanceof Translation;
})
return {completeness, untranslated, total}
total++
used_languages.languages.forEach((ln) => {
const trans = translation.translations
if (trans["*"] !== undefined) {
return
}
if (trans[ln] === undefined) {
if (!untranslated.has(ln)) {
untranslated.set(ln, [])
}
untranslated.get(ln).push(translation.context)
} else {
completeness.set(ln, 1 + (completeness.get(ln) ?? 0))
}
})
},
(o) => {
if (o === undefined || o === null) {
return false
}
return o instanceof Translation
}
)
return { completeness, untranslated, total }
}
}

View file

@ -1,104 +1,102 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import Svg from "../../Svg";
import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import LanguagePicker from "../LanguagePicker";
import Translations from "../i18n/Translations";
import Link from "../Base/Link";
import Toggle from "../Input/Toggle";
import Img from "../Base/Img";
import MapState from "../../Logic/State/MapState";
import {LoginToggle} from "../Popup/LoginButton";
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { FixedUiElement } from "../Base/FixedUiElement"
import LanguagePicker from "../LanguagePicker"
import Translations from "../i18n/Translations"
import Link from "../Base/Link"
import Toggle from "../Input/Toggle"
import Img from "../Base/Img"
import MapState from "../../Logic/State/MapState"
import { LoginToggle } from "../Popup/LoginButton"
export default class UserBadge extends LoginToggle {
constructor(state: MapState) {
const userDetails = state.osmConnection.userDetails;
const logout =
Svg.logout_svg()
.onClick(() => {
state.osmConnection.LogOut();
});
const userDetails = state.osmConnection.userDetails
const logout = Svg.logout_svg().onClick(() => {
state.osmConnection.LogOut()
})
const userBadge = new VariableUiElement(userDetails.map(user => {
{
const homeButton = new VariableUiElement(
userDetails.map((userinfo) => {
if (userinfo.home) {
return Svg.home_svg();
const userBadge = new VariableUiElement(
userDetails.map((user) => {
{
const homeButton = new VariableUiElement(
userDetails.map((userinfo) => {
if (userinfo.home) {
return Svg.home_svg()
}
return " "
})
).onClick(() => {
const home = state.osmConnection.userDetails.data?.home
if (home === undefined) {
return
}
return " ";
state.leafletMap.data?.setView([home.lat, home.lon], 16)
})
).onClick(() => {
const home = state.osmConnection.userDetails.data?.home;
if (home === undefined) {
return;
}
state.leafletMap.data?.setView([home.lat, home.lon], 16);
});
const linkStyle = "flex items-baseline"
const languagePicker = (new LanguagePicker(state.layoutToUse.language, "") ?? new FixedUiElement(""))
.SetStyle("width:min-content;");
const linkStyle = "flex items-baseline"
const languagePicker = (
new LanguagePicker(state.layoutToUse.language, "") ?? new FixedUiElement("")
).SetStyle("width:min-content;")
let messageSpan =
new Link(
let messageSpan = new Link(
new Combine([Svg.envelope, "" + user.totalMessages]).SetClass(linkStyle),
`${user.backend}/messages/inbox`,
true
)
const csCount =
new Link(
const csCount = new Link(
new Combine([Svg.star, "" + user.csCount]).SetClass(linkStyle),
`${user.backend}/user/${user.name}/history`,
true);
if (user.unreadMessages > 0) {
messageSpan = new Link(
new Combine([Svg.envelope, "" + user.unreadMessages]),
`${user.backend}/messages/inbox`,
true
).SetClass("alert")
}
)
let dryrun = new Toggle(
new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"),
undefined,
state.featureSwitchIsTesting
)
if (user.unreadMessages > 0) {
messageSpan = new Link(
new Combine([Svg.envelope, "" + user.unreadMessages]),
`${user.backend}/messages/inbox`,
true
).SetClass("alert")
}
const settings =
new Link(Svg.gear,
let dryrun = new Toggle(
new FixedUiElement("TESTING").SetClass("alert font-xs p-0 max-h-4"),
undefined,
state.featureSwitchIsTesting
)
const settings = new Link(
Svg.gear,
`${user.backend}/user/${encodeURIComponent(user.name)}/account`,
true)
true
)
const userName = new Link(
new FixedUiElement(user.name),
`${user.backend}/user/${user.name}`,
true
)
const userName = new Link(
new FixedUiElement(user.name),
`${user.backend}/user/${user.name}`,
true);
const userStats = new Combine([
homeButton,
settings,
messageSpan,
csCount,
languagePicker,
logout,
]).SetClass("userstats")
const userStats = new Combine([
homeButton,
settings,
messageSpan,
csCount,
languagePicker,
logout
])
.SetClass("userstats")
const usertext = new Combine([
new Combine([userName, dryrun]).SetClass("flex justify-end w-full"),
userStats
]).SetClass("flex flex-col sm:w-auto sm:pl-2 overflow-hidden w-0")
const userIcon =
(user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img)).SetClass("rounded-full opacity-0 m-0 p-0 duration-500 w-16 min-width-16 h16 float-left")
const usertext = new Combine([
new Combine([userName, dryrun]).SetClass("flex justify-end w-full"),
userStats,
]).SetClass("flex flex-col sm:w-auto sm:pl-2 overflow-hidden w-0")
const userIcon = (
user.img === undefined ? Svg.osm_logo_ui() : new Img(user.img)
)
.SetClass(
"rounded-full opacity-0 m-0 p-0 duration-500 w-16 min-width-16 h16 float-left"
)
.onClick(() => {
if (usertext.HasClass("w-0")) {
usertext.RemoveClass("w-0")
@ -110,23 +108,17 @@ export default class UserBadge extends LoginToggle {
}
})
return new Combine([
usertext,
userIcon,
]).SetClass("h-16 flex bg-white")
return new Combine([usertext, userIcon]).SetClass("h-16 flex bg-white")
}
})
)
}
}));
super(
new Combine([userBadge.SetClass("inline-block m-0 w-full").SetStyle("pointer-events: all")])
.SetClass("shadow rounded-full h-min overflow-hidden block w-full md:w-max"),
new Combine([
userBadge.SetClass("inline-block m-0 w-full").SetStyle("pointer-events: all"),
]).SetClass("shadow rounded-full h-min overflow-hidden block w-full md:w-max"),
Translations.t.general.loginWithOpenStreetMap,
state
)
}
}