Full code cleanup

This commit is contained in:
Pieter Vander Vennet 2021-11-07 16:34:51 +01:00
parent 8e6ee8c87f
commit bd21212eba
246 changed files with 19418 additions and 11729 deletions

View file

@ -8,32 +8,31 @@ import {Utils} from "../Utils";
import LanguagePicker from "./LanguagePicker";
import IndexText from "./BigComponents/IndexText";
import FeaturedMessage from "./BigComponents/FeaturedMessage";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
export default class AllThemesGui {
constructor() {
try{
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined);
const intro = new Combine([
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
.SetClass("absolute top-2 right-3"),
new IndexText()
]);
new Combine([
intro,
new FeaturedMessage(),
new MoreScreen(state, true),
Translations.t.general.aboutMapcomplete
.Subs({"osmcha_link": Utils.OsmChaLinkFor(7)})
.SetClass("link-underline"),
new FixedUiElement("v" + Constants.vNumber)
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
.SetStyle("pointer-events: all;")
.AttachTo("topleft-tools");
}catch (e) {
try {
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined);
const intro = new Combine([
LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages())
.SetClass("absolute top-2 right-3"),
new IndexText()
]);
new Combine([
intro,
new FeaturedMessage(),
new MoreScreen(state, true),
Translations.t.general.aboutMapcomplete
.Subs({"osmcha_link": Utils.OsmChaLinkFor(7)})
.SetClass("link-underline"),
new FixedUiElement("v" + Constants.vNumber)
]).SetClass("block m-5 lg:w-3/4 lg:ml-40")
.SetStyle("pointer-events: all;")
.AttachTo("topleft-tools");
} catch (e) {
new FixedUiElement("Seems like no layers are compiled - check the output of `npm run generate:layeroverview`. Is this visible online? Contact pietervdvn immediately!").SetClass("alert")
.AttachTo("centermessage")
}

View file

@ -3,26 +3,25 @@ import {VariableUiElement} from "./VariableUIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loading from "./Loading";
export default class AsyncLazy extends BaseUIElement{
export default class AsyncLazy extends BaseUIElement {
private readonly _f: () => Promise<BaseUIElement>;
constructor(f: () => Promise<BaseUIElement>) {
super();
this._f = f;
}
protected InnerConstructElement(): HTMLElement {
// The caching of the BaseUIElement will guarantee that _f will only be called once
return new VariableUiElement(
UIEventSource.FromPromise(this._f()).map(el => {
if(el === undefined){
if (el === undefined) {
return new Loading()
}
return el
})
).ConstructElement()
}
}

View file

@ -30,13 +30,13 @@ export default class Combine extends BaseUIElement {
if (subEl === undefined || subEl === null) {
continue;
}
try{
const subHtml = subEl.ConstructElement()
if (subHtml !== undefined) {
el.appendChild(subHtml)
}
}catch(e){
try {
const subHtml = subEl.ConstructElement()
if (subHtml !== undefined) {
el.appendChild(subHtml)
}
} catch (e) {
console.error("Could not generate subelement in combine due to ", e)
}
}

View file

@ -10,7 +10,7 @@ export default class Img extends BaseUIElement {
fallbackImage?: string
}) {
super();
if(src === undefined || src === "undefined"){
if (src === undefined || src === "undefined") {
throw "Undefined src for image"
}
this._src = src;
@ -44,7 +44,7 @@ export default class Img extends BaseUIElement {
}
el.onerror = () => {
if (self._options?.fallbackImage) {
if(el.src === self._options.fallbackImage){
if (el.src === self._options.fallbackImage) {
// Sigh... nothing to be done anymore
return;
}

View file

@ -1,16 +1,16 @@
import BaseUIElement from "../BaseUIElement";
export default class Lazy extends BaseUIElement{
export default class Lazy extends BaseUIElement {
private readonly _f: () => BaseUIElement;
constructor(f: () => BaseUIElement) {
super();
this._f = f;
}
protected InnerConstructElement(): HTMLElement {
// The caching of the BaseUIElement will guarantee that _f will only be called once
return this._f().ConstructElement();
}
}

View file

@ -1,4 +1,3 @@
import {FixedUiElement} from "./FixedUiElement";
import {Translation} from "../i18n/Translation";
import Combine from "./Combine";
import Svg from "../../Svg";
@ -6,11 +5,11 @@ import Translations from "../i18n/Translations";
export default class Loading extends Combine {
constructor(msg?: Translation | string) {
const t = Translations.T(msg ) ?? Translations.t.general.loading.Clone();
const t = Translations.T(msg) ?? Translations.t.general.loading.Clone();
t.SetClass("pl-2")
super([
Svg.loading_svg().SetClass("animate-spin").SetStyle("width: 1.5rem; height: 1.5rem;"),
t
t
])
this.SetClass("flex p-1")
}

View file

@ -17,8 +17,10 @@ export interface MinimapOptions {
}
export interface MinimapObj {
readonly leafletMap: UIEventSource<any>,
installBounds(factor: number | BBox, showRange?: boolean) : void
readonly leafletMap: UIEventSource<any>,
installBounds(factor: number | BBox, showRange?: boolean): void
TakeScreenshot(): Promise<any>;
}

View file

@ -103,6 +103,12 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
})
}
public async TakeScreenshot() {
const screenshotter = new SimpleMapScreenshoter();
screenshotter.addTo(this.leafletMap.data);
return await screenshotter.takeScreen('image')
}
protected InnerConstructElement(): HTMLElement {
const div = document.createElement("div")
div.id = this._id;
@ -148,8 +154,8 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
const self = this;
let currentLayer = this._background.data.layer()
let latLon = <[number, number]>[location.data?.lat ?? 0, location.data?.lon ?? 0]
if(isNaN(latLon[0]) || isNaN(latLon[1])){
latLon = [0,0]
if (isNaN(latLon[0]) || isNaN(latLon[1])) {
latLon = [0, 0]
}
const options = {
center: latLon,
@ -279,10 +285,4 @@ export default class MinimapImplementation extends BaseUIElement implements Mini
this.leafletMap.setData(map)
}
public async TakeScreenshot(){
const screenshotter = new SimpleMapScreenshoter();
screenshotter.addTo(this.leafletMap.data);
return await screenshotter.takeScreen('image')
}
}

View file

@ -19,8 +19,8 @@ import Img from "./Img";
export default class ScrollableFullScreen extends UIElement {
private static readonly empty = new FixedUiElement("");
private static _currentlyOpen: ScrollableFullScreen;
private hashToShow: string;
public isShown: UIEventSource<boolean>;
private hashToShow: string;
private _component: BaseUIElement;
private _fullscreencomponent: BaseUIElement;
@ -61,13 +61,6 @@ export default class ScrollableFullScreen extends UIElement {
})
}
private clear() {
ScrollableFullScreen.empty.AttachTo("fullscreen")
const fs = document.getElementById("fullscreen");
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
fs.classList.add("hidden")
}
InnerRender(): BaseUIElement {
return this._component;
}
@ -80,6 +73,13 @@ export default class ScrollableFullScreen extends UIElement {
fs.classList.remove("hidden")
}
private clear() {
ScrollableFullScreen.empty.AttachTo("fullscreen")
const fs = document.getElementById("fullscreen");
ScrollableFullScreen._currentlyOpen?.isShown?.setData(false);
fs.classList.add("hidden")
}
private BuildComponent(title: BaseUIElement, content: BaseUIElement, isShown: UIEventSource<boolean>) {
const returnToTheMap =
new Combine([

View file

@ -6,7 +6,7 @@ import {VariableUiElement} from "./VariableUIElement";
export class TabbedComponent extends Combine {
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
constructor(elements: { header: BaseUIElement | string, content: BaseUIElement | string }[],
openedTab: (UIEventSource<number> | number) = 0,
options?: {
leftOfHeader?: BaseUIElement
@ -15,13 +15,13 @@ export class TabbedComponent extends Combine {
const openedTabSrc = typeof (openedTab) === "number" ? new UIEventSource(openedTab) : (openedTab ?? new UIEventSource<number>(0))
const tabs: BaseUIElement[] = [options?.leftOfHeader ]
const tabs: BaseUIElement[] = [options?.leftOfHeader]
const contentElements: BaseUIElement[] = [];
for (let i = 0; i < elements.length; i++) {
let element = elements[i];
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
openedTabSrc.addCallbackAndRun(selected => {
if(selected >= elements.length){
if (selected >= elements.length) {
selected = 0
}
if (selected === i) {
@ -40,7 +40,7 @@ export class TabbedComponent extends Combine {
}
const header = new Combine(tabs).SetClass("tabs-header-bar")
if(options?.styleHeader){
if (options?.styleHeader) {
options.styleHeader(header)
}
const actualContent = new VariableUiElement(

View file

@ -7,9 +7,9 @@ export default class Title extends BaseUIElement {
constructor(embedded: string | BaseUIElement, level: number = 3) {
super()
if(typeof embedded === "string"){
this._embedded = new FixedUiElement(embedded)
}else{
if (typeof embedded === "string") {
this._embedded = new FixedUiElement(embedded)
} else {
this._embedded = embedded
}
this._level = level;

View file

@ -45,7 +45,9 @@ export default abstract class BaseUIElement {
* Adds all the relevant classes, space separated
*/
public SetClass(clss: string) {
if(clss == undefined){return }
if (clss == undefined) {
return
}
const all = clss.split(" ").map(clsName => clsName.trim());
let recordedChange = false;
for (let c of all) {

View file

@ -15,7 +15,7 @@ import {Utils} from "../../Utils";
*/
export default class Attribution extends Combine {
constructor(location: UIEventSource<Loc>,
constructor(location: UIEventSource<Loc>,
userDetails: UIEventSource<UserDetails>,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>) {

View file

@ -36,8 +36,8 @@ export default class CopyrightPanel extends Combine {
locationControl: UIEventSource<Loc>,
osmConnection: OsmConnection
}, contributions: UIEventSource<Map<string, number>>) {
const t =Translations.t.general.attribution
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const josmState = new UIEventSource<string>(undefined)
// Reset after 15s
@ -53,7 +53,7 @@ export default class CopyrightPanel extends Combine {
newTab: true
}),
new SubtleButton(Svg.statistics_ui().SetStyle(iconStyle), t.openOsmcha.Subs({theme: state.layoutToUse.title}), {
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
url: Utils.OsmChaLinkFor(31, state.layoutToUse.id),
newTab: true
}),
new VariableUiElement(state.locationControl.map(location => {
@ -63,42 +63,45 @@ export default class CopyrightPanel extends Combine {
new VariableUiElement(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), t.openMapillary, {url: mapillaryLink, newTab: true})
return new SubtleButton(Svg.mapillary_black_ui().SetStyle(iconStyle), t.openMapillary, {
url: mapillaryLink,
newTab: true
})
})),
new VariableUiElement(josmState.map(state => {
if(state === undefined){
if (state === undefined) {
return undefined
}
state = state.toUpperCase()
if(state === "OK"){
if (state === "OK") {
return t.josmOpened.SetClass("thanks")
}
return t.josmNotOpened.SetClass("alert")
})),
new Toggle(
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle) , t.editJosm).onClick(() => {
const bounds: any = state.currentBounds.data;
if (bounds === undefined) {
return undefined
}
const 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)),
new SubtleButton(Svg.josm_logo_ui().SetStyle(iconStyle), t.editJosm).onClick(() => {
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 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)),
].map(button => button.SetStyle("max-height: 3rem"))
const iconAttributions = Utils.NoNull(Array.from(layoutToUse.ExtractImages()))
.map(CopyrightPanel.IconAttribution)
let maintainer : BaseUIElement= undefined
if(layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete"){
let maintainer: BaseUIElement = undefined
if (layoutToUse.maintainer !== undefined && layoutToUse.maintainer !== "" && layoutToUse.maintainer.toLowerCase() !== "mapcomplete") {
maintainer = Translations.t.general.attribution.themeBy.Subs({author: layoutToUse.maintainer})
}
super([
Translations.t.general.attribution.attributionContent,
maintainer,
@ -106,7 +109,7 @@ export default class CopyrightPanel extends Combine {
new FixedUiElement(layoutToUse.credits),
new Attribution(State.state.locationControl, State.state.osmConnection.userDetails, State.state.layoutToUse, State.state.currentBounds),
new VariableUiElement(contributions.map(contributions => {
if(contributions === undefined){
if (contributions === undefined) {
return ""
}
const sorted = Array.from(contributions, ([name, value]) => ({

View file

@ -12,26 +12,25 @@ import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {UIEventSource} from "../../Logic/UIEventSource";
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {meta} from "@turf/turf";
import {BBox} from "../../Logic/BBox";
export class DownloadPanel extends Toggle {
constructor() {
const state: {
featurePipeline: FeaturePipeline,
layoutToUse: LayoutConfig,
currentBounds: UIEventSource<BBox>
} = State.state
const t = Translations.t.general.download
const name = State.state.layoutToUse.id;
const includeMetaToggle = new CheckBoxes([t.includeMetaData.Clone()])
const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0)
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
new Combine([t.downloadGeojson.Clone().SetClass("font-bold"),
t.downloadGeoJsonHelper.Clone()]).SetClass("flex flex-col"))
@ -42,7 +41,7 @@ export class DownloadPanel extends Toggle {
mimetype: "application/vnd.geo+json"
});
})
const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine(
[t.downloadCSV.Clone().SetClass("font-bold"),
@ -59,9 +58,9 @@ export class DownloadPanel extends Toggle {
const downloadButtons = new Combine(
[new Title(t.title),
buttonGeoJson,
buttonGeoJson,
buttonCSV,
includeMetaToggle,
includeMetaToggle,
t.licenseInfo.Clone().SetClass("link-underline")])
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
@ -107,7 +106,7 @@ export class DownloadPanel extends Toggle {
}
return {
type:"FeatureCollection",
type: "FeatureCollection",
features: resultFeatures
}

View file

@ -20,7 +20,7 @@ export default class FeaturedMessage extends Combine {
if (wm.end_date <= now) {
continue
}
if (welcome_message !== undefined) {
console.warn("Multiple applicable messages today:", welcome_message.featured_theme)
}
@ -62,7 +62,7 @@ export default class FeaturedMessage extends Combine {
message: wm.message,
featured_theme: wm.featured_theme
})
}
return all_messages
}

View file

@ -28,8 +28,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>,
locationControl: UIEventSource<Loc>,
backgroundLayer: UIEventSource<BaseLayer>,
locationControl: UIEventSource<Loc>,
backgroundLayer: UIEventSource<BaseLayer>,
filteredLayers: UIEventSource<FilteredLayer[]>
} & UserRelatedState) {
const layoutToUse = state.layoutToUse;
@ -70,10 +70,10 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
tabs.push({
header: Svg.add_img,
content:
new Combine([
Translations.t.general.morescreen.intro,
new MoreScreen(state)
]).SetClass("flex flex-col")
new Combine([
Translations.t.general.morescreen.intro,
new MoreScreen(state)
]).SetClass("flex flex-col")
});
}
@ -91,7 +91,7 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)]
tabsWithAboutMc.push({
header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete

View file

@ -9,7 +9,6 @@ import Toggle from "../Input/Toggle";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import {Tag} from "../../Logic/Tags/Tag";
import Loading from "../Base/Loading";
import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes";
@ -32,7 +31,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import BaseLayer from "../../Models/BaseLayer";
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
@ -282,8 +280,8 @@ export default class ImportButton extends Toggle {
}
public static createConfirmForWay(o: ImportButtonState,
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>): BaseUIElement {
isImported: UIEventSource<boolean>,
importClicked: UIEventSource<boolean>): BaseUIElement {
const confirmationMap = Minimap.createMiniMap({
allowMoving: true,
@ -301,8 +299,8 @@ export default class ImportButton extends Toggle {
allElements: o.state.allElements,
layers: o.state.filteredLayers
})
let action : OsmChangeAction & {getPreview() : Promise<FeatureSource>}
let action: OsmChangeAction & { getPreview(): Promise<FeatureSource> }
const theme = o.state.layoutToUse.id
const changes = o.state.changes
@ -320,7 +318,7 @@ export default class ImportButton extends Toggle {
)
confirm = async () => {
changes.applyAction (action)
changes.applyAction(action)
return o.feature.properties.id
}
@ -332,8 +330,8 @@ export default class ImportButton extends Toggle {
} else if (geom.type === "Polygon") {
coordinates = geom.coordinates[0]
}
action = new CreateWayWithPointReuseAction(
o.newTags.data,
coordinates,
@ -341,13 +339,13 @@ export default class ImportButton extends Toggle {
o.state,
[{
withinRangeOfM: 1,
ifMatches: new Tag("_is_part_of_building","true"),
mode:"move_osm_point"
ifMatches: new Tag("_is_part_of_building", "true"),
mode: "move_osm_point"
}]
)
confirm = async () => {
changes.applyAction(action)
return undefined

View file

@ -45,8 +45,8 @@ export default class LeftControls extends Combine {
state,
new ContributorCount(state).Contributors
),
"copyright",
guiState.copyrightViewIsOpened
"copyright",
guiState.copyrightViewIsOpened
);
const copyrightButton = new Toggle(

View file

@ -39,110 +39,6 @@ export default class MoreScreen extends Combine {
]);
}
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
return new VariableUiElement(state.installedThemes.map(customThemes => {
if (customThemes.length <= 0) {
return undefined;
}
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
return new Combine([
Translations.t.general.customThemeIntro.Clone(),
new Combine(customThemeButtons).SetClass(themeListClasses)
]);
}));
}
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-"
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
return new Toggle(
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
.filter(key => key.startsWith(prefix))
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
.map(theme => {
return AllKnownLayouts.allKnownLayouts.get(theme);
}))
if (knownThemes.length === 0) {
return undefined
}
const knownLayouts = new Combine(knownThemes.map(layout =>
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
)).SetClass(themeListStyle)
return new Combine([
new Title(t.previouslyHiddenTitle),
t.hiddenExplanation.Subs({hidden_discovered: ""+knownThemes.length,total_hidden: ""+hiddenTotal}),
knownLayouts
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
let officialThemes = AllKnownLayouts.layoutsList
let buttons = 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) {
return new VariableUiElement(
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
.map(csCount => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
} else {
return button
}
})
)
}
return button;
})
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0, customGeneratorLink);
return new Combine(buttons)
}
/*
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
* */
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
const tr = Translations.t.general.morescreen;
return new VariableUiElement(
state.osmConnection.userDetails.map(userDetails => {
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
return new SubtleButton(null, tr.requestATheme.Clone(), {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true
});
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false
});
})
)
}
/**
* Creates a button linking to the given theme
* @private
@ -161,7 +57,7 @@ export default class MoreScreen extends Combine {
console.error("ID is undefined for layout", layout);
return undefined;
}
if (layout.id === state?.layoutToUse?.id) {
return undefined;
}
@ -210,5 +106,111 @@ export default class MoreScreen extends Combine {
]), {url: linkText, newTab: false});
}
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState, themeListClasses): BaseUIElement {
return new VariableUiElement(state.installedThemes.map(customThemes => {
if (customThemes.length <= 0) {
return undefined;
}
const customThemeButtons = customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
return new Combine([
Translations.t.general.customThemeIntro.Clone(),
new Combine(customThemeButtons).SetClass(themeListClasses)
]);
}));
}
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string, themeListStyle: string) {
const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-"
const hiddenTotal = AllKnownLayouts.layoutsList.filter(layout => layout.hideFromOverview).length
return new Toggle(
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
const knownThemes = Utils.NoNull(Object.keys(allPreferences)
.filter(key => key.startsWith(prefix))
.map(key => key.substring(prefix.length, key.length - "-enabled".length))
.map(theme => {
return AllKnownLayouts.allKnownLayouts.get(theme);
}))
if (knownThemes.length === 0) {
return undefined
}
const knownLayouts = new Combine(knownThemes.map(layout =>
MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
)).SetClass(themeListStyle)
return new Combine([
new Title(t.previouslyHiddenTitle),
t.hiddenExplanation.Subs({
hidden_discovered: "" + knownThemes.length,
total_hidden: "" + hiddenTotal
}),
knownLayouts
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
let officialThemes = AllKnownLayouts.layoutsList
let buttons = 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) {
return new VariableUiElement(
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
.map(csCount => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
} else {
return button
}
})
)
}
return button;
})
let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0, customGeneratorLink);
return new Combine(buttons)
}
/*
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
* */
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
const tr = Translations.t.general.morescreen;
return new VariableUiElement(
state.osmConnection.userDetails.map(userDetails => {
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
return new SubtleButton(null, tr.requestATheme.Clone(), {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true
});
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false
});
})
)
}
}

View file

@ -9,19 +9,19 @@ import AllKnownLayers from "../../Customizations/AllKnownLayers";
export default class RightControls extends Combine {
constructor(state:MapState) {
constructor(state: MapState) {
const geolocatioHandler = new GeoLocationHandler(
state
)
new ShowDataLayer({
layerToShow: AllKnownLayers.sharedLayers.get("gps_location"),
leafletMap: state.leafletMap,
enablePopups: true,
features: geolocatioHandler.currentLocation
})
const geolocationButton = new Toggle(
new MapControlButton(
geolocatioHandler

View file

@ -15,7 +15,7 @@ import FilteredLayer from "../../Models/FilteredLayer";
export default class ShareScreen extends Combine {
constructor(state: {layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>}) {
constructor(state: { layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> }) {
const layout = state?.layoutToUse;
const tr = Translations.t.general.sharescreen;
@ -62,40 +62,39 @@ export default class ShareScreen extends Combine {
}
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 Toggle(
new Combine([check(), currentBackground]),
new Combine([nocheck(), currentBackground]),
new UIEventSource<boolean>(true)
).ToggleOnClick()
optionCheckboxes.push(includeCurrentBackground);
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
if (includeBG) {
return "background=" + currentLayer.data.id
} else {
return null
}
}, [currentLayer]));
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 Toggle(
new Combine([check(), currentBackground]),
new Combine([nocheck(), currentBackground]),
new UIEventSource<boolean>(true)
).ToggleOnClick()
optionCheckboxes.push(includeCurrentBackground);
optionParts.push(includeCurrentBackground.isEnabled.map((includeBG) => {
if (includeBG) {
return "background=" + currentLayer.data.id
} else {
return null
}
}, [currentLayer]));
const includeLayerChoices = new Toggle(
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
new UIEventSource<boolean>(true)
).ToggleOnClick()
optionCheckboxes.push(includeLayerChoices);
const includeLayerChoices = new Toggle(
new Combine([check(), tr.fsIncludeCurrentLayers.Clone()]),
new Combine([nocheck(), tr.fsIncludeCurrentLayers.Clone()]),
new UIEventSource<boolean>(true)
).ToggleOnClick()
optionCheckboxes.push(includeLayerChoices);
optionParts.push(includeLayerChoices.isEnabled.map((includeLayerSelection) => {
if (includeLayerSelection) {
return Utils.NoNull(state.filteredLayers.data.map(fLayerToParam)).join("&")
} else {
return null
}
}, state.filteredLayers.data.map((flayer) => flayer.isDisplayed)));
optionParts.push(includeLayerChoices.isEnabled.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 = [

View file

@ -105,7 +105,7 @@ export default class SimpleAddUI extends Toggle {
selectedPreset.setData(undefined)
}
const message =Translations.t.general.add.addNew.Subs({category: preset.name});
const message = Translations.t.general.add.addNew.Subs({category: preset.name});
return new ConfirmLocationOfPoint(state, filterViewIsOpened, preset,
message,
state.LastClickLocation.data,

View file

@ -46,123 +46,6 @@ export default class DefaultGUI {
this.SetupMap()
}
private SetupMap(){
const state = this.state;
const guiState = this._guiState;
// Attach the map
state.mainMapObject.SetClass("w-full h-full")
.AttachTo("leafletDiv")
this.setupClickDialogOnMap(
guiState.filterViewIsOpened,
state
)
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
features: state.homeLocation,
enablePopups: false,
})
state.leafletMap.addCallbackAndRunD(_ => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
State.state.locationControl.ping();
return true;
})
}
private SetupUIElements(){
const state = this.state;
const guiState = this._guiState;
const self =this
Toggle.If(state.featureSwitchUserbadge,
() => new UserBadge(state)
).AttachTo("userbadge")
Toggle.If(state.featureSwitchSearch,
() => new SearchAndGo(state))
.AttachTo("searchbox");
let iframePopout: () => BaseUIElement = undefined;
if (window !== window.top) {
// MapComplete is running in an iframe
iframePopout = () => new VariableUiElement(state.locationControl.map(loc => {
const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`;
const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5")
return new MapControlButton(link)
}))
}
new Toggle(new Lazy(() => self.InitWelcomeMessage()),
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
state.featureSwitchWelcomeMessage
).AttachTo("messagesbox");
new LeftControls(state, guiState).AttachTo("bottom-left");
new RightControls(state).AttachTo("bottom-right");
new CenterMessageBox(state).AttachTo("centermessage");
document
.getElementById("centermessage")
.classList.add("pointer-events-none");
// We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed
for (const state of guiState.allFullScreenStates) {
if(state.data){
state.ping()
}
}
/**
* At last, if the map moves or an element is selected, we close all the panels just as well
*/
state.selectedElement.addCallbackAndRunD((_) => {
guiState.allFullScreenStates.forEach(s => s.setData(false))
});
}
private InitWelcomeMessage() : BaseUIElement{
const isOpened = this._guiState.welcomeMessageIsOpened
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg());
help.onClick(() => isOpened.setData(true));
const openedTime = new Date().getTime();
this.state.locationControl.addCallback(() => {
if (new Date().getTime() - openedTime < 15 * 1000) {
// Don't autoclose the first 15 secs when the map is moving
return;
}
isOpened.setData(false);
});
this.state.selectedElement.addCallbackAndRunD((_) => {
isOpened.setData(false);
});
return new Toggle(
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
help.SetClass("pointer-events-auto"),
isOpened
)
}
public setupClickDialogOnMap(filterViewIsOpened: UIEventSource<boolean>, state: FeaturePipelineState) {
function setup() {
@ -207,4 +90,120 @@ export default class DefaultGUI {
}
private SetupMap() {
const state = this.state;
const guiState = this._guiState;
// Attach the map
state.mainMapObject.SetClass("w-full h-full")
.AttachTo("leafletDiv")
this.setupClickDialogOnMap(
guiState.filterViewIsOpened,
state
)
new ShowDataLayer({
leafletMap: state.leafletMap,
layerToShow: AllKnownLayers.sharedLayers.get("home_location"),
features: state.homeLocation,
enablePopups: false,
})
state.leafletMap.addCallbackAndRunD(_ => {
// Lets assume that all showDataLayers are initialized at this point
state.selectedElement.ping()
State.state.locationControl.ping();
return true;
})
}
private SetupUIElements() {
const state = this.state;
const guiState = this._guiState;
const self = this
Toggle.If(state.featureSwitchUserbadge,
() => new UserBadge(state)
).AttachTo("userbadge")
Toggle.If(state.featureSwitchSearch,
() => new SearchAndGo(state))
.AttachTo("searchbox");
let iframePopout: () => BaseUIElement = undefined;
if (window !== window.top) {
// MapComplete is running in an iframe
iframePopout = () => new VariableUiElement(state.locationControl.map(loc => {
const url = `${window.location.origin}${window.location.pathname}?z=${loc.zoom ?? 0}&lat=${loc.lat ?? 0}&lon=${loc.lon ?? 0}`;
const link = new Link(Svg.pop_out_img, url, true).SetClass("block w-full h-full p-1.5")
return new MapControlButton(link)
}))
}
new Toggle(new Lazy(() => self.InitWelcomeMessage()),
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
state.featureSwitchWelcomeMessage
).AttachTo("messagesbox");
new LeftControls(state, guiState).AttachTo("bottom-left");
new RightControls(state).AttachTo("bottom-right");
new CenterMessageBox(state).AttachTo("centermessage");
document
.getElementById("centermessage")
.classList.add("pointer-events-none");
// We have to ping the welcomeMessageIsOpened and other isOpened-stuff to activate the FullScreenMessage if needed
for (const state of guiState.allFullScreenStates) {
if (state.data) {
state.ping()
}
}
/**
* At last, if the map moves or an element is selected, we close all the panels just as well
*/
state.selectedElement.addCallbackAndRunD((_) => {
guiState.allFullScreenStates.forEach(s => s.setData(false))
});
}
private InitWelcomeMessage(): BaseUIElement {
const isOpened = this._guiState.welcomeMessageIsOpened
const fullOptions = new FullWelcomePaneWithTabs(isOpened, this._guiState.welcomeMessageOpenedTab, this.state);
// ?-Button on Desktop, opens panel with close-X.
const help = new MapControlButton(Svg.help_svg());
help.onClick(() => isOpened.setData(true));
const openedTime = new Date().getTime();
this.state.locationControl.addCallback(() => {
if (new Date().getTime() - openedTime < 15 * 1000) {
// Don't autoclose the first 15 secs when the map is moving
return;
}
isOpened.setData(false);
});
this.state.selectedElement.addCallbackAndRunD((_) => {
isOpened.setData(false);
});
return new Toggle(
fullOptions.SetClass("welcomeMessage pointer-events-auto"),
help.SetClass("pointer-events-auto"),
isOpened
)
}
}

View file

@ -4,13 +4,13 @@ import Constants from "../Models/Constants";
import Hash from "../Logic/Web/Hash";
export class DefaultGuiState {
static state: DefaultGuiState;
public readonly welcomeMessageIsOpened: UIEventSource<boolean>;
public readonly downloadControlIsOpened: UIEventSource<boolean>;
public readonly filterViewIsOpened: UIEventSource<boolean>;
public readonly copyrightViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab: UIEventSource<number>
public readonly allFullScreenStates: UIEventSource<boolean>[] = []
static state: DefaultGuiState;
constructor() {

View file

@ -11,6 +11,7 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import {BBox} from "../Logic/BBox";
/**
* Creates screenshoter to take png screenshot
* Creates jspdf and downloads it
@ -79,10 +80,10 @@ export default class ExportPDF {
minimap.AttachTo(options.freeDivId)
// Next: we prepare the features. Only fully contained features are shown
minimap.leafletMap .addCallbackAndRunD(leaflet => {
minimap.leafletMap.addCallbackAndRunD(leaflet => {
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2))
options.features.GetTilesPerLayerWithin(bounds, tile => {
if(tile.layer.layerDef.minzoom > l.zoom){
if (tile.layer.layerDef.minzoom > l.zoom) {
return
}
new ShowDataLayer(
@ -95,7 +96,7 @@ export default class ExportPDF {
}
)
})
})
State.state.AddAllOverlaysToMap(minimap.leafletMap)
@ -107,9 +108,8 @@ export default class ExportPDF {
}
private async CreatePdf(minimap: MinimapObj) {
console.log("PDF creation started")
const t = Translations.t.general.pdf;
const layout = this._layout

View file

@ -12,10 +12,10 @@ export class AttributedImage extends Combine {
let img: BaseUIElement;
let attr: BaseUIElement
img = new Img(imageInfo.url, false, {
fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined
fallbackImage: imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined
});
attr = new Attribution(imageInfo.provider.GetAttributionFor(imageInfo.url),
imageInfo.provider.SourceIcon(),
imageInfo.provider.SourceIcon(),
)

View file

@ -13,10 +13,10 @@ export default class Attribution extends VariableUiElement {
}
super(
license.map((license: LicenseInfo) => {
if(license === undefined){
if (license === undefined) {
return undefined
}
return new Combine([
icon?.SetClass("block left").SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),

View file

@ -15,19 +15,19 @@ export default class DeleteImage extends Toggle {
const isDeletedBadge = Translations.t.image.isDeleted.Clone()
.SetClass("rounded-full p-1")
.SetStyle("color:white;background:#ff8c8c")
.onClick(async() => {
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
changeType: "answer",
theme: "test"
}))
.onClick(async () => {
await State.state?.changes?.applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
changeType: "answer",
theme: "test"
}))
});
const deleteButton = Translations.t.image.doDelete.Clone()
.SetClass("block w-full pl-4 pr-4")
.SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;")
.onClick( async() => {
await State.state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data,{
.onClick(async () => {
await State.state?.changes?.applyAction(
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, {
changeType: "answer",
theme: "test"
})

View file

@ -9,7 +9,7 @@ import ImageProvider from "../../Logic/ImageProviders/ImageProvider";
export class ImageCarousel extends Toggle {
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>,
constructor(images: UIEventSource<{ key: string, url: string, provider: ImageProvider }[]>,
tags: UIEventSource<any>,
keys: string[]) {
const uiElements = images.map((imageURLS: { key: string, url: string, provider: ImageProvider }[]) => {

View file

@ -16,13 +16,13 @@ import {VariableUiElement} from "../Base/VariableUIElement";
export class ImageUploadFlow extends Toggle {
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
const perId = ImageUploadFlow.uploadCountsPerId
const id = tagsSource.data.id
if(!perId.has(id)){
if (!perId.has(id)) {
perId.set(id, new UIEventSource<number>(0))
}
const uploadedCount = perId.get(id)
@ -39,7 +39,7 @@ export class ImageUploadFlow extends Toggle {
key = imagePrefix + ":" + freeIndex;
}
console.log("Adding image:" + key, url);
uploadedCount.data ++
uploadedCount.data++
uploadedCount.ping()
Promise.resolve(State.state.changes
.applyAction(new ChangeTagAction(
@ -50,17 +50,17 @@ export class ImageUploadFlow extends Toggle {
}
)))
})
const licensePicker = new LicensePicker()
const t = Translations.t.image;
let labelContent : BaseUIElement
if(text === undefined) {
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
}else{
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
}
let labelContent: BaseUIElement
if (text === undefined) {
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
} else {
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
}
const label = new Combine([
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
labelContent
@ -74,17 +74,17 @@ export class ImageUploadFlow extends Toggle {
for (var i = 0; i < filelist.length; i++) {
const sizeInBytes= filelist[i].size
const sizeInBytes = filelist[i].size
console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes");
if(sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000){
if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) {
alert(Translations.t.image.toBig.Subs({
actual_size: (Math.floor(sizeInBytes / 1000000)) + "MB",
max_size: uploader.maxFileSizeInMegabytes+"MB"
max_size: uploader.maxFileSizeInMegabytes + "MB"
}).txt)
return;
}
}
console.log("Received images from the user, starting upload")
const license = licensePicker.GetValue()?.data ?? "CC0"
@ -114,31 +114,31 @@ export class ImageUploadFlow extends Toggle {
const uploadFlow: BaseUIElement = new Combine([
new VariableUiElement(uploader.queue.map(q => q.length).map(l => {
if(l == 0){
if (l == 0) {
return undefined;
}
if(l == 1){
return t.uploadingPicture.Clone().SetClass("alert")
}else{
if (l == 1) {
return t.uploadingPicture.Clone().SetClass("alert")
} else {
return t.uploadingMultiple.Subs({count: "" + l}).SetClass("alert")
}
})),
new VariableUiElement(uploader.failed.map(q => q.length).map(l => {
if(l==0){
if (l == 0) {
return undefined
}
return t.uploadFailed.Clone().SetClass("alert");
})),
new VariableUiElement(uploadedCount.map(l => {
if(l == 0){
return undefined;
if (l == 0) {
return undefined;
}
if(l == 1){
if (l == 1) {
return t.uploadDone.Clone().SetClass("thanks");
}
return t.uploadMultipleDone.Subs({count: l}).SetClass("thanks")
})),
fileSelector,
Translations.t.image.respectPrivacy.Clone().SetStyle("font-size:small;"),
licensePicker

View file

@ -15,9 +15,9 @@ export class FixedInputElement<T> extends InputElement<T> {
comparator: ((t0: T, t1: T) => boolean) = undefined) {
super();
this._comparator = comparator ?? ((t0, t1) => t0 == t1);
if(value instanceof UIEventSource){
if (value instanceof UIEventSource) {
this.value = value
}else{
} else {
this.value = new UIEventSource<T>(value);
}

View file

@ -45,7 +45,7 @@ export default class LengthInput extends InputElement<string> {
background: this.background,
allowMoving: false,
location: this._location,
attribution:true,
attribution: true,
leafletOptions: {
tap: true
}

View file

@ -96,22 +96,22 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
let min = undefined;
let matchedWay = undefined;
for (const feature of self._snapTo.data ?? []) {
try{
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
if (min === undefined) {
min = nearestPointOnLine
matchedWay = feature.feature;
continue;
}
try {
if (min.properties.dist > nearestPointOnLine.properties.dist) {
min = nearestPointOnLine
matchedWay = feature.feature;
const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat])
if (min === undefined) {
min = nearestPointOnLine
matchedWay = feature.feature;
continue;
}
}
}catch(e){
console.log("Snapping to a nearest point failed for ", feature.feature,"due to ", e)
if (min.properties.dist > nearestPointOnLine.properties.dist) {
min = nearestPointOnLine
matchedWay = feature.feature;
}
} catch (e) {
console.log("Snapping to a nearest point failed for ", feature.feature, "due to ", e)
}
}
@ -167,8 +167,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
installBounds(factor: number | BBox, showRange?: boolean): void {
this.map.installBounds(factor, showRange)
}
TakeScreenshot(): Promise<any> {
return this.map.TakeScreenshot()
return this.map.TakeScreenshot()
}
protected InnerConstructElement(): HTMLElement {

View file

@ -18,16 +18,8 @@ export default class Toggle extends VariableUiElement {
this.isEnabled = isEnabled
}
public ToggleOnClick(): Toggle {
const self = this;
this.onClick(() => {
self.isEnabled.setData(!self.isEnabled.data);
})
return this;
}
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
if(constructor === undefined){
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
if (constructor === undefined) {
return undefined
}
return new Toggle(
@ -35,6 +27,14 @@ export default class Toggle extends VariableUiElement {
undefined,
condition
)
}
public ToggleOnClick(): Toggle {
const self = this;
this.onClick(() => {
self.isEnabled.setData(!self.isEnabled.data);
})
return this;
}
}

View file

@ -271,7 +271,7 @@ export default class ValidatedTextField {
if (args[0]) {
zoom = Number(args[0])
if (isNaN(zoom)) {
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ",args[0]," (using 19 instead)")
console.error("Invalid zoom level for argument at 'length'-input. The offending argument is: ", args[0], " (using 19 instead)")
zoom = 19
}
}

View file

@ -5,9 +5,9 @@ import {VariableUiElement} from "../Base/VariableUIElement";
export default class VariableInputElement<T> extends InputElement<T> {
public readonly IsSelected: UIEventSource<boolean>;
private readonly value: UIEventSource<T>;
private readonly element: BaseUIElement
public readonly IsSelected: UIEventSource<boolean>;
private readonly upstream: UIEventSource<InputElement<T>>;
constructor(upstream: UIEventSource<InputElement<T>>) {
@ -23,13 +23,12 @@ export default class VariableInputElement<T> extends InputElement<T> {
return this.value;
}
protected InnerConstructElement(): HTMLElement {
return this.element.ConstructElement();
}
IsValid(t: T): boolean {
return this.upstream.data.IsValid(t);
}
protected InnerConstructElement(): HTMLElement {
return this.element.ConstructElement();
}
}

View file

@ -28,12 +28,12 @@ export default class OpeningHoursInput extends InputElement<string> {
this._value = value;
let valueWithoutPrefix = value
if (prefix !== "" && postfix !== "") {
valueWithoutPrefix = value.map(str => {
if (str === undefined) {
return undefined;
}
if(str === ""){
if (str === "") {
return ""
}
if (str.startsWith(prefix) && str.endsWith(postfix)) {
@ -44,7 +44,7 @@ export default class OpeningHoursInput extends InputElement<string> {
if (noPrefix === undefined) {
return undefined;
}
if(noPrefix === ""){
if (noPrefix === "") {
return ""
}
if (noPrefix.startsWith(prefix) && noPrefix.endsWith(postfix)) {

View file

@ -16,9 +16,9 @@ export default class EditableTagRendering extends Toggle {
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit [],
options:{
editMode? : UIEventSource<boolean> ,
innerElementClasses?: string
options: {
editMode?: UIEventSource<boolean>,
innerElementClasses?: string
}
) {
@ -28,7 +28,7 @@ export default class EditableTagRendering extends Toggle {
const renderingIsShown = tags.map(tags =>
configuration.IsKnown(tags) &&
(configuration?.condition?.matchesProperties(tags) ?? true))
super(
new Lazy(() => {
const editMode = options.editMode ?? new UIEventSource<boolean>(false)
@ -40,8 +40,8 @@ export default class EditableTagRendering extends Toggle {
renderingIsShown
)
}
private static CreateRendering(tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>) : BaseUIElement{
private static CreateRendering(tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>): BaseUIElement {
const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration)
answer.SetClass("w-full")
let rendering = answer;

View file

@ -77,23 +77,23 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
renderingsForGroup.push(questionBox)
} else {
let classes = innerClasses
let isHeader = renderingsForGroup.length === 0 && i > 0
if(isHeader){
let isHeader = renderingsForGroup.length === 0 && i > 0
if (isHeader) {
// This is the first element of a group!
// It should act as header and be sticky
classes= ""
classes = ""
}
const etr = new EditableTagRendering(tags, tr, layerConfig.units,{
const etr = new EditableTagRendering(tags, tr, layerConfig.units, {
innerElementClasses: innerClasses
})
if(isHeader){
if (isHeader) {
etr.SetClass("sticky top-0")
}
renderingsForGroup.push(etr)
}
}
allRenderings.push(...renderingsForGroup)
}

View file

@ -42,7 +42,7 @@ export default class MoveWizard extends Toggle {
changes: Changes,
layoutToUse: LayoutConfig,
allElements: ElementStorage
}, options : MoveConfig) {
}, options: MoveConfig) {
const t = Translations.t.move
const loginButton = new Toggle(
@ -64,7 +64,7 @@ export default class MoveWizard extends Toggle {
minZoom: 6
})
}
if(options.enableImproveAccuracy){
if (options.enableImproveAccuracy) {
reasons.push({
text: t.reasons.reasonInaccurate,
invitingText: t.inviteToMove.reasonInaccurate,
@ -79,8 +79,8 @@ export default class MoveWizard extends Toggle {
const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">("start")
const moveReason = new UIEventSource<MoveReason>(undefined)
let moveButton : BaseUIElement;
if(reasons.length === 1){
let moveButton: BaseUIElement;
if (reasons.length === 1) {
const reason = reasons[0]
moveReason.setData(reason)
moveButton = new SubtleButton(
@ -89,7 +89,7 @@ export default class MoveWizard extends Toggle {
).onClick(() => {
currentStep.setData("pick_location")
})
}else{
} else {
moveButton = new SubtleButton(
Svg.move_ui().SetStyle("height: 1.5rem; width: auto"),
t.inviteToMove.generic
@ -97,7 +97,7 @@ export default class MoveWizard extends Toggle {
currentStep.setData("reason")
})
}
const moveAgainButton = new SubtleButton(
Svg.move_ui(),
@ -107,8 +107,6 @@ export default class MoveWizard extends Toggle {
})
const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => {
moveReason.setData(r)
currentStep.setData("pick_location")
@ -129,16 +127,16 @@ export default class MoveWizard extends Toggle {
})
let background: string[]
if(typeof reason.background == "string"){
if (typeof reason.background == "string") {
background = [reason.background]
}else{
} else {
background = reason.background
}
const locationInput = new LocationInput({
minZoom: reason.minZoom,
centerLocation: loc,
mapBackground:AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background))
mapBackground: AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background))
})
if (reason.lockBounds) {
@ -198,8 +196,8 @@ export default class MoveWizard extends Toggle {
moveDisallowedReason.setData(t.isWay)
} else if (id.startsWith("relation")) {
moveDisallowedReason.setData(t.isRelation)
} else if(id.indexOf("-") < 0) {
} else if (id.indexOf("-") < 0) {
OsmObject.DownloadReferencingWays(id).then(referencing => {
if (referencing.length > 0) {
console.log("Got a referencing way, move not allowed")
@ -207,7 +205,7 @@ export default class MoveWizard extends Toggle {
}
})
OsmObject.DownloadReferencingRelations(id).then(partOf => {
if(partOf.length > 0){
if (partOf.length > 0) {
moveDisallowedReason.setData(t.partOfRelation)
}
})

View file

@ -32,6 +32,7 @@ export interface MultiApplyParams {
class MultiApplyExecutor {
private static executorCache = new Map<string, MultiApplyExecutor>()
private readonly originalValues = new Map<string, string>()
private readonly params: MultiApplyParams;
@ -48,7 +49,7 @@ class MultiApplyExecutor {
const self = this;
const relevantValues = p.tagsSource.map(tags => {
const currentValues = p.keysToApply.map(key => tags[key])
// By stringifying, we have a very clear ping when they changec
// By stringifying, we have a very clear ping when they changec
return JSON.stringify(currentValues);
})
relevantValues.addCallbackD(_ => {
@ -57,6 +58,15 @@ class MultiApplyExecutor {
}
}
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
if (MultiApplyExecutor.executorCache.has(id)) {
return MultiApplyExecutor.executorCache.get(id)
}
const applicator = new MultiApplyExecutor(params)
MultiApplyExecutor.executorCache.set(id, applicator)
return applicator
}
public applyTaggingOnOtherFeatures() {
console.log("Multi-applying changes...")
const featuresToChange = this.params.featureIds.data
@ -103,17 +113,6 @@ class MultiApplyExecutor {
}
}
private static executorCache = new Map<string, MultiApplyExecutor>()
public static GetApplicator(id: string, params: MultiApplyParams): MultiApplyExecutor {
if (MultiApplyExecutor.executorCache.has(id)) {
return MultiApplyExecutor.executorCache.get(id)
}
const applicator = new MultiApplyExecutor(params)
MultiApplyExecutor.executorCache.set(id, applicator)
return applicator
}
}
export default class MultiApply extends Toggle {

View file

@ -16,7 +16,7 @@ import Lazy from "../Base/Lazy";
export default class QuestionBox extends VariableUiElement {
constructor(tagsSource: UIEventSource<any>, tagRenderings: TagRenderingConfig[], units: Unit[]) {
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
tagRenderings = tagRenderings
@ -31,20 +31,20 @@ export default class QuestionBox extends VariableUiElement {
const tagRenderingQuestions = tagRenderings
.map((tagRendering, i) =>
new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering,
{
units: units,
afterSave: () => {
// We save and indicate progress by pinging and recalculating
skippedQuestions.ping();
},
cancelButton: Translations.t.general.skip.Clone()
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
skippedQuestions.data.push(i);
{
units: units,
afterSave: () => {
// We save and indicate progress by pinging and recalculating
skippedQuestions.ping();
})
}
)));
},
cancelButton: Translations.t.general.skip.Clone()
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
skippedQuestions.data.push(i);
skippedQuestions.ping();
})
}
)));
const skippedQuestionsButton = Translations.t.general.skippedQuestions
.onClick(() => {

View file

@ -23,7 +23,7 @@ export default class SplitRoadWizard extends Toggle {
source: {osmTags: "_cutposition=yes"},
mapRendering: [
{
location: ["point","centroid"],
location: ["point", "centroid"],
icon: {render: "circle:white;./assets/svg/scissors.svg"},
iconSize: {render: "30,30,center"}
}

View file

@ -1,4 +1,3 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
@ -20,6 +19,7 @@ We don't actually import it here. It is imported in the 'MinimapImplementation'-
*/
export default class ShowDataLayer {
private static dataLayerIds = 0
private readonly _leafletMap: UIEventSource<L.Map>;
private readonly _enablePopups: boolean;
private readonly _features: RenderingMultiPlexerFeatureSource
@ -30,7 +30,6 @@ export default class ShowDataLayer {
private _cleanCount = 0;
private geoLayer = undefined;
private isDirty = false;
/**
* If the selected element triggers, this is used to lookup the correct layer and to open the popup
* Used to avoid a lot of callbacks on the selected element
@ -39,9 +38,7 @@ export default class ShowDataLayer {
* @private
*/
private readonly leafletLayersPerId = new Map<string, { feature: any, leafletlayer: any }>()
private readonly showDataLayerid: number;
private static dataLayerIds = 0
constructor(options: ShowDataLayerOptions & { layerToShow: LayerConfig }) {
this._leafletMap = options.leafletMap;
@ -161,24 +158,24 @@ export default class ShowDataLayer {
const tagsSource = this.allElements?.addOrGetElement(feat) ?? new UIEventSource<any>(feat.properties);
let offsettedLine;
tagsSource
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
.map(tags => this._layerToShow.lineRendering[feat.lineRenderingIndex].GenerateLeafletStyle(tags))
.withEqualityStabilized((a, b) => {
if(a === b){
if (a === b) {
return true
}
if(a === undefined || b === undefined){
if (a === undefined || b === undefined) {
return false
}
return a.offset === b.offset && a.color === b.color && a.weight === b.weight && a.dashArray === b.dashArray
})
.addCallbackAndRunD(lineStyle => {
if (offsettedLine !== undefined) {
self.geoLayer.removeLayer(offsettedLine)
}
offsettedLine = L.polyline(coords, lineStyle);
this.postProcessFeature(feat, offsettedLine)
offsettedLine.addTo(this.geoLayer)
})
if (offsettedLine !== undefined) {
self.geoLayer.removeLayer(offsettedLine)
}
offsettedLine = L.polyline(coords, lineStyle);
this.postProcessFeature(feat, offsettedLine)
offsettedLine.addTo(this.geoLayer)
})
} else {
this.geoLayer.addData(feat);
}
@ -192,7 +189,7 @@ export default class ShowDataLayer {
const bounds = this.geoLayer.getBounds()
mp.fitBounds(bounds, {animate: false})
} catch (e) {
console.debug("Invalid bounds",e)
console.debug("Invalid bounds", e)
}
}
@ -292,7 +289,7 @@ export default class ShowDataLayer {
}
});
// Add the feature to the index to open the popup when needed
this.leafletLayersPerId.set(feature.properties.id + feature.geometry.type, {

View file

@ -1,19 +1,18 @@
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
import * as L from "leaflet";
export default class ShowOverlayLayer {
public static implementation: (config: TilesourceConfig,
leafletMap: UIEventSource<any>,
isShown?: UIEventSource<boolean>) => void;
constructor(config: TilesourceConfig,
leafletMap: UIEventSource<any>,
isShown: UIEventSource<boolean> = undefined) {
if(ShowOverlayLayer.implementation === undefined){
if (ShowOverlayLayer.implementation === undefined) {
throw "Call ShowOverlayLayerImplemenation.initialize() first before using this"
}
ShowOverlayLayer.implementation(config, leafletMap, isShown)
ShowOverlayLayer.implementation(config, leafletMap, isShown)
}
}

View file

@ -4,14 +4,14 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import ShowOverlayLayer from "./ShowOverlayLayer";
export default class ShowOverlayLayerImplementation {
public static Implement(){
public static Implement() {
ShowOverlayLayer.implementation = ShowOverlayLayerImplementation.AddToMap
}
public static AddToMap(config: TilesourceConfig,
leafletMap: UIEventSource<any>,
isShown: UIEventSource<boolean> = undefined){
isShown: UIEventSource<boolean> = undefined) {
leafletMap.map(leaflet => {
if (leaflet === undefined) {
return;
@ -41,5 +41,5 @@ export default class ShowOverlayLayerImplementation {
})
}
}

View file

@ -6,6 +6,7 @@ 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"
export default class ShowTileInfo {
public static readonly styling = new LayerConfig(
clusterstyle, "tileinfo", true)

View file

@ -10,6 +10,12 @@ import FilteredLayer from "../../Models/FilteredLayer";
* A feature source containing but a single feature, which keeps stats about a tile
*/
export class TileHierarchyAggregator implements FeatureSource {
private static readonly empty = []
public totalValue: number = 0
public showCount: number = 0
public hiddenCount: number = 0
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
public readonly name;
private _parent: TileHierarchyAggregator;
private _root: TileHierarchyAggregator;
private _z: number;
@ -17,21 +23,12 @@ export class TileHierarchyAggregator implements FeatureSource {
private _y: number;
private _tileIndex: number
private _counter: SingleTileCounter
private _subtiles: [TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator, TileHierarchyAggregator] = [undefined, undefined, undefined, undefined]
public totalValue: number = 0
public showCount: number = 0
public hiddenCount: number = 0
private static readonly empty = []
public readonly features = new UIEventSource<{ feature: any, freshness: Date }[]>(TileHierarchyAggregator.empty)
public readonly name;
private readonly featuresStatic = []
private readonly featureProperties: { count: string, kilocount: string, tileId: string, id: string, showCount: string, totalCount: string };
private readonly _state: { filteredLayers: UIEventSource<FilteredLayer[]> };
private readonly updateSignal = new UIEventSource<any>(undefined)
private constructor(parent: TileHierarchyAggregator,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>
@ -45,7 +42,7 @@ export class TileHierarchyAggregator implements FeatureSource {
this._y = y;
this._tileIndex = Tiles.tile_index(z, x, y)
this.name = "Count(" + this._tileIndex + ")"
const totals = {
id: "" + this._tileIndex,
tileId: "" + this._tileIndex,
@ -87,6 +84,10 @@ export class TileHierarchyAggregator implements FeatureSource {
this.featuresStatic.push({feature: box, freshness: now})
}
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
}
public getTile(tileIndex): TileHierarchyAggregator {
if (tileIndex === this._tileIndex) {
return this;
@ -103,6 +104,61 @@ export class TileHierarchyAggregator implements FeatureSource {
return this._subtiles[subtileIndex]?.getTile(tileIndex)
}
public addTile(source: FeatureSourceForLayer & Tiled) {
const self = this;
if (source.tileIndex === this._tileIndex) {
if (this._counter === undefined) {
this._counter = new SingleTileCounter(this._tileIndex)
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
}
this._counter.addTileCount(source)
} else {
// We have to give it to one of the subtiles
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
while (tileZ - 1 > this._z) {
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
tileZ--
}
const xDiff = tileX - (2 * this._x)
const yDiff = tileY - (2 * this._y)
const subtileIndex = yDiff * 2 + xDiff;
if (this._subtiles[subtileIndex] === undefined) {
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
}
this._subtiles[subtileIndex].addTile(source)
}
this.updateSignal.setData(source)
}
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
const self = this
const empty = []
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
if (targetZoom - 1 > clusteringConfig.maxZoom) {
return empty
}
const features = []
self.visitSubTiles(aggr => {
if (aggr.showCount < cutoff) {
return false
}
if (aggr._z === targetZoom) {
features.push(...aggr.features.data)
return false
}
return aggr._z <= targetZoom;
})
return features
}, [this.updateSignal.stabilized(500)])
return new StaticFeatureSource(features, true);
}
private update() {
const newMap = new Map<string, number>()
let total = 0
@ -162,71 +218,12 @@ export class TileHierarchyAggregator implements FeatureSource {
}
}
public addTile(source: FeatureSourceForLayer & Tiled) {
const self = this;
if (source.tileIndex === this._tileIndex) {
if (this._counter === undefined) {
this._counter = new SingleTileCounter(this._tileIndex)
this._counter.countsPerLayer.addCallbackAndRun(_ => self.update())
}
this._counter.addTileCount(source)
} else {
// We have to give it to one of the subtiles
let [tileZ, tileX, tileY] = Tiles.tile_from_index(source.tileIndex)
while (tileZ - 1 > this._z) {
tileX = Math.floor(tileX / 2)
tileY = Math.floor(tileY / 2)
tileZ--
}
const xDiff = tileX - (2 * this._x)
const yDiff = tileY - (2 * this._y)
const subtileIndex = yDiff * 2 + xDiff;
if (this._subtiles[subtileIndex] === undefined) {
this._subtiles[subtileIndex] = new TileHierarchyAggregator(this, this._state, tileZ, tileX, tileY)
}
this._subtiles[subtileIndex].addTile(source)
}
this.updateSignal.setData(source)
}
public static createHierarchy(state: { filteredLayers: UIEventSource<FilteredLayer[]> }) {
return new TileHierarchyAggregator(undefined, state, 0, 0, 0)
}
private visitSubTiles(f: (aggr: TileHierarchyAggregator) => boolean) {
const visitFurther = f(this)
if (visitFurther) {
this._subtiles.forEach(tile => tile?.visitSubTiles(f))
}
}
getCountsForZoom(clusteringConfig: { maxZoom: number }, locationControl: UIEventSource<{ zoom: number }>, cutoff: number = 0): FeatureSource {
const self = this
const empty = []
const features = locationControl.map(loc => loc.zoom).map(targetZoom => {
if (targetZoom - 1 > clusteringConfig.maxZoom) {
return empty
}
const features = []
self.visitSubTiles(aggr => {
if (aggr.showCount < cutoff) {
return false
}
if (aggr._z === targetZoom) {
features.push(...aggr.features.data)
return false
}
return aggr._z <= targetZoom;
})
return features
}, [this.updateSignal.stabilized(500)])
return new StaticFeatureSource(features, true);
}
}
/**
@ -236,11 +233,10 @@ class SingleTileCounter implements Tiled {
public readonly bbox: BBox;
public readonly tileIndex: number;
public readonly countsPerLayer: UIEventSource<Map<string, number>> = new UIEventSource<Map<string, number>>(new Map<string, number>())
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
public readonly z: number
public readonly x: number
public readonly y: number
private readonly registeredLayers: Map<string, LayerConfig> = new Map<string, LayerConfig>();
constructor(tileIndex: number) {
this.tileIndex = tileIndex

View file

@ -33,13 +33,10 @@ import AllKnownLayers from "../Customizations/AllKnownLayers";
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
import Link from "./Base/Link";
import List from "./Base/List";
import {OsmConnection} from "../Logic/Osm/OsmConnection";
import {SubtleButton} from "./Base/SubtleButton";
import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../Logic/Tags/And";
import Toggle from "./Input/Toggle";
import Img from "./Base/Img";
import FilteredLayer from "../Models/FilteredLayer";
import {DefaultGuiState} from "./DefaultGuiState";
export interface SpecialVisualization {
@ -568,22 +565,22 @@ export default class SpecialVisualizations {
}
const targetIdKey = args[3]
const t = Translations.t.general.apply_button
const tagsExplanation = new VariableUiElement(tagsToApply.map(tagsToApply => {
const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&");
let el: BaseUIElement = new FixedUiElement(tagsStr)
if(targetIdKey !== undefined){
const targetId = tags.data[targetIdKey] ?? tags.data.id
el = t.appliedOnAnotherObject.Subs({tags: tagsStr , id: targetId })
if (targetIdKey !== undefined) {
const targetId = tags.data[targetIdKey] ?? tags.data.id
el = t.appliedOnAnotherObject.Subs({tags: tagsStr, id: targetId})
}
return el;
}
)).SetClass("subtle")
const applied = new UIEventSource(false)
const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col"))
.onClick(() => {
const targetId = tags.data[ targetIdKey] ?? tags.data.id
const targetId = tags.data[targetIdKey] ?? tags.data.id
const changeAction = new ChangeTagAction(targetId,
new And(tagsToApply.data),
tags.data, // We pass in the tags of the selected element, not the tags of the target element!
@ -596,11 +593,11 @@ export default class SpecialVisualizations {
applied.setData(true)
})
return new Toggle(
new Toggle(
t.isApplied.SetClass("thanks"),
applyButton,
t.isApplied.SetClass("thanks"),
applyButton,
applied
)
, undefined, state.osmConnection.isLoggedIn)

View file

@ -4,7 +4,6 @@ import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
import {Translation} from "../i18n/Translation";
import {FixedUiElement} from "../Base/FixedUiElement";
import Loading from "../Base/Loading";
import {Transform} from "stream";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import Img from "../Base/Img";
@ -16,6 +15,43 @@ import {Utils} from "../../Utils";
export default class WikidataPreviewBox extends VariableUiElement {
private static isHuman = [
{p: 31/*is a*/, q: 5 /* human */},
]
// @ts-ignore
private static extraProperties: {
requires?: { p: number, q?: number }[],
property: string,
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
}[] = [
{
requires: WikidataPreviewBox.isHuman,
property: "P21",
display: new Map([
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
['Q1097630', () => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
['Q1052281', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
['Q2449503', () => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
['Q48270', () => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
])
},
{
property: "P569",
requires: WikidataPreviewBox.isHuman,
display: new Translation({
"*": "Born: {value}"
})
},
{
property: "P570",
requires: WikidataPreviewBox.isHuman,
display: new Translation({
"*": "Died: {value}"
})
}
]
constructor(wikidataId: UIEventSource<string>) {
let inited = false;
const wikidata = wikidataId
@ -45,6 +81,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
}))
}
// @ts-ignore
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement {
let link = new Link(
@ -57,7 +94,7 @@ export default class WikidataPreviewBox extends VariableUiElement {
let info = new Combine([
new Combine(
[Translation.fromMap(wikidata.labels)?.SetClass("font-bold"),
link]).SetClass("flex justify-between"),
link]).SetClass("flex justify-between"),
Translation.fromMap(wikidata.descriptions),
WikidataPreviewBox.QuickFacts(wikidata)
]).SetClass("flex flex-col link-underline")
@ -80,87 +117,49 @@ export default class WikidataPreviewBox extends VariableUiElement {
return info
}
private static isHuman = [
{p: 31/*is a*/, q: 5 /* human */},
]
// @ts-ignore
// @ts-ignore
private static extraProperties: {
requires?: { p: number, q?: number }[],
property: string,
display: Translation | Map<string, string | (() => BaseUIElement) /*If translation: Subs({value: * }) */>
}[] = [
{
requires: WikidataPreviewBox.isHuman,
property: "P21",
display: new Map([
['Q6581097', () => Svg.gender_male_ui().SetStyle("width: 1rem; height: auto")],
['Q6581072', () => Svg.gender_female_ui().SetStyle("width: 1rem; height: auto")],
['Q1097630',() => Svg.gender_inter_ui().SetStyle("width: 1rem; height: auto")],
['Q1052281',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transwomen'*/],
['Q2449503',() => Svg.gender_trans_ui().SetStyle("width: 1rem; height: auto")/*'transmen'*/],
['Q48270',() => Svg.gender_queer_ui().SetStyle("width: 1rem; height: auto")]
])
},
{
property: "P569",
requires: WikidataPreviewBox.isHuman,
display: new Translation({
"*":"Born: {value}"
})
},
{
property: "P570",
requires: WikidataPreviewBox.isHuman,
display: new Translation({
"*":"Died: {value}"
})
}
]
public static QuickFacts(wikidata: WikidataResponse): BaseUIElement {
const els : BaseUIElement[] = []
const els: BaseUIElement[] = []
for (const extraProperty of WikidataPreviewBox.extraProperties) {
let hasAllRequirements = true
for (const requirement of extraProperty.requires) {
if(!wikidata.claims?.has("P"+requirement.p)){
if (!wikidata.claims?.has("P" + requirement.p)) {
hasAllRequirements = false;
break
}
if(!wikidata.claims?.get("P"+requirement.p).has("Q"+requirement.q)){
if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) {
hasAllRequirements = false;
break
}
}
if(!hasAllRequirements){
if (!hasAllRequirements) {
continue
}
const key = extraProperty.property
const display = extraProperty.display
const value: string[] = Array.from(wikidata.claims.get(key))
if(value === undefined){
if (value === undefined) {
continue
}
if(display instanceof Translation){
if (display instanceof Translation) {
els.push(display.Subs({value: value.join(", ")}).SetClass("m-2"))
continue
}
const constructors = Utils.NoNull(value.map(property => display.get(property)))
const elems = constructors.map(v => {
if(typeof v === "string"){
if (typeof v === "string") {
return new FixedUiElement(v)
}else{
} else {
return v();
}
})
els.push(new Combine(elems).SetClass("flex m-2"))
}
if(els.length === 0){
if (els.length === 0) {
return undefined;
}
return new Combine(els).SetClass("flex")
}

View file

@ -88,7 +88,7 @@ export default class WikipediaBox extends Combine {
}
const wikidata = <WikidataResponse>maybewikidata["success"]
if(wikidata === undefined){
if (wikidata === undefined) {
return "failed"
}
if (wikidata.wikisites.size === 0) {
@ -118,13 +118,13 @@ export default class WikipediaBox extends Combine {
return wp.failed.Clone().SetClass("alert p-4")
}
if (status[0] == "no page") {
const [_, wd] = <[string, WikidataResponse]> status
const [_, wd] = <[string, WikidataResponse]>status
return new Combine([
WikidataPreviewBox.WikidataResponsePreview(wd),
wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4")
}
const [pagetitle, language, wd] = <[string, string, WikidataResponse]> status
const [pagetitle, language, wd] = <[string, string, WikidataResponse]>status
return WikipediaBox.createContents(pagetitle, language, wd)
})
@ -134,27 +134,27 @@ export default class WikipediaBox extends Combine {
const titleElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") {
const [pagetitle, _] = state
if(pagetitle === "no page"){
const wd = <WikidataResponse> state[1]
return new Title( Translation.fromMap(wd.labels),3)
if (pagetitle === "no page") {
const wd = <WikidataResponse>state[1]
return new Title(Translation.fromMap(wd.labels), 3)
}
return new Title(pagetitle, 3)
}
//return new Title(Translations.t.general.wikipedia.wikipediaboxTitle.Clone(), 2)
return new Title(wikidataId,3)
return new Title(wikidataId, 3)
}))
const linkElement = new VariableUiElement(wikiLink.map(state => {
if (typeof state !== "string") {
const [pagetitle, language] = state
if(pagetitle === "no page"){
const wd = <WikidataResponse> state[1]
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "),
"https://www.wikidata.org/wiki/"+wd.id
if (pagetitle === "no page") {
const wd = <WikidataResponse>state[1]
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "),
"https://www.wikidata.org/wiki/" + wd.id
, true)
}
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
return new Link(Svg.pop_out_ui().SetStyle("width: 1.2rem").SetClass("block "), url, true)
}
@ -202,7 +202,7 @@ export default class WikipediaBox extends Combine {
return new Combine([
quickFacts?.SetClass("border-2 border-grey rounded-lg m-1 mb-0"),
new VariableUiElement(contents)
.SetClass("block pl-6 pt-2")])
.SetClass("block pl-6 pt-2")])
}
}

View file

@ -34,6 +34,38 @@ export class Translation extends BaseUIElement {
return this.textFor(Translation.forcedLanguage ?? Locale.language.data)
}
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
const allTranslations: { context: string, tr: Translation }[] = []
for (const key in object) {
const v = object[key]
if (v === undefined || v === null) {
continue
}
if (v instanceof Translation) {
allTranslations.push({context: context + "." + key, tr: v})
continue
}
if (typeof v === "object") {
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
}
}
return allTranslations
}
static fromMap(transl: Map<string, string>) {
const translations = {}
let hasTranslation = false;
transl?.forEach((value, key) => {
translations[key] = value
hasTranslation = true
})
if (!hasTranslation) {
return undefined
}
return new Translation(translations);
}
public textFor(language: string): string {
if (this.translations["*"]) {
return this.translations["*"];
@ -195,36 +227,4 @@ export class Translation extends BaseUIElement {
}
return allIcons.filter(icon => icon != undefined)
}
static ExtractAllTranslationsFrom(object: any, context = ""): { context: string, tr: Translation }[] {
const allTranslations: { context: string, tr: Translation }[] = []
for (const key in object) {
const v = object[key]
if (v === undefined || v === null) {
continue
}
if (v instanceof Translation) {
allTranslations.push({context: context +"." + key, tr: v})
continue
}
if (typeof v === "object") {
allTranslations.push(...Translation.ExtractAllTranslationsFrom(v, context + "." + key))
continue
}
}
return allTranslations
}
static fromMap(transl: Map<string, string>) {
const translations = {}
let hasTranslation = false;
transl?.forEach((value, key) => {
translations[key] = value
hasTranslation = true
})
if(!hasTranslation){
return undefined
}
return new Translation(translations);
}
}

View file

@ -25,8 +25,8 @@ export default class Translations {
if (t === undefined || t === null) {
return undefined;
}
if(typeof t === "number"){
t = ""+t
if (typeof t === "number") {
t = "" + t
}
if (typeof t === "string") {
return new Translation({"*": t}, context);