Huge refactoring of state and initial UI setup

This commit is contained in:
Pieter Vander Vennet 2021-10-15 05:20:02 +02:00
parent 4e43673de5
commit eff6b5bfad
37 changed files with 5232 additions and 4907 deletions

20
UI/AllThemesGui.ts Normal file
View file

@ -0,0 +1,20 @@
import {FixedUiElement} from "./Base/FixedUiElement";
import State from "../State";
import Combine from "./Base/Combine";
import MoreScreen from "./BigComponents/MoreScreen";
import Translations from "./i18n/Translations";
import Constants from "../Models/Constants";
import UserRelatedState from "../Logic/State/UserRelatedState";
export default class AllThemesGui {
constructor() {
new FixedUiElement("").AttachTo("centermessage")
const state = new UserRelatedState(undefined);
new Combine([new MoreScreen(state, true),
Translations.t.general.aboutMapcomplete.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");
}
}

View file

@ -1,4 +1,3 @@
import State from "../../State";
import ThemeIntroductionPanel from "./ThemeIntroductionPanel";
import Svg from "../../Svg";
import Translations from "../i18n/Translations";
@ -8,31 +7,46 @@ import Constants from "../../Models/Constants";
import Combine from "../Base/Combine";
import {TabbedComponent} from "../Base/TabbedComponent";
import {UIEventSource} from "../../Logic/UIEventSource";
import UserDetails from "../../Logic/Osm/OsmConnection";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import BaseUIElement from "../BaseUIElement";
import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Utils} from "../../Utils";
import UserRelatedState from "../../Logic/State/UserRelatedState";
export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
constructor(isShown: UIEventSource<boolean>) {
const layoutToUse = State.state.layoutToUse;
constructor(isShown: UIEventSource<boolean>,
currentTab: UIEventSource<number>,
state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
} & UserRelatedState) {
const layoutToUse = state.layoutToUse;
super(
() => layoutToUse.title.Clone(),
() => FullWelcomePaneWithTabs.GenerateContents(layoutToUse, State.state.osmConnection.userDetails, isShown),
"welcome", isShown
() => FullWelcomePaneWithTabs.GenerateContents(state, currentTab, isShown),
undefined, isShown
)
}
private static ConstructBaseTabs(layoutToUse: LayoutConfig, isShown: UIEventSource<boolean>): { header: string | BaseUIElement; content: BaseUIElement }[] {
private static ConstructBaseTabs(state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
} & UserRelatedState,
isShown: UIEventSource<boolean>):
{ header: string | BaseUIElement; content: BaseUIElement }[] {
let welcome: BaseUIElement = new ThemeIntroductionPanel(isShown);
const tabs: { header: string | BaseUIElement, content: BaseUIElement }[] = [
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
{header: `<img src='${state.layoutToUse.icon}'>`, content: welcome},
{
header: Svg.osm_logo_img,
content: Translations.t.general.openStreetMapIntro.Clone().SetClass("link-underline")
@ -40,31 +54,36 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
]
if (State.state.featureSwitchShareScreen.data) {
if (state.featureSwitchShareScreen.data) {
tabs.push({header: Svg.share_img, content: new ShareScreen()});
}
if (State.state.featureSwitchMoreQuests.data) {
if (state.featureSwitchMoreQuests.data) {
tabs.push({
header: Svg.add_img,
content: new MoreScreen()
content: new MoreScreen(state)
});
}
return tabs;
}
private static GenerateContents(layoutToUse: LayoutConfig, userDetails: UIEventSource<UserDetails>, isShown: UIEventSource<boolean>) {
private static GenerateContents(state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
featureSwitchShareScreen: UIEventSource<boolean>,
featureSwitchMoreQuests: UIEventSource<boolean>
} & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) {
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(layoutToUse, isShown)]
const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)
const tabsWithAboutMc = [...FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown)]
const now = new Date()
const lastWeek = new Date(now.getDate() - 7 * 24 * 60 * 60 * 1000)
const date = lastWeek.getFullYear()+"-"+Utils.TwoDigits(lastWeek.getMonth()+1)+"-"+Utils.TwoDigits(lastWeek.getDate())
const date = lastWeek.getFullYear() + "-" + Utils.TwoDigits(lastWeek.getMonth() + 1) + "-" + Utils.TwoDigits(lastWeek.getDate())
const osmcha_link = `https://osmcha.org/?filters=%7B%22date__gte%22%3A%5B%7B%22label%22%3A%22${date}%22%2C%22value%22%3A%222021-01-01%22%7D%5D%2C%22editor%22%3A%5B%7B%22label%22%3A%22mapcomplete%22%2C%22value%22%3A%22mapcomplete%22%7D%5D%7D`
tabsWithAboutMc.push({
header: Svg.help,
content: new Combine([Translations.t.general.aboutMapcomplete.Clone()
@ -75,11 +94,11 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen {
tabs.forEach(c => c.content.SetClass("p-4"))
tabsWithAboutMc.forEach(c => c.content.SetClass("p-4"))
return new Toggle(
new TabbedComponent(tabsWithAboutMc, State.state.welcomeMessageOpenedTab),
new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab),
userDetails.map((userdetails: UserDetails) =>
new TabbedComponent(tabsWithAboutMc, currentTab),
new TabbedComponent(tabs, currentTab),
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
userdetails.loggedIn &&
userdetails.csCount >= Constants.userJourney.mapCompleteHelpUnlock)
)

View file

@ -2,7 +2,6 @@ import Combine from "../Base/Combine";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Translations from "../i18n/Translations";
import AttributionPanel from "./AttributionPanel";
import State from "../../State";
import ContributorCount from "../../Logic/ContributorCount";
import Toggle from "../Input/Toggle";
import MapControlButton from "../MapControlButton";
@ -13,16 +12,33 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import Loc from "../../Models/Loc";
import {BBox} from "../../Logic/BBox";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FilteredLayer from "../../Models/FilteredLayer";
export default class LeftControls extends Combine {
constructor(state: {featurePipeline: FeaturePipeline, currentBounds: UIEventSource<BBox>, locationControl: UIEventSource<Loc>, overlayToggles: any}) {
constructor(state: {
layoutToUse: LayoutConfig,
featurePipeline: FeaturePipeline,
currentBounds: UIEventSource<BBox>,
locationControl: UIEventSource<Loc>,
overlayToggles: any,
featureSwitchEnableExport: UIEventSource<boolean>,
featureSwitchExportAsPdf: UIEventSource<boolean>,
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
selectedElement: UIEventSource<any>
},
guiState: {
downloadControlIsOpened: UIEventSource<boolean>,
filterViewIsOpened: UIEventSource<boolean>,
}) {
const toggledCopyright = new ScrollableFullScreen(
() => Translations.t.general.attribution.attributionTitle.Clone(),
() =>
new AttributionPanel(
State.state.layoutToUse,
state.layoutToUse,
new ContributorCount(state).Contributors
),
undefined
@ -38,50 +54,50 @@ export default class LeftControls extends Combine {
const toggledDownload = new Toggle(
new AllDownloads(
State.state.downloadControlIsOpened
guiState.downloadControlIsOpened
).SetClass("block p-1 rounded-full"),
new MapControlButton(Svg.download_svg())
.onClick(() => State.state.downloadControlIsOpened.setData(true)),
State.state.downloadControlIsOpened
.onClick(() => guiState.downloadControlIsOpened.setData(true)),
guiState.downloadControlIsOpened
)
const downloadButtonn = new Toggle(
toggledDownload,
undefined,
State.state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || State.state.featureSwitchExportAsPdf.data,
[State.state.featureSwitchExportAsPdf])
state.featureSwitchEnableExport.map(downloadEnabled => downloadEnabled || state.featureSwitchExportAsPdf.data,
[state.featureSwitchExportAsPdf])
);
const toggledFilter = new Toggle(
new ScrollableFullScreen(
() => Translations.t.general.layerSelection.title.Clone(),
() =>
new FilterView(State.state.filteredLayers, state.overlayToggles).SetClass(
new FilterView(state.filteredLayers, state.overlayToggles).SetClass(
"block p-1 rounded-full"
),
undefined,
State.state.filterIsOpened
guiState.filterViewIsOpened
),
new MapControlButton(Svg.filter_svg())
.onClick(() => State.state.filterIsOpened.setData(true)),
State.state.filterIsOpened
.onClick(() => guiState.filterViewIsOpened.setData(true)),
guiState.filterViewIsOpened
)
const filterButton = new Toggle(
toggledFilter,
undefined,
State.state.featureSwitchFilter
state.featureSwitchFilter
);
State.state.locationControl.addCallback(() => {
state.locationControl.addCallback(() => {
// Close the layer selection when the map is moved
toggledDownload.isEnabled.setData(false);
copyrightButton.isEnabled.setData(false);
toggledFilter.isEnabled.setData(false);
});
State.state.selectedElement.addCallbackAndRunD((_) => {
state.selectedElement.addCallbackAndRunD((_) => {
toggledDownload.isEnabled.setData(false);
copyrightButton.isEnabled.setData(false);
toggledFilter.isEnabled.setData(false);

View file

@ -1,7 +1,6 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
import Svg from "../../Svg";
import State from "../../State";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import Translations from "../i18n/Translations";
@ -11,15 +10,21 @@ import LanguagePicker from "../LanguagePicker";
import IndexText from "./IndexText";
import BaseUIElement from "../BaseUIElement";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {UIEventSource} from "../../Logic/UIEventSource";
import Loc from "../../Models/Loc";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import UserRelatedState from "../../Logic/State/UserRelatedState";
import Toggle from "../Input/Toggle";
import {Utils} from "../../Utils";
import Title from "../Base/Title";
export default class MoreScreen extends Combine {
constructor(onMainScreen: boolean = false) {
super(MoreScreen.Init(onMainScreen, State.state));
}
private static Init(onMainScreen: boolean, state: State): BaseUIElement [] {
constructor(state: UserRelatedState & {
locationControl?: UIEventSource<Loc>,
layoutToUse?: LayoutConfig
}, onMainScreen: boolean = false) {
const tr = Translations.t.general.morescreen;
let intro: BaseUIElement = tr.intro.Clone();
let themeButtonStyle = ""
@ -35,30 +40,59 @@ export default class MoreScreen extends Combine {
themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
}
return [
super([
intro,
MoreScreen.createOfficialThemesList(state, themeButtonStyle).SetClass(themeListStyle),
MoreScreen.createUnofficialThemeList(themeButtonStyle)?.SetClass(themeListStyle),
MoreScreen.createPreviouslyVistedHiddenList(state, themeButtonStyle).SetClass(themeListStyle),
MoreScreen.createUnofficialThemeList(themeButtonStyle, state)?.SetClass(themeListStyle),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10")
];
]);
}
private static createUnofficialThemeList(buttonClass: string): BaseUIElement {
return new VariableUiElement(State.state.installedThemes.map(customThemes => {
private static createUnofficialThemeList(buttonClass: string, state: UserRelatedState): BaseUIElement {
return new VariableUiElement(state.installedThemes.map(customThemes => {
const els: BaseUIElement[] = []
if (customThemes.length > 0) {
els.push(Translations.t.general.customThemeIntro.Clone())
const customThemesElement = new Combine(
customThemes.map(theme => MoreScreen.createLinkButton(theme.layout, theme.definition)?.SetClass(buttonClass))
customThemes.map(theme => MoreScreen.createLinkButton(state, theme.layout, theme.definition)?.SetClass(buttonClass))
)
els.push(customThemesElement)
}
return els;
}));
}
private static createPreviouslyVistedHiddenList(state: UserRelatedState, buttonClass: string){
const t= Translations.t.general.morescreen
return new Toggle(
new Combine([
new Title(t.previouslyHiddenTitle.Clone()),
t.hiddenExplanation,
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map(allPreferences => {
const knownThemes = Utils.NoNull( Object.keys(allPreferences).filter(key => key.startsWith("hidden-theme-"))
.map(key => key.substr("hidden-theme-".length, key.length - "-enabled".length))
.map(theme => AllKnownLayouts.allKnownLayouts.get(theme) ))
return new Combine(knownThemes.map(layout =>
MoreScreen.createLinkButton(state, layout ).SetClass(buttonClass)
))
})
)]).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement {
}
private static createOfficialThemesList(state: { osmConnection: OsmConnection, locationControl?: UIEventSource<Loc> }, buttonClass: string): BaseUIElement {
let officialThemes = AllKnownLayouts.layoutsList
let buttons = officialThemes.map((layout) => {
@ -66,10 +100,10 @@ export default class MoreScreen extends Combine {
console.trace("Layout is undefined")
return undefined
}
const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass);
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass);
if (layout.id === personal.id) {
return new VariableUiElement(
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
state.osmConnection.userDetails.map(userdetails => userdetails.csCount)
.map(csCount => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
@ -91,7 +125,7 @@ export default class MoreScreen extends Combine {
/*
* 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: State): VariableUiElement {
private static createCustomGeneratorButton(state: { osmConnection: OsmConnection }): VariableUiElement {
const tr = Translations.t.general.morescreen;
return new VariableUiElement(
state.osmConnection.userDetails.map(userDetails => {
@ -111,13 +145,22 @@ export default class MoreScreen extends Combine {
/**
* Creates a button linking to the given theme
* @param layout
* @param customThemeDefinition
* @private
*/
private static createLinkButton(layout: LayoutConfig, customThemeDefinition: string = undefined): BaseUIElement {
if (layout === undefined) {
return undefined;
private static createLinkButton(
state: {
locationControl?: UIEventSource<Loc>,
layoutToUse?: LayoutConfig
}, layout: LayoutConfig, customThemeDefinition: string = undefined
):
BaseUIElement {
if (layout
===
undefined
) {
return
undefined;
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout);
@ -126,11 +169,11 @@ export default class MoreScreen extends Combine {
if (layout.hideFromOverview) {
return undefined;
}
if (layout.id === State.state.layoutToUse?.id) {
if (layout.id === state?.layoutToUse?.id) {
return undefined;
}
const currentLocation = State.state.locationControl;
const currentLocation = state?.locationControl;
let path = window.location.pathname;
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
@ -151,7 +194,7 @@ export default class MoreScreen extends Combine {
linkSuffix = `#${customThemeDefinition}`
}
const linkText = currentLocation.map(currentLocation => {
const linkText = currentLocation?.map(currentLocation => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
@ -160,7 +203,7 @@ export default class MoreScreen extends Combine {
.map(part => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${linkSuffix}`;
})
}) ?? new UIEventSource<string>(`${linkPrefix}${linkSuffix}`)
let description = Translations.WT(layout.shortDescription).Clone();

View file

@ -2,38 +2,38 @@ import Combine from "../Base/Combine";
import Toggle from "../Input/Toggle";
import MapControlButton from "../MapControlButton";
import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler";
import State from "../../State";
import Svg from "../../Svg";
import MapState from "../../Logic/State/MapState";
export default class RightControls extends Combine {
constructor() {
constructor(state:MapState) {
const geolocationButton = new Toggle(
new MapControlButton(
new GeoLocationHandler(
State.state.currentGPSLocation,
State.state.leafletMap,
State.state.layoutToUse
state.currentGPSLocation,
state.leafletMap,
state.layoutToUse
), {
dontStyle: true
}
),
undefined,
State.state.featureSwitchGeolocation
state.featureSwitchGeolocation
);
const plus = new MapControlButton(
Svg.plus_svg()
).onClick(() => {
State.state.locationControl.data.zoom++;
State.state.locationControl.ping();
state.locationControl.data.zoom++;
state.locationControl.ping();
});
const min = new MapControlButton(
Svg.min_svg()
).onClick(() => {
State.state.locationControl.data.zoom--;
State.state.locationControl.ping();
state.locationControl.data.zoom--;
state.locationControl.ping();
});
super([plus, min, geolocationButton].map(el => el.SetClass("m-0.5 md:m-1")))

View file

@ -2,7 +2,6 @@ import {UIEventSource} from "../../Logic/UIEventSource";
import {Translation} from "../i18n/Translation";
import {VariableUiElement} from "../Base/VariableUIElement";
import Svg from "../../Svg";
import State from "../../State";
import {TextField} from "../Input/TextField";
import {Geocoding} from "../../Logic/Osm/Geocoding";
import Translations from "../i18n/Translations";
@ -10,7 +9,10 @@ import Hash from "../../Logic/Web/Hash";
import Combine from "../Base/Combine";
export default class SearchAndGo extends Combine {
constructor() {
constructor(state: {
leafletMap: UIEventSource<any>,
selectedElement: UIEventSource<any>
}) {
const goButton = Svg.search_ui().SetClass(
"w-8 h-8 full-rounded border-black float-right"
);
@ -64,9 +66,9 @@ export default class SearchAndGo extends Combine {
[bb[0], bb[2]],
[bb[1], bb[3]],
];
State.state.selectedElement.setData(undefined);
state.selectedElement.setData(undefined);
Hash.hash.setData(poi.osm_type + "/" + poi.osm_id);
State.state.leafletMap.data.fitBounds(bounds);
state.leafletMap.data.fitBounds(bounds);
placeholder.setData(Translations.t.general.search.search);
},
() => {

View file

@ -4,7 +4,6 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import Svg from "../../Svg";
import {SubtleButton} from "../Base/SubtleButton";
import State from "../../State";
import Combine from "../Base/Combine";
import Translations from "../i18n/Translations";
import Constants from "../../Models/Constants";
@ -12,7 +11,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import UserDetails from "../../Logic/Osm/OsmConnection";
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
import LocationInput from "../Input/LocationInput";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
@ -20,6 +19,11 @@ import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import PresetConfig from "../../Models/ThemeConfig/PresetConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import {BBox} from "../../Logic/BBox";
import Loc from "../../Models/Loc";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import {ElementStorage} from "../../Logic/ElementStorage";
/*
* The SimpleAddUI is a single panel, which can have multiple states:
@ -38,9 +42,22 @@ interface PresetInfo extends PresetConfig {
export default class SimpleAddUI extends Toggle {
constructor(isShown: UIEventSource<boolean>) {
constructor(isShown: UIEventSource<boolean>,
filterViewIsOpened: UIEventSource<boolean>,
state: {
layoutToUse: LayoutConfig,
osmConnection: OsmConnection,
changes: Changes,
allElements: ElementStorage,
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
featurePipeline: FeaturePipeline,
selectedElement: UIEventSource<any>,
locationControl: UIEventSource<Loc>,
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
}) {
const loginButton = new SubtleButton(Svg.osm_logo_ui(), Translations.t.general.add.pleaseLogin.Clone())
.onClick(() => State.state.osmConnection.AttemptLogin());
.onClick(() => state.osmConnection.AttemptLogin());
const readYourMessages = new Combine([
Translations.t.general.readYourMessages.Clone().SetClass("alert"),
new SubtleButton(Svg.envelope_ui(),
@ -50,20 +67,21 @@ export default class SimpleAddUI extends Toggle {
const selectedPreset = new UIEventSource<PresetInfo>(undefined);
isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
State.state.LastClickLocation.addCallback( _ => selectedPreset.setData(undefined))
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset)
state.LastClickLocation.addCallback(_ => selectedPreset.setData(undefined))
const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset, state)
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
async function createNewPoint(tags: any[], location: { lat: number, lon: number }, snapOntoWay?: OsmWay) {
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: State.state?.layoutToUse?.id ?? "unkown",
theme: state.layoutToUse?.id ?? "unkown",
changeType: "create",
snapOnto: snapOntoWay})
await State.state.changes.applyAction(newElementAction)
snapOnto: snapOntoWay
})
await state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
isShown.setData(false)
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
}
@ -73,7 +91,7 @@ export default class SimpleAddUI extends Toggle {
if (preset === undefined) {
return presetsOverview
}
return SimpleAddUI.CreateConfirmButton(preset,
return SimpleAddUI.CreateConfirmButton(state, filterViewIsOpened, preset,
(tags, location, snapOntoWayId?: string) => {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
@ -97,18 +115,18 @@ export default class SimpleAddUI extends Toggle {
new Toggle(
addUi,
Translations.t.general.add.stillLoading.Clone().SetClass("alert"),
State.state.featurePipeline.somethingLoaded
state.featurePipeline.somethingLoaded
),
Translations.t.general.add.zoomInFurther.Clone().SetClass("alert"),
State.state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
state.locationControl.map(loc => loc.zoom >= Constants.userJourney.minZoomLevelToAddNewPoints)
),
readYourMessages,
State.state.osmConnection.userDetails.map((userdetails: UserDetails) =>
state.osmConnection.userDetails.map((userdetails: UserDetails) =>
userdetails.csCount >= Constants.userJourney.addNewPointWithUnreadMessagesUnlock ||
userdetails.unreadMessages == 0)
),
loginButton,
State.state.osmConnection.isLoggedIn
state.osmConnection.isLoggedIn
)
@ -116,11 +134,18 @@ export default class SimpleAddUI extends Toggle {
}
private static CreateConfirmButton(preset: PresetInfo,
private static CreateConfirmButton(
state: {
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
osmConnection: OsmConnection,
featurePipeline: FeaturePipeline
},
filterViewIsOpened: UIEventSource<boolean>,
preset: PresetInfo,
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
cancel: () => void): BaseUIElement {
let location = State.state.LastClickLocation;
let location = state.LastClickLocation;
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// We uncouple the event source
@ -143,7 +168,6 @@ export default class SimpleAddUI extends Toggle {
}
const tags = TagUtils.KVtoProperties(preset.tags ?? []);
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
@ -160,24 +184,24 @@ export default class SimpleAddUI extends Toggle {
if (preset.preciseInput.snapToLayers) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox : BBox= undefined
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD(bbox => {
if(loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)){
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(2);
loadedBbox = bbox;
const allFeatures: {feature: any}[] = []
const allFeatures: { feature: any }[] = []
preset.preciseInput.snapToLayers.forEach(layerId => {
State.state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature :f}))))
state.featurePipeline.GetFeaturesWithin(layerId, bbox).forEach(feats => allFeatures.push(...feats.map(f => ({feature: f}))))
})
snapToFeatures.setData(allFeatures)
})
}
}
@ -205,7 +229,7 @@ export default class SimpleAddUI extends Toggle {
Translations.t.general.add.openLayerControl
])
)
.onClick(() => State.state.filterIsOpened.setData(true))
.onClick(() => filterViewIsOpened.setData(true))
const openLayerOrConfirm = new Toggle(
@ -234,36 +258,35 @@ export default class SimpleAddUI extends Toggle {
openLayerOrConfirm,
disableFilter,
preset.layerToAddTo.appliedFilters.map(filters => {
if(filters === undefined || filters.length === 0){
if (filters === undefined || filters.length === 0) {
return true;
}
for (const filter of filters) {
if(filter.selected === 0 && filter.filter.options.length === 1){
if (filter.selected === 0 && filter.filter.options.length === 1) {
return false;
}
if(filter.selected !== undefined){
if (filter.selected !== undefined) {
const tags = filter.filter.options[filter.selected].osmTags
if(tags !== undefined && tags["and"]?.length !== 0){
if (tags !== undefined && tags["and"]?.length !== 0) {
// This actually doesn't filter anything at all
return false;
}
}
}
return true
})
)
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset);
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
const cancelButton = new SubtleButton(Svg.close_ui(),
Translations.t.general.cancel
).onClick(cancel)
return new Combine([
// Translations.t.general.add.confirmIntro.Subs({title: preset.name}),
State.state.osmConnection.userDetails.data.dryRun ?
state.osmConnection.userDetails.data.dryRun ?
Translations.t.general.testing.Clone().SetClass("alert") : undefined,
disableFiltersOrConfirm,
cancelButton,
@ -274,24 +297,29 @@ export default class SimpleAddUI extends Toggle {
}
private static CreateTagInfoFor(preset: PresetInfo, optionallyLinkToWiki = true) {
const csCount = State.state.osmConnection.userDetails.data.csCount;
private static CreateTagInfoFor(preset: PresetInfo, osmConnection: OsmConnection, optionallyLinkToWiki = true) {
const csCount = osmConnection.userDetails.data.csCount;
return new Toggle(
Translations.t.general.add.presetInfo.Subs({
tags: preset.tags.map(t => t.asHumanString(optionallyLinkToWiki && csCount > Constants.userJourney.tagsVisibleAndWikiLinked, true)).join("&"),
}).SetStyle("word-break: break-all"),
undefined,
State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
osmConnection.userDetails.map(userdetails => userdetails.csCount >= Constants.userJourney.tagsVisibleAt)
);
}
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
const presetButtons = SimpleAddUI.CreatePresetButtons(selectedPreset)
private static CreateAllPresetsPanel(selectedPreset: UIEventSource<PresetInfo>,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
osmConnection: OsmConnection
}): BaseUIElement {
const presetButtons = SimpleAddUI.CreatePresetButtons(state, selectedPreset)
let intro: BaseUIElement = Translations.t.general.add.intro;
let testMode: BaseUIElement = undefined;
if (State.state.osmConnection?.userDetails?.data?.dryRun) {
if (state.osmConnection?.userDetails?.data?.dryRun) {
testMode = Translations.t.general.testing.Clone().SetClass("alert")
}
@ -299,9 +327,9 @@ export default class SimpleAddUI extends Toggle {
}
private static CreatePresetSelectButton(preset: PresetInfo) {
private static CreatePresetSelectButton(preset: PresetInfo, osmConnection: OsmConnection) {
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, false);
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, osmConnection ,false);
return new SubtleButton(
preset.icon(),
new Combine([
@ -316,11 +344,17 @@ export default class SimpleAddUI extends Toggle {
/*
* Generates the list with all the buttons.*/
private static CreatePresetButtons(selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
private static CreatePresetButtons(
state: {
filteredLayers: UIEventSource<FilteredLayer[]>,
featureSwitchFilter: UIEventSource<boolean>,
osmConnection: OsmConnection
},
selectedPreset: UIEventSource<PresetInfo>): BaseUIElement {
const allButtons = [];
for (const layer of State.state.filteredLayers.data) {
for (const layer of state.filteredLayers.data) {
if (layer.isDisplayed.data === false && !State.state.featureSwitchFilter.data) {
if (layer.isDisplayed.data === false && !state.featureSwitchFilter.data) {
// The layer is not displayed and we cannot enable the layer control -> we skip
continue;
}
@ -346,7 +380,7 @@ export default class SimpleAddUI extends Toggle {
preciseInput: preset.preciseInput
}
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo);
const button = SimpleAddUI.CreatePresetSelectButton(presetInfo, state.osmConnection);
button.onClick(() => {
selectedPreset.setData(presetInfo)
})

View file

@ -1,6 +1,5 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import Svg from "../../Svg";
import State from "../../State";
import Combine from "../Base/Combine";
import {FixedUiElement} from "../Base/FixedUiElement";
import LanguagePicker from "../LanguagePicker";
@ -8,24 +7,25 @@ import Translations from "../i18n/Translations";
import Link from "../Base/Link";
import Toggle from "../Input/Toggle";
import Img from "../Base/Img";
import MapState from "../../Logic/State/MapState";
export default class UserBadge extends Toggle {
constructor() {
constructor(state: MapState) {
const userDetails = State.state.osmConnection.userDetails;
const userDetails = state.osmConnection.userDetails;
const loginButton = Translations.t.general.loginWithOpenStreetMap
.Clone()
.SetClass("userbadge-login inline-flex justify-center items-center w-full h-full text-lg font-bold min-w-[20em]")
.onClick(() => State.state.osmConnection.AttemptLogin());
.onClick(() => state.osmConnection.AttemptLogin());
const logout =
Svg.logout_svg()
.onClick(() => {
State.state.osmConnection.LogOut();
state.osmConnection.LogOut();
});
@ -39,15 +39,15 @@ export default class UserBadge extends Toggle {
return " ";
})
).onClick(() => {
const home = State.state.osmConnection.userDetails.data?.home;
const home = state.osmConnection.userDetails.data?.home;
if (home === undefined) {
return;
}
State.state.leafletMap.data.setView([home.lat, home.lon], 16);
state.leafletMap.data.setView([home.lat, home.lon], 16);
});
const linkStyle = "flex items-baseline"
const languagePicker = (LanguagePicker.CreateLanguagePicker(State.state.layoutToUse.language) ?? new FixedUiElement(""))
const languagePicker = (LanguagePicker.CreateLanguagePicker(state.layoutToUse.language) ?? new FixedUiElement(""))
.SetStyle("width:min-content;");
let messageSpan =
@ -129,7 +129,7 @@ export default class UserBadge extends Toggle {
super(
userBadge,
loginButton,
State.state.osmConnection.isLoggedIn
state.osmConnection.isLoggedIn
)

View file

@ -1,12 +1,11 @@
import Translations from "./i18n/Translations";
import State from "../State";
import {VariableUiElement} from "./Base/VariableUIElement";
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
export default class CenterMessageBox extends VariableUiElement {
constructor() {
const state = State.state;
const updater = State.state.featurePipeline;
constructor(state: FeaturePipelineState) {
const updater = state.featurePipeline;
const t = Translations.t.centerMessage;
const message = updater.runningQuery.map(
isRunning => {

161
UI/DefaultGUI.ts Normal file
View file

@ -0,0 +1,161 @@
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import State from "../State";
import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource";
import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs";
import MapControlButton from "./MapControlButton";
import Svg from "../Svg";
import Toggle from "./Input/Toggle";
import Hash from "../Logic/Web/Hash";
import {QueryParameters} from "../Logic/Web/QueryParameters";
import Constants from "../Models/Constants";
import UserBadge from "./BigComponents/UserBadge";
import SearchAndGo from "./BigComponents/SearchAndGo";
import Link from "./Base/Link";
import BaseUIElement from "./BaseUIElement";
import {VariableUiElement} from "./Base/VariableUIElement";
import LeftControls from "./BigComponents/LeftControls";
import RightControls from "./BigComponents/RightControls";
import CenterMessageBox from "./CenterMessageBox";
export class DefaultGuiState {
public readonly welcomeMessageIsOpened;
public readonly downloadControlIsOpened: UIEventSource<boolean>;
public readonly filterViewIsOpened: UIEventSource<boolean>;
public readonly welcomeMessageOpenedTab
constructor() {
this.filterViewIsOpened = QueryParameters.GetQueryParameter(
"filter-toggle",
"false",
"Whether or not the filter view is shown"
).map<boolean>(
(str) => str !== "false",
[],
(b) => "" + b
);
this.welcomeMessageIsOpened = new UIEventSource<boolean>(Hash.hash.data === undefined ||
Hash.hash.data === "" ||
Hash.hash.data == "welcome");
this.welcomeMessageOpenedTab = QueryParameters.GetQueryParameter(
"tab",
"0",
`The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`
).map<number>(
(str) => (isNaN(Number(str)) ? 0 : Number(str)),
[],
(n) => "" + n
);
this.downloadControlIsOpened =
QueryParameters.GetQueryParameter(
"download-control-toggle",
"false",
"Whether or not the download panel is shown"
).map<boolean>(
(str) => str !== "false",
[],
(b) => "" + b
);
}
}
/**
* The default MapComplete GUI initializor
*
* Adds a welcome pane, contorl buttons, ... etc to index.html
*/
export default class DefaultGUI {
private readonly _guiState: DefaultGuiState;
private readonly state: FeaturePipelineState;
constructor(state: FeaturePipelineState, guiState: DefaultGuiState) {
this.state = state;
this._guiState = guiState;
const self = this;
if (state.layoutToUse.customCss !== undefined) {
Utils.LoadCustomCss(state.layoutToUse.customCss);
}
// Attach the map
state.mainMapObject.SetClass("w-full h-full")
.AttachTo("leafletDiv")
state.setupClickDialogOnMap(
guiState.filterViewIsOpened,
state.leafletMap
)
this.InitWelcomeMessage();
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(self.InitWelcomeMessage(),
Toggle.If(state.featureSwitchIframePopoutEnabled, iframePopout),
state.featureSwitchWelcomeMessage
).AttachTo("messagesbox");
new LeftControls(state, guiState).AttachTo("bottom-left");
new RightControls(state).AttachTo("bottom-right");
State.state.locationControl.ping();
new CenterMessageBox(state).AttachTo("centermessage");
document
.getElementById("centermessage")
.classList.add("pointer-events-none");
}
private InitWelcomeMessage() {
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

@ -104,19 +104,7 @@ export default class ExportPDF {
})
const initialized =new Set()
for (const overlayToggle of State.state.overlayToggles) {
new ShowOverlayLayer(overlayToggle.config, minimap.leafletMap, overlayToggle.isDisplayed)
initialized.add(overlayToggle.config)
}
for (const tileLayerSource of State.state.layoutToUse.tileLayerSources) {
if (initialized.has(tileLayerSource)) {
continue
}
new ShowOverlayLayer(tileLayerSource, minimap.leafletMap)
}
State.state.AddAllOverlaysToMap(minimap.leafletMap)
}
private cleanup() {

View file

@ -176,7 +176,8 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
enablePopups: false,
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layers: State.state.filteredLayers
layers: State.state.filteredLayers,
allElements: State.state.allElements
}
)
// Show the central point
@ -191,7 +192,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO
enablePopups: false,
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layerToShow: this._matching_layer
layerToShow: this._matching_layer,
allElements: State.state.allElements,
selectedElement: State.state.selectedElement
})
}

View file

@ -1,6 +1,7 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Lazy from "../Base/Lazy";
/**
* The 'Toggle' is a UIElement showing either one of two elements, depending on the state.
@ -24,4 +25,16 @@ export default class Toggle extends VariableUiElement {
})
return this;
}
public static If(condition: UIEventSource<boolean>, constructor: () => BaseUIElement): BaseUIElement {
if(constructor === undefined){
return undefined
}
return new Toggle(
new Lazy(constructor),
undefined,
condition
)
}
}

View file

@ -68,7 +68,7 @@ export default class SplitRoadWizard extends Toggle {
leafletMap: miniMap.leafletMap,
zoomToFeatures: false,
enablePopups: false,
layerToShow: SplitRoadWizard.splitLayerStyling
layerToShow: SplitRoadWizard.splitLayerStyling,
})
new ShowDataMultiLayer({
@ -76,7 +76,8 @@ export default class SplitRoadWizard extends Toggle {
layers: State.state.filteredLayers,
leafletMap: miniMap.leafletMap,
enablePopups: false,
zoomToFeatures: true
zoomToFeatures: true,
allElements: State.state.allElements,
})
/**

View file

@ -4,9 +4,9 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import FeatureInfoBox from "../Popup/FeatureInfoBox";
import State from "../../State";
import {ShowDataLayerOptions} from "./ShowDataLayerOptions";
import {FixedUiElement} from "../Base/FixedUiElement";
import {ElementStorage} from "../../Logic/ElementStorage";
import Hash from "../../Logic/Web/Hash";
export default class ShowDataLayer {
@ -14,7 +14,8 @@ export default class ShowDataLayer {
private readonly _enablePopups: boolean;
private readonly _features: UIEventSource<{ feature: any }[]>
private readonly _layerToShow: LayerConfig;
private readonly _selectedElement: UIEventSource<any>
private readonly allElements : ElementStorage
// Used to generate a fresh ID when needed
private _cleanCount = 0;
private geoLayer = undefined;
@ -43,6 +44,8 @@ export default class ShowDataLayer {
const features = options.features.features.map(featFreshes => featFreshes.map(ff => ff.feature));
this._features = features;
this._layerToShow = options.layerToShow;
this._selectedElement = options.selectedElement
this.allElements = options.allElements;
const self = this;
options.leafletMap.addCallbackAndRunD(_ => {
@ -71,7 +74,7 @@ export default class ShowDataLayer {
})
State.state.selectedElement.addCallbackAndRunD(selected => {
this._selectedElement?.addCallbackAndRunD(selected => {
if (self._leafletMap.data === undefined) {
return;
}
@ -162,7 +165,7 @@ export default class ShowDataLayer {
private createStyleFor(feature) {
const tagsSource = State.state.allElements.addOrGetElement(feature);
const tagsSource = this.allElements?.addOrGetElement(feature) ?? new UIEventSource<any>(feature.properties.id);
// Every object is tied to exactly one layer
const layer = this._layerToShow
return layer?.GenerateLeafletStyle(tagsSource, true);
@ -178,10 +181,7 @@ export default class ShowDataLayer {
return;
}
let tagSource = State.state.allElements.getEventSourceById(feature.properties.id)
if (tagSource === undefined) {
tagSource = new UIEventSource<any>(feature.properties)
}
let tagSource = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties)
const clickable = !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)
const style = layer.GenerateLeafletStyle(tagSource, clickable);
const baseElement = style.icon.html;
@ -230,20 +230,20 @@ export default class ShowDataLayer {
popup.setContent(`<div style='height: 65vh' id='${id}'>Popup for ${feature.properties.id} ${feature.geometry.type} ${id} is loading</div>`)
leafletLayer.on("popupopen", () => {
if (infobox === undefined) {
const tags = State.state.allElements.getEventSourceById(feature.properties.id);
const tags = this.allElements?.getEventSourceById(feature.properties.id) ?? new UIEventSource<any>(feature.properties);
infobox = new FeatureInfoBox(tags, layer);
infobox.isShown.addCallback(isShown => {
if (!isShown) {
State.state.selectedElement.setData(undefined);
this._selectedElement?.setData(undefined);
leafletLayer.closePopup()
}
});
}
infobox.AttachTo(id)
infobox.Activate();
if (State.state?.selectedElement?.data?.properties?.id !== feature.properties.id) {
State.state.selectedElement.setData(feature)
if (this._selectedElement?.data?.properties?.id !== feature.properties.id) {
this._selectedElement?.setData(feature)
}
});
@ -254,6 +254,7 @@ export default class ShowDataLayer {
feature: feature,
leafletlayer: leafletLayer
})
}

View file

@ -1,8 +1,11 @@
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import {UIEventSource} from "../../Logic/UIEventSource";
import {ElementStorage} from "../../Logic/ElementStorage";
export interface ShowDataLayerOptions {
features: FeatureSource,
selectedElement?: UIEventSource<any>,
allElements?: ElementStorage,
leafletMap: UIEventSource<L.Map>,
enablePopups?: true | boolean,
zoomToFeatures?: false | boolean,

View file

@ -208,7 +208,8 @@ export default class SpecialVisualizations {
enablePopups: false,
zoomToFeatures: true,
layers: State.state.filteredLayers,
features: new StaticFeatureSource(featuresToShow, true)
features: new StaticFeatureSource(featuresToShow, true),
allElements: State.state.allElements
}
)