forked from MapComplete/MapComplete
Reformat all files with prettier
This commit is contained in:
parent
e22d189376
commit
b541d3eab4
382 changed files with 50893 additions and 35566 deletions
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}))
|
||||
|
||||
}
|
||||
))
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'>
|
||||
<iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${layout.title?.txt ?? "MapComplete"} with MapComplete"></iframe>
|
||||
<iframe src="${url}" allow="geolocation" width="100%" height="100%" style="min-width: 250px; min-height: 250px" title="${
|
||||
layout.title?.txt ?? "MapComplete"
|
||||
} with MapComplete"></iframe>
|
||||
</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")
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue