forked from MapComplete/MapComplete
Merge branch 'develop'
This commit is contained in:
commit
4876b8f426
137 changed files with 30516 additions and 1815 deletions
|
@ -3,8 +3,11 @@ import Locale from "../i18n/Locale"
|
|||
import Link from "./Link"
|
||||
import Svg from "../../Svg"
|
||||
|
||||
/**
|
||||
* The little 'translate'-icon next to every icon + some static helper functions
|
||||
*/
|
||||
export default class LinkToWeblate extends VariableUiElement {
|
||||
private static URI: any
|
||||
|
||||
constructor(context: string, availableTranslations: object) {
|
||||
super(
|
||||
Locale.language.map(
|
||||
|
|
|
@ -3,6 +3,7 @@ import Loc from "../../Models/Loc"
|
|||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import {deprecate} from "util";
|
||||
|
||||
export interface MinimapOptions {
|
||||
background?: UIEventSource<BaseLayer>
|
||||
|
@ -24,7 +25,10 @@ export interface MinimapObj {
|
|||
|
||||
installBounds(factor: number | BBox, showRange?: boolean): void
|
||||
|
||||
TakeScreenshot(): Promise<any>
|
||||
TakeScreenshot(format): Promise<string>
|
||||
TakeScreenshot(format: "image"): Promise<string>
|
||||
TakeScreenshot(format:"blob"): Promise<Blob>
|
||||
TakeScreenshot(format?: "image" | "blob"): Promise<string | Blob>
|
||||
}
|
||||
|
||||
export default class Minimap {
|
||||
|
|
|
@ -109,10 +109,27 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
|
|||
mp.remove()
|
||||
}
|
||||
|
||||
public async TakeScreenshot() {
|
||||
/**
|
||||
* Takes a screenshot of the current map
|
||||
* @param format: image: give a base64 encoded png image;
|
||||
* @constructor
|
||||
*/
|
||||
public async TakeScreenshot(): Promise<string> ;
|
||||
public async TakeScreenshot(format: "image"): Promise<string> ;
|
||||
public async TakeScreenshot(format: "blob"): Promise<Blob> ;
|
||||
public async TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> ;
|
||||
public async TakeScreenshot(format: "image" | "blob" = "image"): Promise<string | Blob> {
|
||||
console.log("Taking a screenshot...")
|
||||
const screenshotter = new SimpleMapScreenshoter()
|
||||
screenshotter.addTo(this.leafletMap.data)
|
||||
return await screenshotter.takeScreen("image")
|
||||
const result = <any> await screenshotter.takeScreen((<any> format) ?? "image")
|
||||
if(format === "image" && typeof result === "string"){
|
||||
return result
|
||||
}
|
||||
if(format === "blob" && result instanceof Blob){
|
||||
return result
|
||||
}
|
||||
throw "Something went wrong while creating the screenshot: "+result
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
|
|
|
@ -148,7 +148,7 @@ export default abstract class BaseUIElement {
|
|||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
if (domExc) {
|
||||
console.log("An exception occured", domExc.code, domExc.message, domExc.name)
|
||||
console.error("An exception occured", domExc.code, domExc.message, domExc.name, domExc)
|
||||
}
|
||||
console.error(e)
|
||||
}
|
||||
|
|
347
UI/BigComponents/PdfExportGui.ts
Normal file
347
UI/BigComponents/PdfExportGui.ts
Normal file
|
@ -0,0 +1,347 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep";
|
||||
import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf";
|
||||
import {FixedInputElement} from "../Input/FixedInputElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import FileSelectorButton from "../Input/FileSelectorButton";
|
||||
import InputElementMap from "../Input/InputElementMap";
|
||||
import {RadioButton} from "../Input/RadioButton";
|
||||
import {Utils} from "../../Utils";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Img from "../Base/Img";
|
||||
import Title from "../Base/Title";
|
||||
import {CheckBox} from "../Input/Checkboxes";
|
||||
import Minimap from "../Base/Minimap";
|
||||
import SearchAndGo from "./SearchAndGo";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import List from "../Base/List";
|
||||
import LeftIndex from "../Base/LeftIndex";
|
||||
import Constants from "../../Models/Constants";
|
||||
import Toggleable from "../Base/Toggleable";
|
||||
import Lazy from "../Base/Lazy";
|
||||
import LinkToWeblate from "../Base/LinkToWeblate";
|
||||
import Link from "../Base/Link";
|
||||
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
|
||||
import * as languages from "../../assets/language_translations.json"
|
||||
import {Translation} from "../i18n/Translation";
|
||||
|
||||
class SelectTemplate extends Combine implements FlowStep<{ title: string, pages: string[] }> {
|
||||
readonly IsValid: Store<boolean>;
|
||||
readonly Value: Store<{ title: string, pages: string[] }>;
|
||||
|
||||
constructor() {
|
||||
const elements: InputElement<{ templateName: string, pages: string[] }>[] = []
|
||||
for (const templateName in SvgToPdf.templates) {
|
||||
const template = SvgToPdf.templates[templateName]
|
||||
elements.push(new FixedInputElement(
|
||||
new Combine([new FixedUiElement(templateName).SetClass("font-bold pr-2"),
|
||||
template.description
|
||||
])
|
||||
, new UIEventSource({templateName, pages: template.pages})))
|
||||
}
|
||||
|
||||
const file = new FileSelectorButton(new FixedUiElement("Select an svg image which acts as template"), {
|
||||
acceptType: "image/svg+xml",
|
||||
allowMultiple: true
|
||||
})
|
||||
const fileMapped = new InputElementMap<FileList, { templateName: string, pages: string[], fromFile: true }>(file, (x0, x1) => x0 === x1,
|
||||
(filelist) => {
|
||||
if (filelist === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const pages = []
|
||||
let templateName: string = undefined;
|
||||
for (const file of Array.from(filelist)) {
|
||||
|
||||
if (templateName == undefined) {
|
||||
templateName = file.name.substring(file.name.lastIndexOf("/") + 1)
|
||||
templateName = templateName.substring(0, templateName.lastIndexOf("."))
|
||||
}
|
||||
pages.push(file.text())
|
||||
}
|
||||
|
||||
return {
|
||||
templateName,
|
||||
pages,
|
||||
fromFile: true
|
||||
}
|
||||
|
||||
},
|
||||
_ => undefined
|
||||
)
|
||||
elements.push(fileMapped)
|
||||
const radio = new RadioButton(elements, {selectFirstAsDefault: true})
|
||||
|
||||
const loaded: Store<{ success: { title: string, pages: string[] } } | { error: any }> = radio.GetValue().bind(template => {
|
||||
if (template === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (template["fromFile"]) {
|
||||
return UIEventSource.FromPromiseWithErr(Promise.all(template.pages).then(pages => ({
|
||||
title: template.templateName,
|
||||
pages
|
||||
})))
|
||||
}
|
||||
const urls = template.pages.map(p => SelectTemplate.ToUrl(p))
|
||||
const dloadAll: Promise<{ title: string, pages: string[] }> = Promise.all(urls.map(url => Utils.download(url))).then(pages => ({
|
||||
pages,
|
||||
title: template.templateName
|
||||
}))
|
||||
|
||||
return UIEventSource.FromPromiseWithErr(dloadAll)
|
||||
})
|
||||
const preview = new VariableUiElement(
|
||||
loaded.map(pages => {
|
||||
if (pages === undefined) {
|
||||
return new Loading()
|
||||
}
|
||||
if (pages["error"] !== undefined) {
|
||||
return new FixedUiElement("Loading preview failed: " + pages["err"]).SetClass("alert")
|
||||
}
|
||||
const svgs = pages["success"].pages
|
||||
if (svgs.length === 0) {
|
||||
return new FixedUiElement("No pages loaded...").SetClass("alert")
|
||||
}
|
||||
const els: BaseUIElement[] = []
|
||||
for (const pageSrc of svgs) {
|
||||
const el = new Img(pageSrc, true)
|
||||
.SetClass("w-96 m-2 border-black border-2")
|
||||
els.push(el)
|
||||
}
|
||||
return new Combine(els).SetClass("flex border border-subtle rounded-xl");
|
||||
})
|
||||
)
|
||||
|
||||
super([
|
||||
new Title("Select template"),
|
||||
radio,
|
||||
new Title("Preview"),
|
||||
preview
|
||||
]);
|
||||
this.Value = loaded.map(l => l === undefined ? undefined : l["success"])
|
||||
this.IsValid = this.Value.map(v => v !== undefined)
|
||||
}
|
||||
|
||||
public static ToUrl(spec: string) {
|
||||
if (spec.startsWith("http")) {
|
||||
return spec
|
||||
}
|
||||
let path = window.location.pathname
|
||||
path = path.substring(0, path.lastIndexOf("/"))
|
||||
return window.location.protocol + "//" + window.location.host + path + "/" + spec
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SelectPdfOptions extends Combine implements FlowStep<{ title: string, pages: string[], options: SvgToPdfOptions }> {
|
||||
readonly IsValid: Store<boolean>;
|
||||
readonly Value: Store<{ title: string, pages: string[], options: SvgToPdfOptions }>;
|
||||
|
||||
constructor(title: string, pages: string[], getFreeDiv: () => string) {
|
||||
const dummy = new CheckBox("Don't add data to the map (to quickly preview the PDF)", false)
|
||||
const overrideMapLocation = new CheckBox("Override map location: use a selected location instead of the location set in the template", false)
|
||||
const locationInput = Minimap.createMiniMap().SetClass("block w-full")
|
||||
const searchField = new SearchAndGo({leafletMap: locationInput.leafletMap})
|
||||
const selectLocation =
|
||||
new Combine([
|
||||
new Toggle(new Combine([new Title("Select override location"), searchField]).SetClass("flex"), undefined, overrideMapLocation.GetValue()),
|
||||
new Toggle(locationInput.SetStyle("height: 20rem"), undefined, overrideMapLocation.GetValue()).SetStyle("height: 20rem")
|
||||
]).SetClass("block").SetStyle("height: 25rem")
|
||||
super([new Title("Select options"),
|
||||
dummy,
|
||||
overrideMapLocation,
|
||||
selectLocation
|
||||
]);
|
||||
this.Value = dummy.GetValue().map((disableMaps) => {
|
||||
return {
|
||||
pages,
|
||||
title,
|
||||
options: <SvgToPdfOptions>{
|
||||
disableMaps,
|
||||
getFreeDiv,
|
||||
overrideLocation: overrideMapLocation.GetValue().data ? locationInput.location.data : undefined
|
||||
}
|
||||
}
|
||||
}, [overrideMapLocation.GetValue(), locationInput.location])
|
||||
this.IsValid = new ImmutableStore(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf, languages: string[] }> {
|
||||
readonly IsValid: Store<boolean>;
|
||||
readonly Value: Store<{ svgToPdf: SvgToPdf, languages: string[] }>;
|
||||
|
||||
constructor(title: string, pages: string[], options: SvgToPdfOptions) {
|
||||
const svgToPdf = new SvgToPdf(title, pages, options)
|
||||
const languageOptions = [
|
||||
new FixedInputElement("Nederlands", "nl"),
|
||||
new FixedInputElement("English", "en")
|
||||
]
|
||||
const langs: string[] = Array.from(Object.keys(languages["default"] ?? languages))
|
||||
console.log("Available languages are:", langs)
|
||||
const languageSelector = new SearchablePillsSelector(
|
||||
langs.map(l => ({
|
||||
show: new Translation(languages[l]),
|
||||
value: l,
|
||||
mainTerm: languages[l]
|
||||
})), {
|
||||
mode: "select-many"
|
||||
}
|
||||
)
|
||||
|
||||
const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare())
|
||||
|
||||
super([
|
||||
new Title("Select languages..."),
|
||||
languageSelector,
|
||||
new Toggle(
|
||||
new Loading("Preparing maps..."),
|
||||
undefined,
|
||||
isPrepared.map(p => p === undefined)
|
||||
)
|
||||
]);
|
||||
this.Value = isPrepared.map(isPrepped => {
|
||||
if (isPrepped === undefined) {
|
||||
return undefined
|
||||
}
|
||||
if (isPrepped["success"] !== undefined) {
|
||||
const svgToPdf = isPrepped["success"]
|
||||
const langs = languageSelector.GetValue().data
|
||||
console.log("Languages are", langs)
|
||||
if (langs.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return {svgToPdf, languages: langs}
|
||||
}
|
||||
return undefined;
|
||||
}, [languageSelector.GetValue()])
|
||||
this.IsValid = this.Value.map(v => v !== undefined)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class InspectStrings extends Toggle implements FlowStep<{ svgToPdf: SvgToPdf, languages: string[] }> {
|
||||
readonly IsValid: Store<boolean>;
|
||||
readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }>;
|
||||
|
||||
constructor(svgToPdf: SvgToPdf, languages: string[]) {
|
||||
|
||||
const didLoadLanguages = UIEventSource.FromPromiseWithErr(svgToPdf.PrepareLanguages(languages)).map(l => l !== undefined && l["success"] !== undefined)
|
||||
|
||||
super(new Combine([
|
||||
new Title("Inspect translation strings"),
|
||||
...languages.map(l => new Lazy(() => InspectStrings.createOverviewPanel(svgToPdf, l)))
|
||||
]),
|
||||
new Loading(),
|
||||
didLoadLanguages
|
||||
);
|
||||
this.Value = new ImmutableStore({svgToPdf, languages})
|
||||
this.IsValid = didLoadLanguages
|
||||
}
|
||||
|
||||
private static createOverviewPanel(svgToPdf: SvgToPdf, language: string): BaseUIElement {
|
||||
const elements: BaseUIElement[] = []
|
||||
let foundTranslations = 0
|
||||
const allKeys = Array.from(svgToPdf.translationKeys())
|
||||
for (const translationKey of allKeys) {
|
||||
let spec = translationKey
|
||||
if (translationKey.startsWith("layer.")) {
|
||||
spec = "layers:" + translationKey.substring(6)
|
||||
} else {
|
||||
spec = "core:" + translationKey
|
||||
}
|
||||
const translated = svgToPdf.getTranslation("$" + translationKey, language, true)
|
||||
if (translated) {
|
||||
foundTranslations++
|
||||
}
|
||||
const linkToWeblate = new Link(spec, LinkToWeblate.hrefToWeblate(language, spec), true).SetClass("font-bold link-underline")
|
||||
elements.push(new Combine([
|
||||
linkToWeblate,
|
||||
" ",
|
||||
translated ?? new FixedUiElement("No translation found!").SetClass("alert")
|
||||
|
||||
]))
|
||||
}
|
||||
|
||||
return new Toggleable(
|
||||
new Title("Translations for " + language),
|
||||
new Combine([
|
||||
`${foundTranslations}/${allKeys.length} of translations are found (${Math.floor(100 * foundTranslations / allKeys.length)}%)`,
|
||||
"The following keys are used:",
|
||||
new List(elements)
|
||||
]),
|
||||
{closeOnClick: false, height: "15rem"})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class SavePdf extends Combine {
|
||||
|
||||
constructor(svgToPdf: SvgToPdf, languages: string[]) {
|
||||
|
||||
super([
|
||||
new Title("Generating your pdfs..."),
|
||||
new List(languages.map(lng => new Toggle(
|
||||
lng + " is done!",
|
||||
new Loading("Creating pdf for " + lng),
|
||||
UIEventSource.FromPromiseWithErr(svgToPdf.ConvertSvg(lng).then(() => true))
|
||||
.map(x => x !== undefined && x["success"] === true)
|
||||
)))
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class PdfExportGui extends LeftIndex {
|
||||
|
||||
|
||||
constructor(freeDivId: string) {
|
||||
|
||||
let i = 0
|
||||
const createDiv = (): string => {
|
||||
const div = document.createElement("div")
|
||||
div.id = "freediv-" + (i++)
|
||||
document.getElementById(freeDivId).append(div)
|
||||
return div.id
|
||||
}
|
||||
|
||||
Constants.defaultOverpassUrls.splice(0, 1)
|
||||
const {flow, furthestStep, titles} = FlowPanelFactory.start(
|
||||
new Title("Select template"), new SelectTemplate()
|
||||
).then(new Title("Select options"), ({title, pages}) => new SelectPdfOptions(title, pages, createDiv))
|
||||
.then("Generate maps...", ({title, pages, options}) => new PreparePdf(title, pages, options))
|
||||
.then("Inspect translations", (({svgToPdf, languages}) => new InspectStrings(svgToPdf, languages)))
|
||||
.finish("Generating...", ({svgToPdf, languages}) => new SavePdf(svgToPdf, languages))
|
||||
|
||||
|
||||
const toc = new List(
|
||||
titles.map(
|
||||
(title, i) =>
|
||||
new VariableUiElement(
|
||||
furthestStep.map((currentStep) => {
|
||||
if (i > currentStep) {
|
||||
return new Combine([title]).SetClass("subtle")
|
||||
}
|
||||
if (i == currentStep) {
|
||||
return new Combine([title]).SetClass("font-bold")
|
||||
}
|
||||
if (i < currentStep) {
|
||||
return title
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
true
|
||||
)
|
||||
|
||||
const leftContents: BaseUIElement[] = [
|
||||
toc
|
||||
].map((el) => el?.SetClass("pl-4"))
|
||||
|
||||
super(leftContents, flow)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import Combine from "../Base/Combine"
|
|||
import Locale from "../i18n/Locale"
|
||||
|
||||
export default class SearchAndGo extends Combine {
|
||||
constructor(state: { leafletMap: UIEventSource<any>; selectedElement: UIEventSource<any> }) {
|
||||
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)
|
||||
|
@ -63,7 +63,7 @@ export default class SearchAndGo extends Combine {
|
|||
[bb[0], bb[2]],
|
||||
[bb[1], bb[3]],
|
||||
]
|
||||
state.selectedElement.setData(undefined)
|
||||
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)
|
||||
|
|
|
@ -26,6 +26,7 @@ import BaseLayer from "../../Models/BaseLayer"
|
|||
import Loading from "../Base/Loading"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import {WayId} from "../../Models/OsmFeature";
|
||||
|
||||
/*
|
||||
* The SimpleAddUI is a single panel, which can have multiple states:
|
||||
|
@ -123,13 +124,13 @@ export default class SimpleAddUI extends Toggle {
|
|||
function confirm(
|
||||
tags: any[],
|
||||
location: { lat: number; lon: number },
|
||||
snapOntoWayId?: string
|
||||
snapOntoWayId?: WayId
|
||||
) {
|
||||
if (snapOntoWayId === undefined) {
|
||||
createNewPoint(tags, location, undefined)
|
||||
} else {
|
||||
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD((way) => {
|
||||
createNewPoint(tags, location, <OsmWay>way)
|
||||
createNewPoint(tags, location, way)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ 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 { Store } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import * as native_languages from "../../assets/language_native.json"
|
||||
|
@ -89,8 +89,6 @@ class TranslatorsPanelContent extends Combine {
|
|||
]
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// "translationCompleteness": "Translations for {theme} in {language} are at {percentage}: {translated} out of {total}",
|
||||
const translated = seed.Subs({
|
||||
total,
|
||||
|
|
|
@ -70,7 +70,7 @@ export default class ExportPDF {
|
|||
console.error(e)
|
||||
self.cleanup()
|
||||
}
|
||||
}, 500),
|
||||
}, 500)
|
||||
})
|
||||
|
||||
minimap.SetStyle(
|
||||
|
@ -166,7 +166,7 @@ export default class ExportPDF {
|
|||
// Add the logo of the layout
|
||||
let img = document.createElement("img")
|
||||
const imgSource = layout.icon
|
||||
const imgType = imgSource.substr(imgSource.lastIndexOf(".") + 1)
|
||||
const imgType = imgSource.substring(imgSource.lastIndexOf(".") + 1)
|
||||
img.src = imgSource
|
||||
if (imgType.toLowerCase() === "svg") {
|
||||
new FixedUiElement("").AttachTo(this.freeDivId)
|
||||
|
|
|
@ -3,11 +3,12 @@ import { UIEventSource } from "../../Logic/UIEventSource"
|
|||
import { Utils } from "../../Utils"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import InputElementMap from "./InputElementMap"
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
export class CheckBox extends InputElementMap<number[], boolean> {
|
||||
constructor(el: BaseUIElement, defaultValue?: boolean) {
|
||||
constructor(el: (BaseUIElement | string), defaultValue?: boolean) {
|
||||
super(
|
||||
new CheckBoxes([el]),
|
||||
new CheckBoxes([Translations.W(el)]),
|
||||
(x0, x1) => x0 === x1,
|
||||
(t) => t.length > 0,
|
||||
(x) => (x ? [0] : [])
|
||||
|
|
|
@ -1,44 +1,55 @@
|
|||
import { ReadonlyInputElement } from "./InputElement"
|
||||
import {ReadonlyInputElement} from "./InputElement"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Minimap, { MinimapObj } from "../Base/Minimap"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import Minimap, {MinimapObj} from "../Base/Minimap"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import State from "../../State"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import {BBox} from "../../Logic/BBox"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "./Toggle"
|
||||
import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||
import FilteredLayer from "../../Models/FilteredLayer";
|
||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
|
||||
import {RelationId, WayId} from "../../Models/OsmFeature";
|
||||
import {Feature, LineString, Polygon} from "geojson";
|
||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||
|
||||
export default class LocationInput
|
||||
extends BaseUIElement
|
||||
implements ReadonlyInputElement<Loc>, MinimapObj
|
||||
{
|
||||
implements ReadonlyInputElement<Loc>, MinimapObj {
|
||||
private static readonly matchLayer = new LayerConfig(
|
||||
matchpoint,
|
||||
"LocationInput.matchpoint",
|
||||
true
|
||||
)
|
||||
|
||||
public readonly snappedOnto: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
public readonly snappedOnto: UIEventSource<Feature & { properties : { id : WayId} }> = new UIEventSource(undefined)
|
||||
public readonly _matching_layer: LayerConfig
|
||||
public readonly leafletMap: UIEventSource<any>
|
||||
public readonly bounds
|
||||
public readonly location
|
||||
private _centerLocation: UIEventSource<Loc>
|
||||
private readonly _centerLocation: UIEventSource<Loc>
|
||||
private readonly mapBackground: UIEventSource<BaseLayer>
|
||||
/**
|
||||
* The features to which the input should be snapped
|
||||
* @private
|
||||
*/
|
||||
private readonly _snapTo: Store<{ feature: any }[]>
|
||||
private readonly _snapTo: Store< (Feature<LineString | Polygon> & {properties: {id : WayId}})[]>
|
||||
/**
|
||||
* The features to which the input should be snapped without cleanup of relations and memberships
|
||||
* Used for rendering
|
||||
* @private
|
||||
*/
|
||||
private readonly _snapToRaw: Store< {feature: Feature}[]>
|
||||
private readonly _value: Store<Loc>
|
||||
private readonly _snappedPoint: Store<any>
|
||||
private readonly _maxSnapDistance: number
|
||||
|
@ -47,33 +58,80 @@ export default class LocationInput
|
|||
private readonly map: BaseUIElement & MinimapObj
|
||||
private readonly clickLocation: UIEventSource<Loc>
|
||||
private readonly _minZoom: number
|
||||
private readonly _state: {
|
||||
readonly filteredLayers: Store<FilteredLayer[]>;
|
||||
readonly backgroundLayer: UIEventSource<BaseLayer>;
|
||||
readonly layoutToUse: LayoutConfig;
|
||||
readonly selectedElement: UIEventSource<any>;
|
||||
readonly allElements: ElementStorage
|
||||
}
|
||||
|
||||
constructor(options: {
|
||||
/**
|
||||
* Given a list of geojson-features, will prepare these features to be snappable:
|
||||
* - points are removed
|
||||
* - LineStrings are passed as-is
|
||||
* - Multipolygons are decomposed into their member ways by downloading them
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private static async prepareSnapOnto(features: Feature[]): Promise<(Feature<LineString | Polygon> & {properties : {id: WayId}})[]> {
|
||||
const linesAndPolygon : Feature<LineString | Polygon>[] = <any> features.filter(f => f.geometry.type !== "Point")
|
||||
// Clean the features: multipolygons are split into their it's members
|
||||
const linestrings : (Feature<LineString | Polygon> & {properties: {id: WayId}})[] = []
|
||||
for (const feature of linesAndPolygon) {
|
||||
if(feature.properties.id.startsWith("way")){
|
||||
// A normal way - we continue
|
||||
linestrings.push(<any> feature)
|
||||
continue
|
||||
}
|
||||
|
||||
// We have a multipolygon, thus: a relation
|
||||
// Download the members
|
||||
const relation = await OsmObject.DownloadObjectAsync(<RelationId> feature.properties.id, 60 * 60)
|
||||
const members: OsmWay[] = await Promise.all(relation.members
|
||||
.filter(m => m.type === "way")
|
||||
.map(m => OsmObject.DownloadObjectAsync(<WayId> ("way/"+m.ref), 60 * 60)))
|
||||
linestrings.push(...members.map(m => m.asGeoJson()))
|
||||
}
|
||||
return linestrings
|
||||
|
||||
}
|
||||
|
||||
constructor(options?: {
|
||||
minZoom?: number
|
||||
mapBackground?: UIEventSource<BaseLayer>
|
||||
snapTo?: UIEventSource<{ feature: any }[]>
|
||||
snapTo?: UIEventSource<{ feature: Feature }[]>
|
||||
maxSnapDistance?: number
|
||||
snappedPointTags?: any
|
||||
requiresSnapping?: boolean
|
||||
centerLocation: UIEventSource<Loc>
|
||||
centerLocation?: UIEventSource<Loc>
|
||||
bounds?: UIEventSource<BBox>
|
||||
state?: {
|
||||
readonly filteredLayers: Store<FilteredLayer[]>;
|
||||
readonly backgroundLayer: UIEventSource<BaseLayer>;
|
||||
readonly layoutToUse: LayoutConfig;
|
||||
readonly selectedElement: UIEventSource<any>;
|
||||
readonly allElements: ElementStorage
|
||||
}
|
||||
}) {
|
||||
super()
|
||||
this._snapTo = options.snapTo?.map((features) =>
|
||||
features?.filter((feat) => feat.feature.geometry.type !== "Point")
|
||||
)
|
||||
this._maxSnapDistance = options.maxSnapDistance
|
||||
this._centerLocation = options.centerLocation
|
||||
this._snappedPointTags = options.snappedPointTags
|
||||
this._bounds = options.bounds
|
||||
this._minZoom = options.minZoom
|
||||
this._snapToRaw = options?.snapTo?.map(feats => feats.filter(f => f.feature.geometry.type !== "Point"))
|
||||
this._snapTo = options?.snapTo?.bind((features) => UIEventSource.FromPromise(LocationInput.prepareSnapOnto(features.map(f => f.feature))))?.map(f => f ?? [])
|
||||
this._maxSnapDistance = options?.maxSnapDistance
|
||||
this._centerLocation = options?.centerLocation ?? new UIEventSource<Loc>({
|
||||
lat: 0, lon: 0, zoom: 0
|
||||
})
|
||||
this._snappedPointTags = options?.snappedPointTags
|
||||
this._bounds = options?.bounds
|
||||
this._minZoom = options?.minZoom
|
||||
this._state = options?.state
|
||||
if (this._snapTo === undefined) {
|
||||
this._value = this._centerLocation
|
||||
} else {
|
||||
const self = this
|
||||
|
||||
if (self._snappedPointTags !== undefined) {
|
||||
const layout = State.state.layoutToUse
|
||||
const layout = this._state.layoutToUse
|
||||
|
||||
let matchingLayer = LocationInput.matchLayer
|
||||
for (const layer of layout.layers) {
|
||||
|
@ -86,36 +144,39 @@ export default class LocationInput
|
|||
this._matching_layer = LocationInput.matchLayer
|
||||
}
|
||||
|
||||
this._snappedPoint = options.centerLocation.map(
|
||||
// Calculate the location of the point based by snapping it onto a way
|
||||
// As a side-effect, the actual snapped-onto way (if any) is saved into 'snappedOnto'
|
||||
this._snappedPoint = this._centerLocation.map(
|
||||
(loc) => {
|
||||
if (loc === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
||||
// We reproject the location onto every 'snap-to-feature' and select the closest
|
||||
|
||||
let min = undefined
|
||||
let matchedWay = undefined
|
||||
let matchedWay: Feature<LineString | Polygon> & {properties : {id : WayId}} = undefined
|
||||
for (const feature of self._snapTo.data ?? []) {
|
||||
try {
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [
|
||||
const nearestPointOnLine = GeoOperations.nearestPoint(feature, [
|
||||
loc.lon,
|
||||
loc.lat,
|
||||
])
|
||||
if (min === undefined) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature
|
||||
matchedWay = feature
|
||||
continue
|
||||
}
|
||||
|
||||
if (min.properties.dist > nearestPointOnLine.properties.dist) {
|
||||
min = nearestPointOnLine
|
||||
matchedWay = feature.feature
|
||||
matchedWay = feature
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"Snapping to a nearest point failed for ",
|
||||
feature.feature,
|
||||
feature,
|
||||
"due to ",
|
||||
e
|
||||
)
|
||||
|
@ -123,18 +184,25 @@ export default class LocationInput
|
|||
}
|
||||
|
||||
if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) {
|
||||
if (options.requiresSnapping) {
|
||||
if (options?.requiresSnapping) {
|
||||
return undefined
|
||||
} else {
|
||||
// No match found - the original coordinates are returned as is
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: options.snappedPointTags ?? min.properties,
|
||||
geometry: { type: "Point", coordinates: [loc.lon, loc.lat] },
|
||||
properties: options?.snappedPointTags ?? min.properties,
|
||||
geometry: {type: "Point", coordinates: [loc.lon, loc.lat]},
|
||||
}
|
||||
}
|
||||
}
|
||||
min.properties = options.snappedPointTags ?? min.properties
|
||||
self.snappedOnto.setData(matchedWay)
|
||||
min.properties = options?.snappedPointTags ?? min.properties
|
||||
if(matchedWay.properties.id.startsWith("relation/")){
|
||||
// We matched a relation instead of a way
|
||||
console.log("Snapping onto a relation. The relation is", matchedWay)
|
||||
|
||||
|
||||
}
|
||||
self.snappedOnto.setData(<any> matchedWay)
|
||||
return min
|
||||
},
|
||||
[this._snapTo]
|
||||
|
@ -149,14 +217,14 @@ export default class LocationInput
|
|||
}
|
||||
})
|
||||
}
|
||||
this.mapBackground = options.mapBackground ?? State.state?.backgroundLayer
|
||||
this.mapBackground = options?.mapBackground ?? this._state?.backgroundLayer ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto)
|
||||
this.SetClass("block h-full")
|
||||
|
||||
this.clickLocation = new UIEventSource<Loc>(undefined)
|
||||
this.map = Minimap.createMiniMap({
|
||||
location: this._centerLocation,
|
||||
background: this.mapBackground,
|
||||
attribution: this.mapBackground !== State.state?.backgroundLayer,
|
||||
attribution: this.mapBackground !== this._state?.backgroundLayer,
|
||||
lastClickLocation: this.clickLocation,
|
||||
bounds: this._bounds,
|
||||
addLayerControl: true,
|
||||
|
@ -177,15 +245,11 @@ export default class LocationInput
|
|||
this.map.installBounds(factor, showRange)
|
||||
}
|
||||
|
||||
TakeScreenshot(): Promise<any> {
|
||||
return this.map.TakeScreenshot()
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
try {
|
||||
const self = this
|
||||
const hasMoved = new UIEventSource(false)
|
||||
const startLocation = { ...this._centerLocation.data }
|
||||
const startLocation = {...this._centerLocation.data}
|
||||
this._centerLocation.addCallbackD((newLocation) => {
|
||||
const f = 100000
|
||||
console.log(newLocation.lon, startLocation.lon)
|
||||
|
@ -201,21 +265,21 @@ export default class LocationInput
|
|||
this.clickLocation.addCallbackAndRunD((location) =>
|
||||
this._centerLocation.setData(location)
|
||||
)
|
||||
if (this._snapTo !== undefined) {
|
||||
if (this._snapToRaw !== undefined) {
|
||||
// Show the lines to snap to
|
||||
console.log("Constructing the snap-to layer", this._snapTo)
|
||||
console.log("Constructing the snap-to layer", this._snapToRaw)
|
||||
new ShowDataMultiLayer({
|
||||
features: StaticFeatureSource.fromDateless(this._snapTo),
|
||||
features: StaticFeatureSource.fromDateless(this._snapToRaw),
|
||||
zoomToFeatures: false,
|
||||
leafletMap: this.map.leafletMap,
|
||||
layers: State.state.filteredLayers,
|
||||
layers: this._state.filteredLayers,
|
||||
})
|
||||
// Show the central point
|
||||
const matchPoint = this._snappedPoint.map((loc) => {
|
||||
if (loc === undefined) {
|
||||
return []
|
||||
}
|
||||
return [{ feature: loc }]
|
||||
return [{feature: loc}]
|
||||
})
|
||||
console.log("Constructing the match layer", matchPoint)
|
||||
|
||||
|
@ -224,8 +288,8 @@ export default class LocationInput
|
|||
zoomToFeatures: false,
|
||||
leafletMap: this.map.leafletMap,
|
||||
layerToShow: this._matching_layer,
|
||||
state: State.state,
|
||||
selectedElement: State.state.selectedElement,
|
||||
state: this._state,
|
||||
selectedElement: this._state.selectedElement,
|
||||
})
|
||||
}
|
||||
this.mapBackground.map(
|
||||
|
@ -270,4 +334,11 @@ export default class LocationInput
|
|||
.ConstructElement()
|
||||
}
|
||||
}
|
||||
|
||||
TakeScreenshot(format: "image"): Promise<string>;
|
||||
TakeScreenshot(format: "blob"): Promise<Blob>;
|
||||
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>;
|
||||
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> {
|
||||
return this.map.TakeScreenshot(format)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,16 +10,15 @@ import Toggle from "./Input/Toggle"
|
|||
|
||||
export default class LanguagePicker extends Toggle {
|
||||
constructor(languages: string[], label: string | BaseUIElement = "") {
|
||||
console.log("Constructing a language pîcker for languages", languages)
|
||||
if (languages === undefined || languages.length <= 1) {
|
||||
super(undefined, undefined, undefined)
|
||||
return undefined
|
||||
}else {
|
||||
const normalPicker = LanguagePicker.dropdownFor(languages, label)
|
||||
const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, label))
|
||||
super(fullPicker, normalPicker, Locale.showLinkToWeblate)
|
||||
const allLanguages: string[] = used_languages.languages
|
||||
}
|
||||
|
||||
const allLanguages: string[] = used_languages.languages
|
||||
|
||||
const normalPicker = LanguagePicker.dropdownFor(languages, label)
|
||||
const fullPicker = new Lazy(() => LanguagePicker.dropdownFor(allLanguages, label))
|
||||
super(fullPicker, normalPicker, Locale.showLinkToWeblate)
|
||||
}
|
||||
|
||||
private static dropdownFor(languages: string[], label: string | BaseUIElement): BaseUIElement {
|
||||
|
|
|
@ -18,6 +18,7 @@ import Title from "../Base/Title"
|
|||
import { GlobalFilter } from "../../Logic/State/MapState"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import {WayId} from "../../Models/OsmFeature";
|
||||
|
||||
export default class ConfirmLocationOfPoint extends Combine {
|
||||
constructor(
|
||||
|
@ -35,7 +36,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
confirm: (
|
||||
tags: any[],
|
||||
location: { lat: number; lon: number },
|
||||
snapOntoWayId: string
|
||||
snapOntoWayId: WayId | undefined
|
||||
) => void,
|
||||
cancel: () => void,
|
||||
closePopup: () => void
|
||||
|
@ -75,6 +76,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
|||
snappedPointTags: tags,
|
||||
maxSnapDistance: preset.preciseInput.maxSnapDistance,
|
||||
bounds: mapBounds,
|
||||
state: <any> state
|
||||
})
|
||||
preciseInput.installBounds(preset.boundsFactor ?? 0.25, true)
|
||||
preciseInput
|
||||
|
|
|
@ -22,6 +22,7 @@ import Title from "../Base/Title"
|
|||
import { SubstitutedTranslation } from "../SubstitutedTranslation"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion"
|
||||
import {OsmId} from "../../Models/OsmFeature";
|
||||
|
||||
export default class DeleteWizard extends Toggle {
|
||||
/**
|
||||
|
@ -43,7 +44,7 @@ export default class DeleteWizard extends Toggle {
|
|||
* @param state: the state of the application
|
||||
* @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted
|
||||
*/
|
||||
constructor(id: string, state: FeaturePipelineState, options: DeleteConfig) {
|
||||
constructor(id: OsmId, state: FeaturePipelineState, options: DeleteConfig) {
|
||||
const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets)
|
||||
const tagsSource = state.allElements.getEventSourceById(id)
|
||||
|
||||
|
|
|
@ -248,31 +248,29 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
)
|
||||
|
||||
editElements.push(
|
||||
new VariableUiElement(
|
||||
state.featureSwitchIsDebugging.map((isDebugging) => {
|
||||
if (isDebugging) {
|
||||
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{all_tags()}" },
|
||||
""
|
||||
)
|
||||
const config_download: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{export_as_geojson()}" },
|
||||
""
|
||||
)
|
||||
const config_id: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{open_in_iD()}" },
|
||||
""
|
||||
)
|
||||
Toggle.If(state.featureSwitchIsDebugging,
|
||||
() => {
|
||||
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{all_tags()}" },
|
||||
""
|
||||
)
|
||||
const config_download: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{export_as_geojson()}" },
|
||||
""
|
||||
)
|
||||
const config_id: TagRenderingConfig = new TagRenderingConfig(
|
||||
{ render: "{open_in_iD()}" },
|
||||
""
|
||||
)
|
||||
|
||||
return new Combine([
|
||||
new TagRenderingAnswer(tags, config_all_tags, state),
|
||||
new TagRenderingAnswer(tags, config_download, state),
|
||||
new TagRenderingAnswer(tags, config_id, state),
|
||||
"This is layer " + layerConfig.id,
|
||||
])
|
||||
}
|
||||
})
|
||||
)
|
||||
return new Combine([
|
||||
new TagRenderingAnswer(tags, config_all_tags, state),
|
||||
new TagRenderingAnswer(tags, config_download, state),
|
||||
new TagRenderingAnswer(tags, config_id, state),
|
||||
"This is layer " + layerConfig.id,
|
||||
])
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return new Combine(editElements).SetClass("flex flex-col")
|
||||
|
|
|
@ -145,6 +145,7 @@ export default class MoveWizard extends Toggle {
|
|||
minZoom: reason.minZoom,
|
||||
centerLocation: loc,
|
||||
mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer
|
||||
state: <any> state
|
||||
})
|
||||
|
||||
if (reason.lockBounds) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Tiles } from "../../Models/TileRange"
|
||||
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
|
||||
import State from "../../State"
|
||||
|
||||
export default class ShowTileInfo {
|
||||
public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true)
|
||||
|
@ -16,7 +15,7 @@ export default class ShowTileInfo {
|
|||
leafletMap: UIEventSource<any>
|
||||
layer?: LayerConfig
|
||||
doShowLayer?: UIEventSource<boolean>
|
||||
}) {
|
||||
}, state) {
|
||||
const source = options.source
|
||||
const metaFeature: Store<{ feature; freshness: Date }[]> = source.features.map(
|
||||
(features) => {
|
||||
|
@ -56,7 +55,7 @@ export default class ShowTileInfo {
|
|||
features: new StaticFeatureSource(metaFeature),
|
||||
leafletMap: options.leafletMap,
|
||||
doShowLayer: options.doShowLayer,
|
||||
state: State.state,
|
||||
state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
/**
|
||||
* The statistics-gui shows statistics from previous MapComplete-edits
|
||||
*/
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import {UIEventSource} from "../Logic/UIEventSource"
|
||||
import {VariableUiElement} from "./Base/VariableUIElement"
|
||||
import Loading from "./Base/Loading"
|
||||
import { Utils } from "../Utils"
|
||||
import {Utils} from "../Utils"
|
||||
import Combine from "./Base/Combine"
|
||||
import { StackedRenderingChart } from "./BigComponents/TagRenderingChart"
|
||||
import { LayerFilterPanel } from "./BigComponents/FilterView"
|
||||
import { AllKnownLayouts } from "../Customizations/AllKnownLayouts"
|
||||
import {StackedRenderingChart} from "./BigComponents/TagRenderingChart"
|
||||
import {LayerFilterPanel} from "./BigComponents/FilterView"
|
||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"
|
||||
import MapState from "../Logic/State/MapState"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import Title from "./Base/Title"
|
||||
import { FixedUiElement } from "./Base/FixedUiElement"
|
||||
import List from "./Base/List";
|
||||
|
||||
class StatisticsForOverviewFile extends Combine {
|
||||
|
||||
constructor(homeUrl: string, paths: string[]) {
|
||||
paths = paths.filter(p => !p.endsWith("file-overview.json"))
|
||||
const layer = AllKnownLayouts.allKnownLayouts.get("mapcomplete-changes").layers[0]
|
||||
const filteredLayer = MapState.InitializeFilteredLayers(
|
||||
{ id: "statistics-view", layers: [layer] },
|
||||
{id: "statistics-view", layers: [layer]},
|
||||
undefined
|
||||
)[0]
|
||||
const filterPanel = new LayerFilterPanel(undefined, filteredLayer)
|
||||
|
@ -27,9 +30,18 @@ class StatisticsForOverviewFile extends Combine {
|
|||
const downloaded = new UIEventSource<{ features: ChangeSetData[] }[]>([])
|
||||
|
||||
for (const filepath of paths) {
|
||||
if(filepath.endsWith("file-overview.json")){
|
||||
continue
|
||||
}
|
||||
Utils.downloadJson(homeUrl + filepath).then((data) => {
|
||||
if (data === undefined) {
|
||||
return
|
||||
}
|
||||
if (data.features === undefined) {
|
||||
data.features = data
|
||||
}
|
||||
data?.features?.forEach((item) => {
|
||||
item.properties = { ...item.properties, ...item.properties.metadata }
|
||||
item.properties = {...item.properties, ...item.properties.metadata}
|
||||
delete item.properties.metadata
|
||||
})
|
||||
downloaded.data.push(data)
|
||||
|
@ -43,6 +55,7 @@ class StatisticsForOverviewFile extends Combine {
|
|||
)
|
||||
)
|
||||
|
||||
|
||||
super([
|
||||
filterPanel,
|
||||
new VariableUiElement(
|
||||
|
@ -83,7 +96,49 @@ class StatisticsForOverviewFile extends Combine {
|
|||
const trs = layer.tagRenderings.filter(
|
||||
(tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined
|
||||
)
|
||||
const elements: BaseUIElement[] = []
|
||||
|
||||
const allKeys = new Set<string>()
|
||||
for (const cs of overview._meta) {
|
||||
for (const propertiesKey in cs.properties) {
|
||||
allKeys.add(propertiesKey)
|
||||
}
|
||||
}
|
||||
console.log("All keys:", allKeys)
|
||||
|
||||
const valuesToSum = [
|
||||
"create",
|
||||
"modify",
|
||||
"delete",
|
||||
"answer",
|
||||
"move",
|
||||
"deletion",
|
||||
"add-image",
|
||||
"plantnet-ai-detection",
|
||||
"import",
|
||||
"conflation",
|
||||
"link-image",
|
||||
"soft-delete"]
|
||||
|
||||
const allThemes = Utils.Dedup(overview._meta.map(f => f.properties.theme))
|
||||
|
||||
const excludedThemes = new Set<string>()
|
||||
if(allThemes.length > 1){
|
||||
excludedThemes.add("grb")
|
||||
excludedThemes.add("etymology")
|
||||
}
|
||||
const summedValues = valuesToSum
|
||||
.map(key => [key, overview.sum(key, excludedThemes)])
|
||||
.filter(kv => kv[1] != 0)
|
||||
.map(kv => kv.join(": "))
|
||||
const elements: BaseUIElement[] = [
|
||||
new Title(allThemes .length === 1 ? "General statistics for "+allThemes[0] :"General statistics (excluding etymology- and GRB-theme changes)"),
|
||||
new Combine([
|
||||
overview._meta.length + " changesets match the filters",
|
||||
new List(summedValues)
|
||||
]).SetClass("flex flex-col border rounded-xl"),
|
||||
|
||||
new Title("Breakdown")
|
||||
]
|
||||
for (const tr of trs) {
|
||||
let total = undefined
|
||||
if (tr.freeform?.key !== undefined) {
|
||||
|
@ -186,6 +241,20 @@ class ChangesetsOverview {
|
|||
return new ChangesetsOverview(this._meta.filter(predicate))
|
||||
}
|
||||
|
||||
public sum(key: string, excludeThemes: Set<string>): number {
|
||||
let s = 0
|
||||
for (const feature of this._meta) {
|
||||
if(excludeThemes.has(feature.properties.theme)){
|
||||
continue
|
||||
}
|
||||
const parsed = Number(feature.properties[key])
|
||||
if (!isNaN(parsed)) {
|
||||
s += parsed
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
private static cleanChangesetData(cs: ChangeSetData): ChangeSetData {
|
||||
if (cs === undefined) {
|
||||
return undefined
|
||||
|
@ -211,7 +280,8 @@ class ChangesetsOverview {
|
|||
}
|
||||
try {
|
||||
cs.properties.host = new URL(cs.properties.host).host
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
}
|
||||
return cs
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ export default class Translations {
|
|||
* translation.textFor("nl") // => "Nederlands"
|
||||
*
|
||||
*/
|
||||
static T(t: string | any, context = undefined): TypedTranslation<object> {
|
||||
static T(t: string | undefined | null | Translation | TypedTranslation<object>, context = undefined): TypedTranslation<object> {
|
||||
if (t === undefined || t === null) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ export default class Translations {
|
|||
if (typeof t === "string") {
|
||||
return new TypedTranslation<object>({ "*": t }, context)
|
||||
}
|
||||
if (t.render !== undefined) {
|
||||
if (t["render"] !== undefined) {
|
||||
const msg =
|
||||
"Creating a translation, but this object contains a 'render'-field. Use the translation directly"
|
||||
console.error(msg, t)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue