Merge branch 'develop' into feature/fitness-station

This commit is contained in:
Robin van der Linde 2022-10-11 14:45:53 +02:00
commit 4cb0cf8b8b
Signed by untrusted user: Robin-van-der-Linde
GPG key ID: 53956B3252478F0D
714 changed files with 93439 additions and 41823 deletions

View file

@ -1,66 +1,79 @@
import {SpecialVisualization} from "../SpecialVisualizations";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import BaseUIElement from "../BaseUIElement";
import {Stores, UIEventSource} from "../../Logic/UIEventSource";
import {DefaultGuiState} from "../DefaultGuiState";
import {SubtleButton} from "../Base/SubtleButton";
import Img from "../Base/Img";
import {FixedUiElement} from "../Base/FixedUiElement";
import Combine from "../Base/Combine";
import Link from "../Base/Link";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {Utils} from "../../Utils";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Translations from "../i18n/Translations";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import {UIElement} from "../UIElement";
import FilteredLayer from "../../Models/FilteredLayer";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import Lazy from "../Base/Lazy";
import List from "../Base/List";
import { SpecialVisualization } from "../SpecialVisualizations"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import BaseUIElement from "../BaseUIElement"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { DefaultGuiState } from "../DefaultGuiState"
import { SubtleButton } from "../Base/SubtleButton"
import Img from "../Base/Img"
import { FixedUiElement } from "../Base/FixedUiElement"
import Combine from "../Base/Combine"
import Link from "../Base/Link"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import { Utils } from "../../Utils"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Translations from "../i18n/Translations"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
import { UIElement } from "../UIElement"
import FilteredLayer from "../../Models/FilteredLayer"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Lazy from "../Base/Lazy"
import List from "../Base/List"
export interface AutoAction extends SpecialVisualization {
supportsAutoAction: boolean
applyActionOn(state: {
layoutToUse: LayoutConfig,
changes: Changes
}, tagSource: UIEventSource<any>, argument: string[]): Promise<void>
applyActionOn(
state: {
layoutToUse: LayoutConfig
changes: Changes
},
tagSource: UIEventSource<any>,
argument: string[]
): Promise<void>
}
class ApplyButton extends UIElement {
private readonly icon: string;
private readonly text: string;
private readonly targetTagRendering: string;
private readonly target_layer_id: string;
private readonly state: FeaturePipelineState;
private readonly target_feature_ids: string[];
private readonly buttonState = new UIEventSource<"idle" | "running" | "done" | { error: string }>("idle")
private readonly layer: FilteredLayer;
private readonly tagRenderingConfig: TagRenderingConfig;
private readonly icon: string
private readonly text: string
private readonly targetTagRendering: string
private readonly target_layer_id: string
private readonly state: FeaturePipelineState
private readonly target_feature_ids: string[]
private readonly buttonState = new UIEventSource<
"idle" | "running" | "done" | { error: string }
>("idle")
private readonly layer: FilteredLayer
private readonly tagRenderingConfig: TagRenderingConfig
constructor(state: FeaturePipelineState, target_feature_ids: string[], options: {
target_layer_id: string,
targetTagRendering: string,
text: string,
icon: string
}) {
constructor(
state: FeaturePipelineState,
target_feature_ids: string[],
options: {
target_layer_id: string
targetTagRendering: string
text: string
icon: string
}
) {
super()
this.state = state;
this.target_feature_ids = target_feature_ids;
this.target_layer_id = options.target_layer_id;
this.targetTagRendering = options.targetTagRendering;
this.state = state
this.target_feature_ids = target_feature_ids
this.target_layer_id = options.target_layer_id
this.targetTagRendering = options.targetTagRendering
this.text = options.text
this.icon = options.icon
this.layer = this.state.filteredLayers.data.find(l => l.layerDef.id === this.target_layer_id)
this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(tr => tr.id === this.targetTagRendering)
this.layer = this.state.filteredLayers.data.find(
(l) => l.layerDef.id === this.target_layer_id
)
this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(
(tr) => tr.id === this.targetTagRendering
)
}
protected InnerRender(): string | BaseUIElement {
@ -68,24 +81,25 @@ class ApplyButton extends UIElement {
return new FixedUiElement("No elements found to perform action")
}
if (this.tagRenderingConfig === undefined) {
return new FixedUiElement("Target tagrendering " + this.targetTagRendering + " not found").SetClass("alert")
return new FixedUiElement(
"Target tagrendering " + this.targetTagRendering + " not found"
).SetClass("alert")
}
const self = this;
const button = new SubtleButton(
new Img(this.icon),
this.text
).onClick(() => {
const self = this
const button = new SubtleButton(new Img(this.icon), this.text).onClick(() => {
this.buttonState.setData("running")
window.setTimeout(() => {
self.Run();
self.Run()
}, 50)
});
})
const explanation = new Combine(["The following objects will be updated: ",
...this.target_feature_ids.map(id => new Combine([new Link(id, "https:/ /openstreetmap.org/" + id, true), ", "]))]).SetClass("subtle")
const explanation = new Combine([
"The following objects will be updated: ",
...this.target_feature_ids.map(
(id) => new Combine([new Link(id, "https:/ /openstreetmap.org/" + id, true), ", "])
),
]).SetClass("subtle")
const previewMap = Minimap.createMiniMap({
allowMoving: false,
@ -93,7 +107,9 @@ class ApplyButton extends UIElement {
addLayerControl: true,
}).SetClass("h-48")
const features = this.target_feature_ids.map(id => this.state.allElements.ContainingFeatures.get(id))
const features = this.target_feature_ids.map((id) =>
this.state.allElements.ContainingFeatures.get(id)
)
new ShowDataLayer({
leafletMap: previewMap.leafletMap,
@ -103,11 +119,10 @@ class ApplyButton extends UIElement {
layerToShow: this.layer.layerDef,
})
return new VariableUiElement(this.buttonState.map(
st => {
return new VariableUiElement(
this.buttonState.map((st) => {
if (st === "idle") {
return new Combine([button, previewMap, explanation]);
return new Combine([button, previewMap, explanation])
}
if (st === "done") {
return new FixedUiElement("All done!").SetClass("thanks")
@ -116,26 +131,32 @@ class ApplyButton extends UIElement {
return new Loading("Applying changes...")
}
const error = st.error
return new Combine([new FixedUiElement("Something went wrong...").SetClass("alert"), new FixedUiElement(error).SetClass("subtle")]).SetClass("flex flex-col")
}
))
return new Combine([
new FixedUiElement("Something went wrong...").SetClass("alert"),
new FixedUiElement(error).SetClass("subtle"),
]).SetClass("flex flex-col")
})
)
}
private async Run() {
try {
console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
for (const targetFeatureId of this.target_feature_ids) {
const featureTags = this.state.allElements.getEventSourceById(targetFeatureId)
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
const specialRenderings = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering)
.map(x => x.special))
.filter(v => v.func["supportsAutoAction"] === true)
const specialRenderings = Utils.NoNull(
SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special)
).filter((v) => v.func["supportsAutoAction"] === true)
if (specialRenderings.length == 0) {
console.warn("AutoApply: feature " + targetFeatureId + " got a rendering without supported auto actions:", rendering)
console.warn(
"AutoApply: feature " +
targetFeatureId +
" got a rendering without supported auto actions:",
rendering
)
}
for (const specialRendering of specialRenderings) {
@ -148,45 +169,53 @@ class ApplyButton extends UIElement {
this.buttonState.setData("done")
} catch (e) {
console.error("Error while running autoApply: ", e)
this.buttonState.setData({error: e})
this.buttonState.setData({ error: e })
}
}
}
export default class AutoApplyButton implements SpecialVisualization {
public readonly docs: BaseUIElement;
public readonly funcName: string = "auto_apply";
public readonly args: { name: string; defaultValue?: string; doc: string, required?: boolean }[] = [
public readonly docs: BaseUIElement
public readonly funcName: string = "auto_apply"
public readonly args: {
name: string
defaultValue?: string
doc: string
required?: boolean
}[] = [
{
name: "target_layer",
doc: "The layer that the target features will reside in",
required: true
required: true,
},
{
name: "target_feature_ids",
doc: "The key, of which the value contains a list of ids",
required: true
required: true,
},
{
name: "tag_rendering_id",
doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed",
required: true
required: true,
},
{
name: "text",
doc: "The text to show on the button",
required: true
required: true,
},
{
name: "icon",
doc: "The icon to show on the button",
defaultValue: "./assets/svg/robot.svg"
}
];
defaultValue: "./assets/svg/robot.svg",
},
]
constructor(allSpecialVisualisations: SpecialVisualization[]) {
this.docs = AutoApplyButton.generateDocs(allSpecialVisualisations.filter(sv => sv["supportsAutoAction"] === true).map(sv => sv.funcName))
this.docs = AutoApplyButton.generateDocs(
allSpecialVisualisations
.filter((sv) => sv["supportsAutoAction"] === true)
.map((sv) => sv.funcName)
)
}
private static generateDocs(supportedActions: string[]) {
@ -194,21 +223,38 @@ export default class AutoApplyButton implements SpecialVisualization {
"A button to run many actions for many features at once.",
"To effectively use this button, you'll need some ingredients:",
new List([
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + supportedActions.join(", "),
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ", new Link("current_view","./BuiltinLayers.md#current_view"),
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
supportedActions.join(", "),
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
new Link("current_view", "./BuiltinLayers.md#current_view"),
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
"At last, add this component"
"At last, add this component",
]),
])
}
constr(state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState): BaseUIElement {
constr(
state: FeaturePipelineState,
tagSource: UIEventSource<any>,
argument: string[],
guistate: DefaultGuiState
): BaseUIElement {
try {
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
const t = Translations.t.general.add.import;
return new Combine([new FixedUiElement("The auto-apply button is only available in official themes (or in testing mode)").SetClass("alert"), t.howToTest])
if (
!state.layoutToUse.official &&
!(
state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
)
) {
const t = Translations.t.general.add.import
return new Combine([
new FixedUiElement(
"The auto-apply button is only available in official themes (or in testing mode)"
).SetClass("alert"),
t.howToTest,
])
}
const target_layer_id = argument[0]
@ -216,7 +262,10 @@ export default class AutoApplyButton implements SpecialVisualization {
const text = argument[3]
const icon = argument[4]
const options = {
target_layer_id, targetTagRendering, text, icon
target_layer_id,
targetTagRendering,
text,
icon,
}
return new Lazy(() => {
@ -227,25 +276,25 @@ export default class AutoApplyButton implements SpecialVisualization {
to_parse.setData(applicable)
})
const loading = new Loading("Gathering which elements support auto-apply... ");
return new VariableUiElement(to_parse.map(ids => {
if (ids === undefined) {
return loading
}
const loading = new Loading("Gathering which elements support auto-apply... ")
return new VariableUiElement(
to_parse.map((ids) => {
if (ids === undefined) {
return loading
}
return new ApplyButton(state, JSON.parse(ids), options);
}))
return new ApplyButton(state, JSON.parse(ids), options)
})
)
})
} catch (e) {
return new FixedUiElement("Could not generate a auto_apply-button for key " + argument[0] + " due to " + e).SetClass("alert")
return new FixedUiElement(
"Could not generate a auto_apply-button for key " + argument[0] + " due to " + e
).SetClass("alert")
}
}
getLayerDependencies(args: string[]): string[] {
return [args[0]]
}
}
}

View file

@ -1,30 +1,30 @@
import {VariableUiElement} from "../Base/VariableUIElement";
import Toggle from "../Input/Toggle";
import Translations from "../i18n/Translations";
import Svg from "../../Svg";
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import {Translation} from "../i18n/Translation";
import BaseUIElement from "../BaseUIElement";
import Constants from "../../Models/Constants";
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig";
import {OsmObject} from "../../Logic/Osm/OsmObject";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {InputElement} from "../Input/InputElement";
import {RadioButton} from "../Input/RadioButton";
import {FixedInputElement} from "../Input/FixedInputElement";
import Title from "../Base/Title";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import TagRenderingQuestion from "./TagRenderingQuestion";
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import Translations from "../i18n/Translations"
import Svg from "../../Svg"
import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
import Constants from "../../Models/Constants"
import DeleteConfig from "../../Models/ThemeConfig/DeleteConfig"
import { OsmObject } from "../../Logic/Osm/OsmObject"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { InputElement } from "../Input/InputElement"
import { RadioButton } from "../Input/RadioButton"
import { FixedInputElement } from "../Input/FixedInputElement"
import Title from "../Base/Title"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import TagRenderingQuestion from "./TagRenderingQuestion"
import {OsmId} from "../../Models/OsmFeature";
export default class DeleteWizard extends Toggle {
/**
* The UI-element which triggers 'deletion' (either soft or hard).
*
@ -44,11 +44,7 @@ export default class DeleteWizard extends Toggle {
* @param state: the state of the application
* @param options softDeletionTags: the tags to apply if the user doesn't have permission to delete, e.g. 'disused:amenity=public_bookcase', 'amenity='. After applying, the element should not be picked up on the map anymore. If undefined, the wizard will only show up if the point can be (hard) deleted
*/
constructor(id: string,
state: FeaturePipelineState,
options: DeleteConfig) {
constructor(id: OsmId, state: FeaturePipelineState, options: DeleteConfig) {
const deleteAbility = new DeleteabilityChecker(id, state, options.neededChangesets)
const tagsSource = state.allElements.getEventSourceById(id)
@ -57,239 +53,262 @@ export default class DeleteWizard extends Toggle {
const confirm = new UIEventSource<boolean>(false)
/**
* This function is the actual delete function
*/
function doDelete(selected: { deleteReason: string } | { retagTo: TagsFilter }) {
let actionToTake: OsmChangeAction;
let actionToTake: OsmChangeAction
if (selected["retagTo"] !== undefined) {
// no _delete_reason is given, which implies that this is _not_ a deletion but merely a retagging via a nonDeleteMapping
actionToTake = new ChangeTagAction(
id,
selected["retagTo"],
tagsSource.data,
{
theme: state?.layoutToUse?.id ?? "unkown",
changeType: "special-delete"
}
)
actionToTake = new ChangeTagAction(id, selected["retagTo"], tagsSource.data, {
theme: state?.layoutToUse?.id ?? "unkown",
changeType: "special-delete",
})
} else {
actionToTake = new DeleteAction(id,
actionToTake = new DeleteAction(
id,
options.softDeletionTags,
{
theme: state?.layoutToUse?.id ?? "unkown",
specialMotivation: selected["deleteReason"]
specialMotivation: selected["deleteReason"],
},
deleteAbility.canBeDeleted.data.canBeDeleted
)
}
state.changes?.applyAction(actionToTake)
isDeleted.setData(true)
}
const t = Translations.t.delete
const cancelButton = t.cancel.SetClass("block btn btn-secondary").onClick(() => confirm.setData(false));
const cancelButton = t.cancel
.SetClass("block btn btn-secondary")
.onClick(() => confirm.setData(false))
/**
* The button which is shown first. Opening it will trigger the check for deletions
*/
const deleteButton = new SubtleButton(
Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"), t.delete)
.onClick(
() => {
deleteAbility.CheckDeleteability(true)
confirm.setData(true);
}
)
Svg.delete_icon_svg().SetStyle("width: 1.5rem; height: 1.5rem;"),
t.delete
).onClick(() => {
deleteAbility.CheckDeleteability(true)
confirm.setData(true)
})
const isShown: Store<boolean> = tagsSource.map(tgs => tgs.id.indexOf("-") < 0)
const isShown: Store<boolean> = tagsSource.map((tgs) => tgs.id.indexOf("-") < 0)
const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state);
const deleteOptionPicker = DeleteWizard.constructMultipleChoice(options, tagsSource, state)
const deleteDialog = new Combine([
new Title(new SubstitutedTranslation(t.whyDelete, tagsSource, state)
.SetClass("question-text"), 3),
new Title(
new SubstitutedTranslation(t.whyDelete, tagsSource, state).SetClass(
"question-text"
),
3
),
deleteOptionPicker,
new Combine([
DeleteWizard.constructExplanation(deleteOptionPicker.GetValue(), deleteAbility, tagsSource, state),
DeleteWizard.constructExplanation(
deleteOptionPicker.GetValue(),
deleteAbility,
tagsSource,
state
),
new Combine([
cancelButton,
DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue())
.onClick(() => doDelete(deleteOptionPicker.GetValue().data))
]).SetClass("flex justify-end flex-wrap-reverse")
]).SetClass("flex mt-2 justify-between")
cancelButton,
DeleteWizard.constructConfirmButton(deleteOptionPicker.GetValue()).onClick(() =>
doDelete(deleteOptionPicker.GetValue().data)
),
]).SetClass("flex justify-end flex-wrap-reverse"),
]).SetClass("flex mt-2 justify-between"),
]).SetClass("question")
super(
new Toggle(
new Combine([Svg.delete_icon_svg().SetClass("h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"),
t.isDeleted]).SetClass("flex m-2 rounded-full"),
new Combine([
Svg.delete_icon_svg().SetClass(
"h-16 w-16 p-2 m-2 block bg-gray-300 rounded-full"
),
t.isDeleted,
]).SetClass("flex m-2 rounded-full"),
new Toggle(
new Toggle(
new Toggle(
new Toggle(
deleteDialog,
new SubtleButton(Svg.envelope_ui(), t.readMessages),
state.osmConnection.userDetails.map(ud => ud.csCount > Constants.userJourney.addNewPointWithUnreadMessagesUnlock || ud.unreadMessages == 0)
state.osmConnection.userDetails.map(
(ud) =>
ud.csCount >
Constants.userJourney
.addNewPointWithUnreadMessagesUnlock ||
ud.unreadMessages == 0
)
),
deleteButton,
confirm),
new VariableUiElement(deleteAbility.canBeDeleted.map(cbd =>
new Combine([
Svg.delete_not_allowed_svg().SetStyle("height: 2rem; width: auto").SetClass("mr-2"),
confirm
),
new VariableUiElement(
deleteAbility.canBeDeleted.map((cbd) =>
new Combine([
t.cannotBeDeleted,
cbd.reason.SetClass("subtle"),
t.useSomethingElse.SetClass("subtle")]).SetClass("flex flex-col")
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200")))
Svg.delete_not_allowed_svg()
.SetStyle("height: 2rem; width: auto")
.SetClass("mr-2"),
new Combine([
t.cannotBeDeleted,
cbd.reason.SetClass("subtle"),
t.useSomethingElse.SetClass("subtle"),
]).SetClass("flex flex-col"),
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200 bg-gray-200")
)
),
,
deleteAbility.canBeDeleted.map(cbd => allowSoftDeletion || cbd.canBeDeleted !== false)),
deleteAbility.canBeDeleted.map(
(cbd) => allowSoftDeletion || cbd.canBeDeleted !== false
)
),
t.loginToDelete.onClick(state.osmConnection.AttemptLogin),
state.osmConnection.isLoggedIn
),
isDeleted),
isDeleted
),
undefined,
isShown)
isShown
)
}
private static constructConfirmButton(deleteReasons: UIEventSource<any | undefined>): BaseUIElement {
const t = Translations.t.delete;
private static constructConfirmButton(
deleteReasons: UIEventSource<any | undefined>
): BaseUIElement {
const t = Translations.t.delete
const btn = new Combine([
Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"),
t.delete
t.delete,
]).SetClass("flex btn bg-red-500")
const btnNonActive = new Combine([
Svg.delete_icon_ui().SetClass("w-6 h-6 mr-3 block"),
t.delete
t.delete,
]).SetClass("flex btn btn-disabled bg-red-200")
return new Toggle(
btn,
btnNonActive,
deleteReasons.map(reason => reason !== undefined)
deleteReasons.map((reason) => reason !== undefined)
)
}
private static constructExplanation(
selectedOption: UIEventSource<{ deleteReason: string } | { retagTo: TagsFilter }>,
deleteAction: DeleteabilityChecker,
currentTags: UIEventSource<object>,
state?: { osmConnection?: OsmConnection }
) {
const t = Translations.t.delete
return new VariableUiElement(
selectedOption.map(
(selectedOption) => {
if (selectedOption === undefined) {
return t.explanations.selectReason.SetClass("subtle")
}
private static constructExplanation(selectedOption: UIEventSource<
{deleteReason: string} | {retagTo: TagsFilter}>, deleteAction: DeleteabilityChecker,
currentTags: UIEventSource<object>,
state?: {osmConnection?: OsmConnection}) {
const t = Translations.t.delete;
return new VariableUiElement(selectedOption.map(
selectedOption => {
if (selectedOption === undefined) {
return t.explanations.selectReason.SetClass("subtle");
}
const retag: TagsFilter | undefined = selectedOption["retagTo"]
if (retag !== undefined) {
// This is a retagging, not a deletion of any kind
return new Combine([
t.explanations.retagNoOtherThemes,
TagRenderingQuestion.CreateTagExplanation(
new UIEventSource<TagsFilter>(retag),
currentTags,
state
).SetClass("subtle"),
])
}
const retag: TagsFilter | undefined = selectedOption["retagTo"]
if(retag !== undefined) {
// This is a retagging, not a deletion of any kind
return new Combine([t.explanations.retagNoOtherThemes,
TagRenderingQuestion.CreateTagExplanation(new UIEventSource<TagsFilter>(retag),
currentTags, state
).SetClass("subtle")
])
}
const deleteReason = selectedOption["deleteReason"];
if(deleteReason !== undefined){
return new VariableUiElement(deleteAction.canBeDeleted.map(({
canBeDeleted, reason
}) => {
if(canBeDeleted){
// This is a hard delete for which we give an explanation
return t.explanations.hardDelete;
}
// This is a soft deletion: we explain _why_ the deletion is soft
return t.explanations.softDelete.Subs({reason: reason})
}))
}
}
, [deleteAction.canBeDeleted]
)).SetClass("block")
const deleteReason = selectedOption["deleteReason"]
if (deleteReason !== undefined) {
return new VariableUiElement(
deleteAction.canBeDeleted.map(({ canBeDeleted, reason }) => {
if (canBeDeleted) {
// This is a hard delete for which we give an explanation
return t.explanations.hardDelete
}
// This is a soft deletion: we explain _why_ the deletion is soft
return t.explanations.softDelete.Subs({ reason: reason })
})
)
}
},
[deleteAction.canBeDeleted]
)
).SetClass("block")
}
private static constructMultipleChoice(config: DeleteConfig, tagsSource: UIEventSource<Record<string, string>>, state: FeaturePipelineState):
InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[ ] = []
private static constructMultipleChoice(
config: DeleteConfig,
tagsSource: UIEventSource<Record<string, string>>,
state: FeaturePipelineState
): InputElement<{ deleteReason: string } | { retagTo: TagsFilter }> {
const elements: InputElement<{ deleteReason: string } | { retagTo: TagsFilter }>[] = []
for (const nonDeleteOption of config.nonDeleteMappings) {
elements.push(new FixedInputElement(
new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state),
{
retagTo: nonDeleteOption.if
}
))
elements.push(
new FixedInputElement(
new SubstitutedTranslation(nonDeleteOption.then, tagsSource, state),
{
retagTo: nonDeleteOption.if,
}
)
)
}
for (const extraDeleteReason of (config.extraDeleteReasons ?? [])) {
elements.push(new FixedInputElement(
new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state),
{
deleteReason: extraDeleteReason.changesetMessage
}
))
for (const extraDeleteReason of config.extraDeleteReasons ?? []) {
elements.push(
new FixedInputElement(
new SubstitutedTranslation(extraDeleteReason.explanation, tagsSource, state),
{
deleteReason: extraDeleteReason.changesetMessage,
}
)
)
}
for (const extraDeleteReason of DeleteConfig.defaultDeleteReasons) {
elements.push(new FixedInputElement(
extraDeleteReason.explanation.Clone(/*Must clone here, as this explanation might be used on many locations*/),
{
deleteReason: extraDeleteReason.changesetMessage
}
))
elements.push(
new FixedInputElement(
extraDeleteReason.explanation.Clone(/*Must clone here, as this explanation might be used on many locations*/),
{
deleteReason: extraDeleteReason.changesetMessage,
}
)
)
}
return new RadioButton(elements, {selectFirstAsDefault: false});
return new RadioButton(elements, { selectFirstAsDefault: false })
}
}
class DeleteabilityChecker {
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean, reason: Translation }>;
private readonly _id: string;
private readonly _allowDeletionAtChangesetCount: number;
public readonly canBeDeleted: UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>
private readonly _id: string
private readonly _allowDeletionAtChangesetCount: number
private readonly _state: {
osmConnection: OsmConnection
};
}
constructor(id: string,
state: { osmConnection: OsmConnection },
allowDeletionAtChangesetCount?: number) {
this._id = id;
this._state = state;
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE;
constructor(
id: string,
state: { osmConnection: OsmConnection },
allowDeletionAtChangesetCount?: number
) {
this._id = id
this._state = state
this._allowDeletionAtChangesetCount = allowDeletionAtChangesetCount ?? Number.MAX_VALUE
this.canBeDeleted = new UIEventSource<{ canBeDeleted?: boolean; reason: Translation }>({
canBeDeleted: undefined,
reason: Translations.t.delete.loading
reason: Translations.t.delete.loading,
})
this.CheckDeleteability(false)
}
@ -301,145 +320,151 @@ class DeleteabilityChecker {
* @private
*/
public CheckDeleteability(useTheInternet: boolean): void {
const t = Translations.t.delete;
const id = this._id;
const t = Translations.t.delete
const id = this._id
const state = this.canBeDeleted
const self = this;
const self = this
if (!id.startsWith("node")) {
this.canBeDeleted.setData({
canBeDeleted: false,
reason: t.isntAPoint
reason: t.isntAPoint,
})
return;
return
}
// Does the currently logged in user have enough experience to delete this point?
const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map(ud => {
const deletingPointsOfOtherAllowed = this._state.osmConnection.userDetails.map((ud) => {
if (ud === undefined) {
return undefined;
return undefined
}
if (!ud.loggedIn) {
return false;
return false
}
return ud.csCount >= Math.min(Constants.userJourney.deletePointsOfOthersUnlock, this._allowDeletionAtChangesetCount);
return (
ud.csCount >=
Math.min(
Constants.userJourney.deletePointsOfOthersUnlock,
this._allowDeletionAtChangesetCount
)
)
})
const previousEditors = new UIEventSource<number[]>(undefined)
const allByMyself = previousEditors.map(previous => {
if (previous === null || previous === undefined) {
// Not yet downloaded
return null;
}
const userId = self._state.osmConnection.userDetails.data.uid;
return !previous.some(editor => editor !== userId)
}, [self._state.osmConnection.userDetails])
const allByMyself = previousEditors.map(
(previous) => {
if (previous === null || previous === undefined) {
// Not yet downloaded
return null
}
const userId = self._state.osmConnection.userDetails.data.uid
return !previous.some((editor) => editor !== userId)
},
[self._state.osmConnection.userDetails]
)
// User allowed OR only edited by self?
const deletetionAllowed = deletingPointsOfOtherAllowed.map(isAllowed => {
if (isAllowed === undefined) {
// No logged in user => definitively not allowed to delete!
return false;
}
if (isAllowed === true) {
return true;
}
const deletetionAllowed = deletingPointsOfOtherAllowed.map(
(isAllowed) => {
if (isAllowed === undefined) {
// No logged in user => definitively not allowed to delete!
return false
}
if (isAllowed === true) {
return true
}
// At this point, the logged in user is not allowed to delete points created/edited by _others_
// however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
// At this point, the logged in user is not allowed to delete points created/edited by _others_
// however, we query OSM and if it turns out the current point has only be edited by the current user, deletion is allowed after all!
if (allByMyself.data === null && useTheInternet) {
// We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
const hist = OsmObject.DownloadHistory(id).map(versions => versions.map(version => version.tags["_last_edit:contributor:uid"]))
hist.addCallbackAndRunD(hist => previousEditors.setData(hist))
}
if (allByMyself.data === true) {
// Yay! We can download!
return true;
}
if (allByMyself.data === false) {
// Nope, downloading not allowed...
return false;
}
if (allByMyself.data === null && useTheInternet) {
// We kickoff the download here as it hasn't yet been downloaded. Note that this is mapped onto 'all by myself' above
const hist = OsmObject.DownloadHistory(id).map((versions) =>
versions.map((version) =>
Number(version.tags["_last_edit:contributor:uid"])
)
)
hist.addCallbackAndRunD((hist) => previousEditors.setData(hist))
}
if (allByMyself.data === true) {
// Yay! We can download!
return true
}
if (allByMyself.data === false) {
// Nope, downloading not allowed...
return false
}
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
return undefined;
}, [allByMyself])
// At this point, we don't have enough information yet to decide if the user is allowed to delete the current point...
return undefined
},
[allByMyself]
)
const hasRelations: UIEventSource<boolean> = new UIEventSource<boolean>(null)
const hasWays: UIEventSource<boolean> = new UIEventSource<boolean>(null)
deletetionAllowed.addCallbackAndRunD(deletetionAllowed => {
deletetionAllowed.addCallbackAndRunD((deletetionAllowed) => {
if (deletetionAllowed === false) {
// Nope, we are not allowed to delete
state.setData({
canBeDeleted: false,
reason: t.notEnoughExperience
reason: t.notEnoughExperience,
})
return true; // unregister this caller!
return true // unregister this caller!
}
if (!useTheInternet) {
return;
return
}
// All right! We have arrived at a point that we should query OSM again to check that the point isn't a part of ways or relations
OsmObject.DownloadReferencingRelations(id).then(rels => {
OsmObject.DownloadReferencingRelations(id).then((rels) => {
hasRelations.setData(rels.length > 0)
})
OsmObject.DownloadReferencingWays(id).then(ways => {
OsmObject.DownloadReferencingWays(id).then((ways) => {
hasWays.setData(ways.length > 0)
})
return true; // unregister to only run once
return true // unregister to only run once
})
const hasWaysOrRelations = hasRelations.map(hasRelationsData => {
if (hasRelationsData === true) {
return true;
}
if (hasWays.data === true) {
return true;
}
if (hasWays.data === null || hasRelationsData === null) {
return null;
}
if (hasWays.data === false && hasRelationsData === false) {
return false;
}
return null;
}, [hasWays])
hasWaysOrRelations.addCallbackAndRun(
waysOrRelations => {
if (waysOrRelations == null) {
// Not yet loaded - we still wait a little bit
return;
const hasWaysOrRelations = hasRelations.map(
(hasRelationsData) => {
if (hasRelationsData === true) {
return true
}
if (waysOrRelations) {
// not deleteble by mapcomplete
state.setData({
canBeDeleted: false,
reason: t.partOfOthers
})
} else {
// alright, this point can be safely deleted!
state.setData({
canBeDeleted: true,
reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete
})
if (hasWays.data === true) {
return true
}
}
if (hasWays.data === null || hasRelationsData === null) {
return null
}
if (hasWays.data === false && hasRelationsData === false) {
return false
}
return null
},
[hasWays]
)
hasWaysOrRelations.addCallbackAndRun((waysOrRelations) => {
if (waysOrRelations == null) {
// Not yet loaded - we still wait a little bit
return
}
if (waysOrRelations) {
// not deleteble by mapcomplete
state.setData({
canBeDeleted: false,
reason: t.partOfOthers,
})
} else {
// alright, this point can be safely deleted!
state.setData({
canBeDeleted: true,
reason: allByMyself.data === true ? t.onlyEditedByLoggedInUser : t.safeDelete,
})
}
})
}
}
}

View file

@ -1,45 +1,52 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
import Svg from "../../Svg";
import Toggle from "../Input/Toggle";
import BaseUIElement from "../BaseUIElement";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {Unit} from "../../Models/Unit";
import Lazy from "../Base/Lazy";
import {FixedUiElement} from "../Base/FixedUiElement";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import { UIEventSource } from "../../Logic/UIEventSource"
import TagRenderingQuestion from "./TagRenderingQuestion"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import TagRenderingAnswer from "./TagRenderingAnswer"
import Svg from "../../Svg"
import Toggle from "../Input/Toggle"
import BaseUIElement from "../BaseUIElement"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Unit } from "../../Models/Unit"
import Lazy from "../Base/Lazy"
import { FixedUiElement } from "../Base/FixedUiElement"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
export default class EditableTagRendering extends Toggle {
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit [],
state,
options: {
editMode?: UIEventSource<boolean>,
innerElementClasses?: string
}
constructor(
tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit[],
state,
options: {
editMode?: UIEventSource<boolean>
innerElementClasses?: string
}
) {
// The tagrendering is hidden if:
// - The answer is unknown. The questionbox will then show the question
// - There is a condition hiding the answer
const renderingIsShown = tags.map(tags =>
configuration.IsKnown(tags) &&
(configuration?.condition?.matchesProperties(tags) ?? true))
const renderingIsShown = tags.map(
(tags) =>
configuration.IsKnown(tags) &&
(configuration?.condition?.matchesProperties(tags) ?? true)
)
super(
new Lazy(() => {
const editMode = options.editMode ?? new UIEventSource<boolean>(false)
let rendering = EditableTagRendering.CreateRendering(state, tags, configuration, units, editMode);
let rendering = EditableTagRendering.CreateRendering(
state,
tags,
configuration,
units,
editMode
)
rendering.SetClass(options.innerElementClasses)
if(state.featureSwitchIsDebugging.data || state.featureSwitchIsTesting.data){
if (state.featureSwitchIsDebugging.data || state.featureSwitchIsTesting.data) {
rendering = new Combine([
new FixedUiElement(configuration.id).SetClass("self-end subtle"),
rendering
rendering,
]).SetClass("flex flex-col")
}
return rendering
@ -49,45 +56,51 @@ export default class EditableTagRendering extends Toggle {
)
}
private static CreateRendering(state: FeaturePipelineState, tags: UIEventSource<any>, configuration: TagRenderingConfig, units: Unit[], editMode: UIEventSource<boolean>): BaseUIElement {
private static CreateRendering(
state: FeaturePipelineState,
tags: UIEventSource<any>,
configuration: TagRenderingConfig,
units: Unit[],
editMode: UIEventSource<boolean>
): BaseUIElement {
const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, state)
answer.SetClass("w-full")
let rendering = answer;
let rendering = answer
if (configuration.question !== undefined && state?.featureSwitchUserbadge?.data) {
// We have a question and editing is enabled
const answerWithEditButton = new Combine([answer,
new Toggle(new Combine([Svg.pencil_ui()]).SetClass("block relative h-10 w-10 p-2 float-right").SetStyle("border: 1px solid black; border-radius: 0.7em")
const answerWithEditButton = new Combine([
answer,
new Toggle(
new Combine([Svg.pencil_ui()])
.SetClass("block relative h-10 w-10 p-2 float-right")
.SetStyle("border: 1px solid black; border-radius: 0.7em")
.onClick(() => {
editMode.setData(true);
editMode.setData(true)
}),
undefined,
state.osmConnection.isLoggedIn)
state.osmConnection.isLoggedIn
),
]).SetClass("flex justify-between w-full")
const question = new Lazy(() =>
new TagRenderingQuestion(tags, configuration, state,
{
const question = new Lazy(
() =>
new TagRenderingQuestion(tags, configuration, state, {
units: units,
cancelButton: Translations.t.general.cancel.Clone()
cancelButton: Translations.t.general.cancel
.Clone()
.SetClass("btn btn-secondary")
.onClick(() => {
editMode.setData(false)
}),
afterSave: () => {
editMode.setData(false)
}
}))
rendering = new Toggle(
question,
answerWithEditButton,
editMode
},
})
)
}
return rendering;
}
}
rendering = new Toggle(question, answerWithEditButton, editMode)
}
return rendering
}
}

View file

@ -1,97 +1,111 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import EditableTagRendering from "./EditableTagRendering";
import QuestionBox from "./QuestionBox";
import Combine from "../Base/Combine";
import TagRenderingAnswer from "./TagRenderingAnswer";
import ScrollableFullScreen from "../Base/ScrollableFullScreen";
import Constants from "../../Models/Constants";
import SharedTagRenderings from "../../Customizations/SharedTagRenderings";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import DeleteWizard from "./DeleteWizard";
import SplitRoadWizard from "./SplitRoadWizard";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {Utils} from "../../Utils";
import MoveWizard from "./MoveWizard";
import Toggle from "../Input/Toggle";
import Lazy from "../Base/Lazy";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import { UIEventSource } from "../../Logic/UIEventSource"
import EditableTagRendering from "./EditableTagRendering"
import QuestionBox from "./QuestionBox"
import Combine from "../Base/Combine"
import TagRenderingAnswer from "./TagRenderingAnswer"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Constants from "../../Models/Constants"
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import DeleteWizard from "./DeleteWizard"
import SplitRoadWizard from "./SplitRoadWizard"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { Utils } from "../../Utils"
import MoveWizard from "./MoveWizard"
import Toggle from "../Input/Toggle"
import Lazy from "../Base/Lazy"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
export default class FeatureInfoBox extends ScrollableFullScreen {
public constructor(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState,
options?: {
hashToShow?: string,
isShown?: UIEventSource<boolean>,
hashToShow?: string
isShown?: UIEventSource<boolean>
setHash?: true | boolean
}
) {
if (state === undefined) {
throw "State is undefined!"
}
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state),
super(
() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig, state),
() => FeatureInfoBox.GenerateContent(tags, layerConfig, state),
options?.hashToShow ?? tags.data.id ?? "item",
options?.isShown,
options);
options
)
if (layerConfig === undefined) {
throw "Undefined layerconfig";
throw "Undefined layerconfig"
}
}
public static GenerateTitleBar(tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: {}): BaseUIElement {
const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), state)
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl");
public static GenerateTitleBar(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: {}
): BaseUIElement {
const title = new TagRenderingAnswer(
tags,
layerConfig.title ?? new TagRenderingConfig("POI"),
state
).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl")
const titleIcons = new Combine(
layerConfig.titleIcons.map(icon => {
return new TagRenderingAnswer(tags, icon, state,
"block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon");
}
))
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
layerConfig.titleIcons.map((icon) => {
return new TagRenderingAnswer(
tags,
icon,
state,
"block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon"
)
})
).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
return new Combine([
new Combine([title, titleIcons]).SetClass("flex flex-col sm:flex-row flex-grow justify-between")
new Combine([title, titleIcons]).SetClass(
"flex flex-col sm:flex-row flex-grow justify-between"
),
])
}
public static GenerateContent(tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>();
public static GenerateContent(
tags: UIEventSource<any>,
layerConfig: LayerConfig,
state: FeaturePipelineState
): BaseUIElement {
let questionBoxes: Map<string, QuestionBox> = new Map<string, QuestionBox>()
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group))
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map((tr) => tr.group))
if (state?.featureSwitchUserbadge?.data ?? true) {
const questionSpecs = layerConfig.tagRenderings.filter(tr => tr.id === "questions")
const questionSpecs = layerConfig.tagRenderings.filter((tr) => tr.id === "questions")
for (const groupName of allGroupNames) {
const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
const questionSpec = questionSpecs.filter(tr => tr.group === groupName)[0]
const questions = layerConfig.tagRenderings.filter((tr) => tr.group === groupName)
const questionSpec = questionSpecs.filter((tr) => tr.group === groupName)[0]
const questionBox = new QuestionBox(state, {
tagsSource: tags,
tagRenderings: questions,
units: layerConfig.units,
showAllQuestionsAtOnce: questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? state.featureSwitchShowAllQuestions
});
showAllQuestionsAtOnce:
questionSpec?.freeform?.helperArgs["showAllQuestions"] ??
state.featureSwitchShowAllQuestions,
})
questionBoxes.set(groupName, questionBox)
}
}
const allRenderings = []
for (let i = 0; i < allGroupNames.length; i++) {
const groupName = allGroupNames[i];
const groupName = allGroupNames[i]
const trs = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
const trs = layerConfig.tagRenderings.filter((tr) => tr.group === groupName)
const renderingsForGroup: (EditableTagRendering | BaseUIElement)[] = []
const innerClasses = "block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2";
const innerClasses =
"block w-full break-word text-default m-1 p-1 border-b border-gray-200 mb-2 pb-2"
for (const tr of trs) {
if (tr.question === null || tr.id === "questions") {
// This is a question box!
@ -100,20 +114,27 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
if (tr.render !== undefined) {
questionBox.SetClass("text-sm")
const renderedQuestion = new TagRenderingAnswer(tags, tr, state,
tr.group + " questions", "", {
specialViz: new Map<string, BaseUIElement>([["questions", questionBox]])
})
const renderedQuestion = new TagRenderingAnswer(
tags,
tr,
state,
tr.group + " questions",
"",
{
specialViz: new Map<string, BaseUIElement>([
["questions", questionBox],
]),
}
)
const possiblyHidden = new Toggle(
renderedQuestion,
undefined,
questionBox.restingQuestions.map(ls => ls?.length > 0)
questionBox.restingQuestions.map((ls) => ls?.length > 0)
)
renderingsForGroup.push(possiblyHidden)
} else {
renderingsForGroup.push(questionBox)
}
} else {
let classes = innerClasses
let isHeader = renderingsForGroup.length === 0 && i > 0
@ -124,7 +145,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
}
const etr = new EditableTagRendering(tags, tr, layerConfig.units, state, {
innerElementClasses: innerClasses
innerElementClasses: innerClasses,
})
if (isHeader) {
etr.SetClass("sticky top-0")
@ -137,10 +158,13 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
}
allRenderings.push(
new Toggle(
new Lazy(() => FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state)),
new Lazy(() =>
FeatureInfoBox.createEditElements(questionBoxes, layerConfig, tags, state)
),
undefined,
state.featureSwitchUserbadge
))
)
)
return new Combine(allRenderings).SetClass("block")
}
@ -153,86 +177,100 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
* @param state
* @private
*/
private static createEditElements(questionBoxes: Map<string, QuestionBox>,
layerConfig: LayerConfig,
tags: UIEventSource<any>,
state: FeaturePipelineState)
: BaseUIElement {
private static createEditElements(
questionBoxes: Map<string, QuestionBox>,
layerConfig: LayerConfig,
tags: UIEventSource<any>,
state: FeaturePipelineState
): BaseUIElement {
let editElements: BaseUIElement[] = []
questionBoxes.forEach(questionBox => {
editElements.push(questionBox);
questionBoxes.forEach((questionBox) => {
editElements.push(questionBox)
})
if (layerConfig.allowMove) {
editElements.push(
new VariableUiElement(tags.map(tags => tags.id).map(id => {
const feature = state.allElements.ContainingFeatures.get(id)
if (feature === undefined) {
return "This feature is not register in the state.allElements and cannot be moved"
}
return new MoveWizard(
feature,
state,
layerConfig.allowMove
);
})
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => {
const feature = state.allElements.ContainingFeatures.get(id)
if (feature === undefined) {
return "This feature is not register in the state.allElements and cannot be moved"
}
return new MoveWizard(feature, state, layerConfig.allowMove)
})
).SetClass("text-base")
);
)
}
if (layerConfig.deletion) {
editElements.push(
new VariableUiElement(tags.map(tags => tags.id).map(id =>
new DeleteWizard(
id,
state,
layerConfig.deletion
))
).SetClass("text-base"))
new VariableUiElement(
tags
.map((tags) => tags.id)
.map((id) => new DeleteWizard(id, state, layerConfig.deletion))
).SetClass("text-base")
)
}
if (layerConfig.allowSplit) {
editElements.push(
new VariableUiElement(tags.map(tags => tags.id).map(id =>
new SplitRoadWizard(id, state))
).SetClass("text-base"))
new VariableUiElement(
tags.map((tags) => tags.id).map((id) => new SplitRoadWizard(id, state))
).SetClass("text-base")
)
}
editElements.push(
new VariableUiElement(
state.osmConnection.userDetails
.map(ud => ud.csCount)
.map(csCount => {
if (csCount <= Constants.userJourney.historyLinkVisible
&& state.featureSwitchIsDebugging.data == false
&& state.featureSwitchIsTesting.data === false) {
return undefined
}
.map((ud) => ud.csCount)
.map(
(csCount) => {
if (
csCount <= Constants.userJourney.historyLinkVisible &&
state.featureSwitchIsDebugging.data == false &&
state.featureSwitchIsTesting.data === false
) {
return undefined
}
return new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("last_edit"), state);
}, [state.featureSwitchIsDebugging, state.featureSwitchIsTesting])
return new TagRenderingAnswer(
tags,
SharedTagRenderings.SharedTagRendering.get("last_edit"),
state
)
},
[state.featureSwitchIsDebugging, state.featureSwitchIsTesting]
)
)
)
editElements.push(
new VariableUiElement(
state.featureSwitchIsDebugging.map(isDebugging => {
if (isDebugging) {
const config_all_tags: TagRenderingConfig = new TagRenderingConfig({render: "{all_tags()}"}, "");
const config_download: TagRenderingConfig = new TagRenderingConfig({render: "{export_as_geojson()}"}, "");
const config_id: TagRenderingConfig = new TagRenderingConfig({render: "{open_in_iD()}"}, "");
Toggle.If(state.featureSwitchIsDebugging,
() => {
const config_all_tags: TagRenderingConfig = new TagRenderingConfig(
{ render: "{all_tags()}" },
""
)
const config_download: TagRenderingConfig = new TagRenderingConfig(
{ render: "{export_as_geojson()}" },
""
)
const config_id: TagRenderingConfig = new TagRenderingConfig(
{ render: "{open_in_iD()}" },
""
)
return new Combine([new TagRenderingAnswer(tags, config_all_tags, state),
new TagRenderingAnswer(tags, config_download, state),
new TagRenderingAnswer(tags, config_id, state),
"This is layer "+layerConfig.id
])
}
})
)
return new Combine([
new TagRenderingAnswer(tags, config_all_tags, state),
new TagRenderingAnswer(tags, config_download, state),
new TagRenderingAnswer(tags, config_id, state),
"This is layer " + layerConfig.id,
])
}
)
)
return new Combine(editElements).SetClass("flex flex-col")

View file

@ -1,48 +1,50 @@
import BaseUIElement from "../BaseUIElement";
import {SubtleButton} from "../Base/SubtleButton";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import Toggle from "../Input/Toggle";
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
import Loading from "../Base/Loading";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Lazy from "../Base/Lazy";
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
import Img from "../Base/Img";
import FilteredLayer from "../../Models/FilteredLayer";
import SpecialVisualizations from "../SpecialVisualizations";
import {FixedUiElement} from "../Base/FixedUiElement";
import Svg from "../../Svg";
import {Utils} from "../../Utils";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import {DefaultGuiState} from "../DefaultGuiState";
import {PresetInfo} from "../BigComponents/SimpleAddUI";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import {And} from "../../Logic/Tags/And";
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
import {Tag} from "../../Logic/Tags/Tag";
import TagApplyButton from "./TagApplyButton";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import * as conflation_json from "../../assets/layers/conflation/conflation.json";
import {GeoOperations} from "../../Logic/GeoOperations";
import {LoginToggle} from "./LoginButton";
import {AutoAction} from "./AutoApplyButton";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import {ElementStorage} from "../../Logic/ElementStorage";
import Hash from "../../Logic/Web/Hash";
import {PreciseInput} from "../../Models/ThemeConfig/PresetConfig";
import BaseUIElement from "../BaseUIElement"
import { SubtleButton } from "../Base/SubtleButton"
import { UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import { VariableUiElement } from "../Base/VariableUIElement"
import Translations from "../i18n/Translations"
import Toggle from "../Input/Toggle"
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"
import Loading from "../Base/Loading"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Lazy from "../Base/Lazy"
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"
import Img from "../Base/Img"
import FilteredLayer from "../../Models/FilteredLayer"
import SpecialVisualizations from "../SpecialVisualizations"
import { FixedUiElement } from "../Base/FixedUiElement"
import Svg from "../../Svg"
import { Utils } from "../../Utils"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import CreateWayWithPointReuseAction, {
MergePointConfig,
} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
import FeatureSource from "../../Logic/FeatureSource/FeatureSource"
import { OsmObject, OsmWay } from "../../Logic/Osm/OsmObject"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import { DefaultGuiState } from "../DefaultGuiState"
import { PresetInfo } from "../BigComponents/SimpleAddUI"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { And } from "../../Logic/Tags/And"
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"
import { Tag } from "../../Logic/Tags/Tag"
import TagApplyButton from "./TagApplyButton"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import * as conflation_json from "../../assets/layers/conflation/conflation.json"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LoginToggle } from "./LoginButton"
import { AutoAction } from "./AutoApplyButton"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
import { ElementStorage } from "../../Logic/ElementStorage"
import Hash from "../../Logic/Web/Hash"
import { PreciseInput } from "../../Models/ThemeConfig/PresetConfig"
/**
* A helper class for the various import-flows.
@ -52,14 +54,18 @@ abstract class AbstractImportButton implements SpecialVisualizations {
protected static importedIds = new Set<string>()
public readonly funcName: string
public readonly docs: string
public readonly args: { name: string, defaultValue?: string, doc: string }[]
private readonly showRemovedTags: boolean;
private readonly cannotBeImportedMessage: BaseUIElement | undefined;
public readonly args: { name: string; defaultValue?: string; doc: string }[]
private readonly showRemovedTags: boolean
private readonly cannotBeImportedMessage: BaseUIElement | undefined
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string, required?: boolean }[],
options?: {showRemovedTags? : true | boolean, cannotBeImportedMessage?: BaseUIElement}) {
constructor(
funcName: string,
docsIntro: string,
extraArgs: { name: string; doc: string; defaultValue?: string; required?: boolean }[],
options?: { showRemovedTags?: true | boolean; cannotBeImportedMessage?: BaseUIElement }
) {
this.funcName = funcName
this.showRemovedTags = options?.showRemovedTags ?? true;
this.showRemovedTags = options?.showRemovedTags ?? true
this.cannotBeImportedMessage = options?.cannotBeImportedMessage
this.docs = `${docsIntro}
@ -77,34 +83,43 @@ ${Utils.special_visualizations_importRequirementDocs}
{
name: "targetLayer",
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
required: true
required: true,
},
{
name: "tags",
doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead",
required: true
required: true,
},
{
name: "text",
doc: "The text to show on the button",
defaultValue: "Import this data into OpenStreetMap"
defaultValue: "Import this data into OpenStreetMap",
},
{
name: "icon",
doc: "A nice icon to show in the button",
defaultValue: "./assets/svg/addSmall.svg"
defaultValue: "./assets/svg/addSmall.svg",
},
...extraArgs]
};
abstract constructElement(state: FeaturePipelineState,
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, newTags: UIEventSource<any>, targetLayer: string },
tagSource: UIEventSource<any>,
guiState: DefaultGuiState,
feature: any,
onCancelClicked: () => void): BaseUIElement;
...extraArgs,
]
}
abstract constructElement(
state: FeaturePipelineState,
args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
tags: string
newTags: UIEventSource<any>
targetLayer: string
},
tagSource: UIEventSource<any>,
guiState: DefaultGuiState,
feature: any,
onCancelClicked: () => void
): BaseUIElement
constr(state, tagSource: UIEventSource<any>, argsRaw, guiState) {
/**
@ -116,16 +131,25 @@ ${Utils.special_visualizations_importRequirementDocs}
* The actual import flow (showing the conflation map, special cases) are handled in 'constructElement'
*/
const t = Translations.t.general.add.import;
const t0 = Translations.t.general.add;
const t = Translations.t.general.add.import
const t0 = Translations.t.general.add
const args = this.parseArgs(argsRaw, tagSource)
{
// Some initial validation
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
if (
!state.layoutToUse.official &&
!(
state.featureSwitchIsTesting.data ||
state.osmConnection._oauth_config.url ===
OsmConnection.oauth_configs["osm-test"].url
)
) {
return new Combine([t.officialThemesOnly.SetClass("alert"), t.howToTest])
}
const targetLayer: FilteredLayer = state.filteredLayers.data.filter(fl => fl.layerDef.id === args.targetLayer)[0]
const targetLayer: FilteredLayer = state.filteredLayers.data.filter(
(fl) => fl.layerDef.id === args.targetLayer
)[0]
if (targetLayer === undefined) {
const e = `Target layer not defined: error in import button for theme: ${state.layoutToUse.id}: layer ${args.targetLayer} not found`
console.error(e)
@ -133,7 +157,6 @@ ${Utils.special_visualizations_importRequirementDocs}
}
}
let img: BaseUIElement
if (args.icon !== undefined && args.icon !== "") {
img = new Img(args.icon)
@ -142,41 +165,54 @@ ${Utils.special_visualizations_importRequirementDocs}
}
const inviteToImportButton = new SubtleButton(img, args.text)
const id = tagSource.data.id;
const id = tagSource.data.id
const feature = state.allElements.ContainingFeatures.get(id)
// Explanation of the tags that will be applied onto the imported/conflated object
let tagSpec = args.tags;
if (tagSpec.indexOf(" ") < 0 && tagSpec.indexOf(";") < 0 && tagSource.data[args.tags] !== undefined) {
let tagSpec = args.tags
if (
tagSpec.indexOf(" ") < 0 &&
tagSpec.indexOf(";") < 0 &&
tagSource.data[args.tags] !== undefined
) {
// This is probably a key
tagSpec = tagSource.data[args.tags]
console.debug("The import button is using tags from properties[" + args.tags + "] of this object, namely ", tagSpec)
console.debug(
"The import button is using tags from properties[" +
args.tags +
"] of this object, namely ",
tagSpec
)
}
const importClicked = new UIEventSource(false);
const importClicked = new UIEventSource(false)
inviteToImportButton.onClick(() => {
importClicked.setData(true);
importClicked.setData(true)
})
const pleaseLoginButton = new Toggle(t0.pleaseLogin
const pleaseLoginButton = new Toggle(
t0.pleaseLogin
.onClick(() => state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly"),
undefined,
state.featureSwitchUserbadge)
state.featureSwitchUserbadge
)
const isImported = tagSource.map(tags => {
const isImported = tagSource.map((tags) => {
AbstractImportButton.importedIds.add(tags.id)
return tags._imported === "yes";
return tags._imported === "yes"
})
/**** THe actual panel showing the import guiding map ****/
const importGuidingPanel = this.constructElement(state, args, tagSource, guiState, feature, () => importClicked.setData(false))
const importGuidingPanel = this.constructElement(
state,
args,
tagSource,
guiState,
feature,
() => importClicked.setData(false)
)
const importFlow = new Toggle(
new Toggle(
@ -186,25 +222,21 @@ ${Utils.special_visualizations_importRequirementDocs}
),
inviteToImportButton,
importClicked
);
)
return new Toggle(
new LoginToggle(
new Toggle(
new Toggle(
t.hasBeenImported,
importFlow,
isImported
),
new Toggle(t.hasBeenImported, importFlow, isImported),
t.zoomInMore.SetClass("alert block"),
state.locationControl.map(l => l.zoom >= 18)
state.locationControl.map((l) => l.zoom >= 18)
),
pleaseLoginButton,
state
),
this.cannotBeImportedMessage ?? t.wrongType,
new UIEventSource(this.canBeImported(feature)))
new UIEventSource(this.canBeImported(feature))
)
}
getLayerDependencies(argsRaw: string[]) {
@ -215,7 +247,7 @@ ${Utils.special_visualizations_importRequirementDocs}
// The target layer
dependsOnLayers.push(args.targetLayer)
const snapOntoLayers = args.snap_onto_layers?.trim() ?? "";
const snapOntoLayers = args.snap_onto_layers?.trim() ?? ""
if (snapOntoLayers !== "") {
dependsOnLayers.push(...snapOntoLayers.split(";"))
}
@ -227,15 +259,23 @@ ${Utils.special_visualizations_importRequirementDocs}
protected createConfirmPanelForWay(
state: FeaturePipelineState,
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<Tag[]>, targetLayer: string },
args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
newTags: UIEventSource<Tag[]>
targetLayer: string
},
feature: any,
originalFeatureTags: UIEventSource<any>,
action: (OsmChangeAction & { getPreview(): Promise<FeatureSource>, newElementId?: string }),
onCancel: () => void): BaseUIElement {
const self = this;
action: OsmChangeAction & { getPreview(): Promise<FeatureSource>; newElementId?: string },
onCancel: () => void
): BaseUIElement {
const self = this
const confirmationMap = Minimap.createMiniMap({
allowMoving: state.featureSwitchIsDebugging.data ?? false,
background: state.backgroundLayer
background: state.backgroundLayer,
})
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
@ -245,28 +285,33 @@ ${Utils.special_visualizations_importRequirementDocs}
zoomToFeatures: true,
features: StaticFeatureSource.fromGeojson([feature]),
state: state,
layers: state.filteredLayers
layers: state.filteredLayers,
})
action.getPreview().then(changePreview => {
action.getPreview().then((changePreview) => {
new ShowDataLayer({
leafletMap: confirmationMap.leafletMap,
zoomToFeatures: false,
features: changePreview,
state,
layerToShow: new LayerConfig(conflation_json, "all_known_layers", true)
layerToShow: new LayerConfig(conflation_json, "all_known_layers", true),
})
})
const tagsExplanation = new VariableUiElement(args.newTags.map(tagsToApply => {
const filteredTags = tagsToApply.filter(t => self.showRemovedTags || (t.value ?? "") !== "")
const tagsExplanation = new VariableUiElement(
args.newTags.map((tagsToApply) => {
const filteredTags = tagsToApply.filter(
(t) => self.showRemovedTags || (t.value ?? "") !== ""
)
const tagsStr = new And(filteredTags).asHumanString(false, true, {})
return Translations.t.general.add.import.importTags.Subs({tags: tagsStr});
}
)).SetClass("subtle")
return Translations.t.general.add.import.importTags.Subs({ tags: tagsStr })
})
).SetClass("subtle")
const confirmButton = new SubtleButton(new Img(args.icon), new Combine([args.text, tagsExplanation]).SetClass("flex flex-col"))
const confirmButton = new SubtleButton(
new Img(args.icon),
new Combine([args.text, tagsExplanation]).SetClass("flex flex-col")
)
confirmButton.onClick(async () => {
{
originalFeatureTags.data["_imported"] = "yes"
@ -277,19 +322,41 @@ ${Utils.special_visualizations_importRequirementDocs}
}
})
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(onCancel)
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(
onCancel
)
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
}
protected parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } {
protected parseArgs(
argsRaw: string[],
originalFeatureTags: UIEventSource<any>
): {
minzoom: string
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
tags: string
targetLayer: string
newTags: UIEventSource<Tag[]>
} {
const baseArgs = Utils.ParseVisArgs(this.args, argsRaw)
if (originalFeatureTags !== undefined) {
const tags = baseArgs.tags
if (tags.indexOf(" ") < 0 && tags.indexOf(";") < 0 && originalFeatureTags.data[tags] !== undefined) {
if (
tags.indexOf(" ") < 0 &&
tags.indexOf(";") < 0 &&
originalFeatureTags.data[tags] !== undefined
) {
// This might be a property to expand...
const items: string = originalFeatureTags.data[tags]
console.debug("The import button is using tags from properties[" + tags + "] of this object, namely ", items)
console.debug(
"The import button is using tags from properties[" +
tags +
"] of this object, namely ",
items
)
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(items, originalFeatureTags)
} else {
baseArgs["newTags"] = TagApplyButton.generateTagsToApply(tags, originalFeatureTags)
@ -300,55 +367,66 @@ ${Utils.special_visualizations_importRequirementDocs}
}
export class ConflateButton extends AbstractImportButton {
constructor() {
super("conflate_button", "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)",
[{
name: "way_to_conflate",
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag"
}],
super(
"conflate_button",
"This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)",
[
{
name: "way_to_conflate",
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
},
],
{
cannotBeImportedMessage: Translations.t.general.add.import.wrongTypeToConflate
cannotBeImportedMessage: Translations.t.general.add.import.wrongTypeToConflate,
}
);
)
}
getLayerDependencies(argsRaw: string[]): string[] {
const deps = super.getLayerDependencies(argsRaw);
const deps = super.getLayerDependencies(argsRaw)
// Force 'type_node' as dependency
deps.push("type_node")
return deps;
return deps
}
constructElement(state: FeaturePipelineState,
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
const nodesMustMatch = args.snap_onto_layers?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
constructElement(
state: FeaturePipelineState,
args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
tags: string
newTags: UIEventSource<Tag[]>
targetLayer: string
},
tagSource: UIEventSource<any>,
guiState: DefaultGuiState,
feature: any,
onCancelClicked: () => void
): BaseUIElement {
const nodesMustMatch = args.snap_onto_layers
?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
const mergeConfigs = []
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
const mergeConfig: MergePointConfig = {
mode: args["point_move_mode"] == "move_osm" ? "move_osm_point" : "reuse_osm_point",
ifMatches: new And(nodesMustMatch),
withinRangeOfM: Number(args.max_snap_distance)
withinRangeOfM: Number(args.max_snap_distance),
}
mergeConfigs.push(mergeConfig)
}
const key = args["way_to_conflate"]
const wayToConflate = tagSource.data[key]
feature = GeoOperations.removeOvernoding(feature);
const action = new ReplaceGeometryAction(
state,
feature,
wayToConflate,
{
theme: state.layoutToUse.id,
newTags: args.newTags.data
}
)
feature = GeoOperations.removeOvernoding(feature)
const action = new ReplaceGeometryAction(state, feature, wayToConflate, {
theme: state.layoutToUse.id,
newTags: args.newTags.data,
})
return this.createConfirmPanelForWay(
state,
@ -361,16 +439,19 @@ export class ConflateButton extends AbstractImportButton {
}
protected canBeImported(feature: any) {
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
return (
feature.geometry.type === "LineString" ||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
)
}
}
export class ImportWayButton extends AbstractImportButton implements AutoAction {
public readonly supportsAutoAction = true;
public readonly supportsAutoAction = true
constructor() {
super("import_way_button",
super(
"import_way_button",
"This button will copy the data from an external dataset into OpenStreetMap",
[
{
@ -380,34 +461,47 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
{
name: "max_snap_distance",
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
defaultValue: "0.05"
defaultValue: "0.05",
},
{
name: "move_osm_point_if",
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
}, {
name: "max_move_distance",
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
defaultValue: "0.05"
}, {
name: "snap_onto_layers",
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
}, {
name: "snap_to_layer_max_distance",
doc: "Distance to distort the geometry to snap to this layer",
defaultValue: "0.1"
}],
{ showRemovedTags: false}
},
{
name: "max_move_distance",
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
defaultValue: "0.05",
},
{
name: "snap_onto_layers",
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
},
{
name: "snap_to_layer_max_distance",
doc: "Distance to distort the geometry to snap to this layer",
defaultValue: "0.1",
},
],
{ showRemovedTags: false }
)
}
private static CreateAction(feature,
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string },
state: FeaturePipelineState,
mergeConfigs: any[]) {
private static CreateAction(
feature,
args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
tags: string
newTags: UIEventSource<any>
targetLayer: string
},
state: FeaturePipelineState,
mergeConfigs: any[]
) {
const coors = feature.geometry.coordinates
if ((feature.geometry.type === "Polygon") && coors.length > 1) {
if (feature.geometry.type === "Polygon" && coors.length > 1) {
const outer = coors[0]
const inner = [...coors]
inner.splice(0, 1)
@ -421,36 +515,27 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
)
} else if (feature.geometry.type === "Polygon") {
const outer = coors[0]
return new CreateWayWithPointReuseAction(
args.newTags.data,
outer,
state,
mergeConfigs
)
return new CreateWayWithPointReuseAction(args.newTags.data, outer, state, mergeConfigs)
} else if (feature.geometry.type === "LineString") {
return new CreateWayWithPointReuseAction(
args.newTags.data,
coors,
state,
mergeConfigs
)
return new CreateWayWithPointReuseAction(args.newTags.data, coors, state, mergeConfigs)
} else {
throw "Unsupported type"
}
}
async applyActionOn(state: { layoutToUse: LayoutConfig; changes: Changes, allElements: ElementStorage },
originalFeatureTags: UIEventSource<any>,
argument: string[]): Promise<void> {
const id = originalFeatureTags.data.id;
if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id)
) {
return;
async applyActionOn(
state: { layoutToUse: LayoutConfig; changes: Changes; allElements: ElementStorage },
originalFeatureTags: UIEventSource<any>,
argument: string[]
): Promise<void> {
const id = originalFeatureTags.data.id
if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id)) {
return
}
AbstractImportButton.importedIds.add(originalFeatureTags.data.id)
const args = this.parseArgs(argument, originalFeatureTags)
const feature = state.allElements.ContainingFeatures.get(id)
const mergeConfigs = this.GetMergeConfig(args);
const mergeConfigs = this.GetMergeConfig(args)
const action = ImportWayButton.CreateAction(
feature,
args,
@ -466,18 +551,12 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
}
getLayerDependencies(argsRaw: string[]): string[] {
const deps = super.getLayerDependencies(argsRaw);
const deps = super.getLayerDependencies(argsRaw)
deps.push("type_node")
return deps
}
constructElement(state, args,
originalFeatureTags,
guiState,
feature,
onCancel): BaseUIElement {
constructElement(state, args, originalFeatureTags, guiState, feature, onCancel): BaseUIElement {
const geometry = feature.geometry
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
@ -485,10 +564,9 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
}
// Upload the way to OSM
const mergeConfigs = this.GetMergeConfig(args);
let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs);
const mergeConfigs = this.GetMergeConfig(args)
let action = ImportWayButton.CreateAction(feature, args, state, mergeConfigs)
return this.createConfirmPanelForWay(
state,
args,
@ -497,83 +575,105 @@ export class ImportWayButton extends AbstractImportButton implements AutoAction
action,
onCancel
)
}
private GetMergeConfig(args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<any>; targetLayer: string })
: MergePointConfig[] {
const nodesMustMatch = args["snap_to_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
private GetMergeConfig(args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
tags: string
newTags: UIEventSource<any>
targetLayer: string
}): MergePointConfig[] {
const nodesMustMatch = args["snap_to_point_if"]
?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
const mergeConfigs = []
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
const mergeConfig: MergePointConfig = {
mode: "reuse_osm_point",
ifMatches: new And(nodesMustMatch),
withinRangeOfM: Number(args.max_snap_distance)
withinRangeOfM: Number(args.max_snap_distance),
}
mergeConfigs.push(mergeConfig)
}
const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
const moveOsmPointIfTags = args["move_osm_point_if"]
?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) {
const moveDistance = Math.min(20, Number(args["max_move_distance"]))
const mergeConfig: MergePointConfig = {
mode: "move_osm_point",
ifMatches: new And(moveOsmPointIfTags),
withinRangeOfM: moveDistance
withinRangeOfM: moveDistance,
}
mergeConfigs.push(mergeConfig)
}
return mergeConfigs;
return mergeConfigs
}
}
export class ImportPointButton extends AbstractImportButton {
constructor() {
super("import_button",
super(
"import_button",
"This button will copy the point from an external dataset into OpenStreetMap",
[
{
name: "snap_onto_layers",
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list"
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
},
{
name: "max_snap_distance",
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
defaultValue: "5"
defaultValue: "5",
},
{
name: "note_id",
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'"
doc: "If given, this key will be read. The corresponding note on OSM will be closed, stating 'imported'",
},
{
name:"location_picker",
name: "location_picker",
defaultValue: "photo",
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled"
doc: "Chooses the background for the precise location picker, options are 'map', 'photo' or 'osmbasedmap' or 'none' if the precise input picker should be disabled",
},
{
name: "maproulette_id",
doc: "If given, the maproulette challenge will be marked as fixed"
}],
{ showRemovedTags: false}
doc: "If given, the maproulette challenge will be marked as fixed",
},
],
{ showRemovedTags: false }
)
}
private static createConfirmPanelForPoint(
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string, note_id: string, maproulette_id: string },
args: {
max_snap_distance: string
snap_onto_layers: string
icon: string
text: string
newTags: UIEventSource<any>
targetLayer: string
note_id: string
maproulette_id: string
},
state: FeaturePipelineState,
guiState: DefaultGuiState,
originalFeatureTags: UIEventSource<any>,
feature: any,
onCancel: () => void,
close: () => void): BaseUIElement {
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
close: () => void
): BaseUIElement {
async function confirm(
tags: any[],
location: { lat: number; lon: number },
snapOntoWayId: string
) {
originalFeatureTags.data["_imported"] = "yes"
originalFeatureTags.ping() // will set isImported as per its definition
let snapOnto: OsmObject = undefined
@ -592,13 +692,13 @@ export class ImportPointButton extends AbstractImportButton {
theme: state.layoutToUse.id,
changeType: "import",
snapOnto: <OsmWay>snapOnto,
specialMotivation: specialMotivation
specialMotivation: specialMotivation,
})
await state.changes.applyAction(newElementAction)
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
newElementAction.newElementId
))
state.selectedElement.setData(
state.allElements.ContainingFeatures.get(newElementAction.newElementId)
)
Hash.hash.setData(newElementAction.newElementId)
if (note_id !== undefined) {
@ -607,47 +707,63 @@ export class ImportPointButton extends AbstractImportButton {
originalFeatureTags.ping()
}
let maproulette_id = originalFeatureTags.data[args.maproulette_id];
console.log("Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")")
let maproulette_id = originalFeatureTags.data[args.maproulette_id]
console.log(
"Checking if we need to mark a maproulette task as fixed (" + maproulette_id + ")"
)
if (maproulette_id !== undefined) {
if (state.featureSwitchIsTesting.data){
console.log("Not marking maproulette task " + maproulette_id + " as fixed, because we are in testing mode")
if (state.featureSwitchIsTesting.data) {
console.log(
"Not marking maproulette task " +
maproulette_id +
" as fixed, because we are in testing mode"
)
} else {
console.log("Marking maproulette task as fixed")
state.maprouletteConnection.closeTask(Number(maproulette_id));
originalFeatureTags.data["mr_taskStatus"] = "Fixed";
originalFeatureTags.ping();
state.maprouletteConnection.closeTask(Number(maproulette_id))
originalFeatureTags.data["mr_taskStatus"] = "Fixed"
originalFeatureTags.ping()
}
}
}
let preciseInputOption = args["location_picker"]
let preciseInputSpec: PreciseInput = undefined
let preciseInputSpec: PreciseInput = undefined
console.log("Precise input location is ", preciseInputOption)
if(preciseInputOption !== "none") {
if (preciseInputOption !== "none") {
preciseInputSpec = {
snapToLayers: args.snap_onto_layers?.split(";"),
maxSnapDistance: Number(args.max_snap_distance),
preferredBackground: args["location_picker"] ?? ["photo", "map"]
maxSnapDistance: Number(args.max_snap_distance),
preferredBackground: args["location_picker"] ?? ["photo", "map"],
}
}
const presetInfo = <PresetInfo>{
tags: args.newTags.data,
icon: () => new Img(args.icon),
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
layerToAddTo: state.filteredLayers.data.filter(
(l) => l.layerDef.id === args.targetLayer
)[0],
name: args.text,
title: Translations.T(args.text),
preciseInput: preciseInputSpec, // must be explicitely assigned, if 'undefined' won't work otherwise
boundsFactor: 3
boundsFactor: 3,
}
const [lon, lat] = feature.geometry.coordinates
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
lon,
lat
}, confirm, onCancel, close)
return new ConfirmLocationOfPoint(
state,
guiState.filterViewIsOpened,
presetInfo,
Translations.W(args.text),
{
lon,
lat,
},
confirm,
onCancel,
close
)
}
canBeImported(feature: any) {
@ -655,7 +771,7 @@ export class ImportPointButton extends AbstractImportButton {
}
getLayerDependencies(argsRaw: string[]): string[] {
const deps = super.getLayerDependencies(argsRaw);
const deps = super.getLayerDependencies(argsRaw)
const layerSnap = argsRaw["snap_onto_layers"] ?? ""
if (layerSnap === "") {
return deps
@ -665,35 +781,34 @@ export class ImportPointButton extends AbstractImportButton {
return deps
}
constructElement(state, args,
originalFeatureTags,
guiState,
feature,
onCancel: () => void): BaseUIElement {
constructElement(
state,
args,
originalFeatureTags,
guiState,
feature,
onCancel: () => void
): BaseUIElement {
const geometry = feature.geometry
if (geometry.type === "Point") {
return new Lazy(() => ImportPointButton.createConfirmPanelForPoint(
args,
state,
guiState,
originalFeatureTags,
feature,
onCancel,
() => {
// Close the current popup
state.selectedElement.setData(undefined)
}
))
return new Lazy(() =>
ImportPointButton.createConfirmPanelForPoint(
args,
state,
guiState,
originalFeatureTags,
feature,
onCancel,
() => {
// Close the current popup
state.selectedElement.setData(undefined)
}
)
)
}
console.error("Invalid type to import", geometry.type)
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
}
}
}

View file

@ -1,46 +1,52 @@
import {SubtleButton} from "../Base/SubtleButton";
import BaseUIElement from "../BaseUIElement";
import Svg from "../../Svg";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Toggle from "../Input/Toggle";
import {VariableUiElement} from "../Base/VariableUIElement";
import Loading from "../Base/Loading";
import Translations from "../i18n/Translations";
import { SubtleButton } from "../Base/SubtleButton"
import BaseUIElement from "../BaseUIElement"
import Svg from "../../Svg"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { VariableUiElement } from "../Base/VariableUIElement"
import Loading from "../Base/Loading"
import Translations from "../i18n/Translations"
class LoginButton extends SubtleButton {
constructor(text: BaseUIElement | string, state: {
osmConnection: OsmConnection
}, icon?: BaseUIElement | string) {
super(icon ?? Svg.osm_logo_ui(), text);
constructor(
text: BaseUIElement | string,
state: {
osmConnection: OsmConnection
},
icon?: BaseUIElement | string
) {
super(icon ?? Svg.osm_logo_ui(), text)
this.onClick(() => {
state.osmConnection.AttemptLogin()
})
}
}
export class LoginToggle extends VariableUiElement {
constructor(el, text: BaseUIElement | string, state: {
osmConnection: OsmConnection
}) {
constructor(
el,
text: BaseUIElement | string,
state: {
osmConnection: OsmConnection
}
) {
const loading = new Loading("Trying to log in...")
const login = new LoginButton(text, state)
super(
state.osmConnection.loadingStatus.map(osmConnectionState => {
if(osmConnectionState === "loading"){
state.osmConnection.loadingStatus.map((osmConnectionState) => {
if (osmConnectionState === "loading") {
return loading
}
if(osmConnectionState === "not-attempted"){
if (osmConnectionState === "not-attempted") {
return login
}
if(osmConnectionState === "logged-in"){
return el
if (osmConnectionState === "logged-in") {
return el
}
// Error!
return new LoginButton(Translations.t.general.loginFailed, state, Svg.invalid_svg())
})
)
)
}
}
}

View file

@ -1,33 +1,33 @@
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Toggle from "../Input/Toggle";
import {UIEventSource} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import {Translation} from "../i18n/Translation";
import BaseUIElement from "../BaseUIElement";
import LocationInput from "../Input/LocationInput";
import Loc from "../../Models/Loc";
import {GeoOperations} from "../../Logic/GeoOperations";
import {OsmObject} from "../../Logic/Osm/OsmObject";
import {Changes} from "../../Logic/Osm/Changes";
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import MoveConfig from "../../Models/ThemeConfig/MoveConfig";
import {ElementStorage} from "../../Logic/ElementStorage";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import BaseLayer from "../../Models/BaseLayer";
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import { Translation } from "../i18n/Translation"
import BaseUIElement from "../BaseUIElement"
import LocationInput from "../Input/LocationInput"
import Loc from "../../Models/Loc"
import { GeoOperations } from "../../Logic/GeoOperations"
import { OsmObject } from "../../Logic/Osm/OsmObject"
import { Changes } from "../../Logic/Osm/Changes"
import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import MoveConfig from "../../Models/ThemeConfig/MoveConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"
import BaseLayer from "../../Models/BaseLayer"
interface MoveReason {
text: Translation | string,
invitingText: Translation | string,
icon: BaseUIElement,
changesetCommentValue: string,
lockBounds: true | boolean,
background: undefined | "map" | "photo" | string | string[],
startZoom: number,
text: Translation | string
invitingText: Translation | string
icon: BaseUIElement
changesetCommentValue: string
lockBounds: true | boolean
background: undefined | "map" | "photo" | string | string[]
startZoom: number
minZoom: number
}
@ -38,13 +38,14 @@ export default class MoveWizard extends Toggle {
constructor(
featureToMove: any,
state: {
osmConnection: OsmConnection,
featureSwitchUserbadge: UIEventSource<boolean>,
changes: Changes,
layoutToUse: LayoutConfig,
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
}, options: MoveConfig) {
},
options: MoveConfig
) {
const t = Translations.t.move
const loginButton = new Toggle(
t.loginToMove.SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()),
@ -62,7 +63,7 @@ export default class MoveWizard extends Toggle {
lockBounds: false,
background: undefined,
startZoom: 12,
minZoom: 6
minZoom: 6,
})
}
if (options.enableImproveAccuracy) {
@ -74,13 +75,15 @@ export default class MoveWizard extends Toggle {
lockBounds: true,
background: "photo",
startZoom: 17,
minZoom: 16
minZoom: 16,
})
}
const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">("start")
const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">(
"start"
)
const moveReason = new UIEventSource<MoveReason>(undefined)
let moveButton: BaseUIElement;
let moveButton: BaseUIElement
if (reasons.length === 1) {
const reason = reasons[0]
moveReason.setData(reason)
@ -99,32 +102,32 @@ export default class MoveWizard extends Toggle {
})
}
const moveAgainButton = new SubtleButton(
Svg.move_ui(),
t.inviteToMoveAgain
).onClick(() => {
const moveAgainButton = new SubtleButton(Svg.move_ui(), t.inviteToMoveAgain).onClick(() => {
currentStep.setData("reason")
})
const selectReason = new Combine(
reasons.map((r) =>
new SubtleButton(r.icon, r.text).onClick(() => {
moveReason.setData(r)
currentStep.setData("pick_location")
})
)
)
const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => {
moveReason.setData(r)
currentStep.setData("pick_location")
})))
const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() => currentStep.setData("start"))
const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() =>
currentStep.setData("start")
)
const [lon, lat] = GeoOperations.centerpointCoordinates(featureToMove)
const locationInput = moveReason.map(reason => {
const locationInput = moveReason.map((reason) => {
if (reason === undefined) {
return undefined
}
const loc = new UIEventSource<Loc>({
lon: lon,
lat: lat,
zoom: reason?.startZoom ?? 16
zoom: reason?.startZoom ?? 16,
})
let background: string[]
@ -134,12 +137,15 @@ export default class MoveWizard extends Toggle {
background = reason.background
}
const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource(background)).data
const preferredBackground = AvailableBaseLayers.SelectBestLayerAccordingTo(
loc,
new UIEventSource(background)
).data
const locationInput = new LocationInput({
minZoom: reason.minZoom,
centerLocation: loc,
mapBackground: new UIEventSource<BaseLayer>(preferredBackground) // We detach the layer
mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer
state: <any> state
})
if (reason.lockBounds) {
@ -151,10 +157,12 @@ export default class MoveWizard extends Toggle {
const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove)
confirmMove.onClick(() => {
const loc = locationInput.GetValue().data
state.changes.applyAction(new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], {
reason: reason.changesetCommentValue,
theme: state.layoutToUse.id
}))
state.changes.applyAction(
new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], {
reason: reason.changesetCommentValue,
theme: state.layoutToUse.id,
})
)
featureToMove.properties._lat = loc.lat
featureToMove.properties._lon = loc.lon
state.allElements.getEventSourceById(id).ping()
@ -163,28 +171,42 @@ export default class MoveWizard extends Toggle {
const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6")
return new Combine([
locationInput,
new Toggle(confirmMove, zoomInFurhter, locationInput.GetValue().map(l => l.zoom >= 19))
new Toggle(
confirmMove,
zoomInFurhter,
locationInput.GetValue().map((l) => l.zoom >= 19)
),
]).SetClass("flex flex-col")
});
})
const dialogClasses = "p-2 md:p-4 m-2 border border-gray-400 rounded-xl flex flex-col"
const moveFlow = new Toggle(
new VariableUiElement(currentStep.map(currentStep => {
switch (currentStep) {
case "start":
return moveButton;
case "reason":
return new Combine([t.whyMove.SetClass("text-lg font-bold"), selectReason, cancelButton]).SetClass(dialogClasses);
case "pick_location":
return new Combine([t.moveTitle.SetClass("text-lg font-bold"), new VariableUiElement(locationInput), cancelButton]).SetClass(dialogClasses)
case "moved":
return new Combine([t.pointIsMoved.SetClass("thanks"), moveAgainButton]).SetClass("flex flex-col");
}
})),
new VariableUiElement(
currentStep.map((currentStep) => {
switch (currentStep) {
case "start":
return moveButton
case "reason":
return new Combine([
t.whyMove.SetClass("text-lg font-bold"),
selectReason,
cancelButton,
]).SetClass(dialogClasses)
case "pick_location":
return new Combine([
t.moveTitle.SetClass("text-lg font-bold"),
new VariableUiElement(locationInput),
cancelButton,
]).SetClass(dialogClasses)
case "moved":
return new Combine([
t.pointIsMoved.SetClass("thanks"),
moveAgainButton,
]).SetClass("flex flex-col")
}
})
),
loginButton,
state.osmConnection.isLoggedIn
)
@ -200,14 +222,13 @@ export default class MoveWizard extends Toggle {
} else if (id.startsWith("relation")) {
moveDisallowedReason.setData(t.isRelation)
} else if (id.indexOf("-") < 0) {
OsmObject.DownloadReferencingWays(id).then(referencing => {
OsmObject.DownloadReferencingWays(id).then((referencing) => {
if (referencing.length > 0) {
console.log("Got a referencing way, move not allowed")
moveDisallowedReason.setData(t.partOfAWay)
}
})
OsmObject.DownloadReferencingRelations(id).then(partOf => {
OsmObject.DownloadReferencingRelations(id).then((partOf) => {
if (partOf.length > 0) {
moveDisallowedReason.setData(t.partOfRelation)
}
@ -217,11 +238,12 @@ export default class MoveWizard extends Toggle {
moveFlow,
new Combine([
Svg.move_not_allowed_svg().SetStyle("height: 2rem").SetClass("m-2"),
new Combine([t.cannotBeMoved,
new VariableUiElement(moveDisallowedReason).SetClass("subtle")
]).SetClass("flex flex-col")
new Combine([
t.cannotBeMoved,
new VariableUiElement(moveDisallowedReason).SetClass("subtle"),
]).SetClass("flex flex-col"),
]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"),
moveDisallowedReason.map(r => r === undefined)
moveDisallowedReason.map((r) => r === undefined)
)
}
}
}

View file

@ -1,43 +1,41 @@
import {Store} from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import {Changes} from "../../Logic/Osm/Changes";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {Tag} from "../../Logic/Tags/Tag";
import {ElementStorage} from "../../Logic/ElementStorage";
import {And} from "../../Logic/Tags/And";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import Toggle from "../Input/Toggle";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import { Store } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import { Changes } from "../../Logic/Osm/Changes"
import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { Tag } from "../../Logic/Tags/Tag"
import { ElementStorage } from "../../Logic/ElementStorage"
import { And } from "../../Logic/Tags/And"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import Toggle from "../Input/Toggle"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
export interface MultiApplyParams {
featureIds: Store<string[]>,
keysToApply: string[],
text: string,
autoapply: boolean,
overwrite: boolean,
tagsSource: Store<any>,
featureIds: Store<string[]>
keysToApply: string[]
text: string
autoapply: boolean
overwrite: boolean
tagsSource: Store<any>
state: {
changes: Changes,
allElements: ElementStorage,
layoutToUse: LayoutConfig,
changes: Changes
allElements: ElementStorage
layoutToUse: LayoutConfig
osmConnection: OsmConnection
}
}
class MultiApplyExecutor {
private static executorCache = new Map<string, MultiApplyExecutor>()
private readonly originalValues = new Map<string, string>()
private readonly params: MultiApplyParams;
private readonly params: MultiApplyParams
private constructor(params: MultiApplyParams) {
this.params = params;
this.params = params
const p = params
for (const key of p.keysToApply) {
@ -45,14 +43,13 @@ class MultiApplyExecutor {
}
if (p.autoapply) {
const self = this;
const relevantValues = p.tagsSource.map(tags => {
const currentValues = p.keysToApply.map(key => tags[key])
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
return JSON.stringify(currentValues);
return JSON.stringify(currentValues)
})
relevantValues.addCallbackD(_ => {
relevantValues.addCallbackD((_) => {
self.applyTaggingOnOtherFeatures()
})
}
@ -74,7 +71,7 @@ class MultiApplyExecutor {
const allElements = this.params.state.allElements
const keysToChange = this.params.keysToApply
const overwrite = this.params.overwrite
const selfTags = this.params.tagsSource.data;
const selfTags = this.params.tagsSource.data
const theme = this.params.state.layoutToUse.id
for (const id of featuresToChange) {
const tagsToApply: Tag[] = []
@ -86,42 +83,42 @@ class MultiApplyExecutor {
}
const otherValue = otherFeatureTags[key]
if (newValue === otherValue) {
continue;// No changes to be made
continue // No changes to be made
}
if (overwrite) {
tagsToApply.push(new Tag(key, newValue))
continue;
continue
}
if (otherValue === undefined || otherValue === "" || otherValue === this.originalValues.get(key)) {
if (
otherValue === undefined ||
otherValue === "" ||
otherValue === this.originalValues.get(key)
) {
tagsToApply.push(new Tag(key, newValue))
}
}
if (tagsToApply.length == 0) {
continue;
continue
}
changes.applyAction(
new ChangeTagAction(id, new And(tagsToApply), otherFeatureTags, {
theme,
changeType: "answer"
}))
changeType: "answer",
})
)
}
}
}
export default class MultiApply extends Toggle {
constructor(params: MultiApplyParams) {
const p = params
const t = Translations.t.multi_apply
const featureId = p.tagsSource.data.id
if (featureId === undefined) {
@ -133,24 +130,30 @@ export default class MultiApply extends Toggle {
const elems: (string | BaseUIElement)[] = []
if (p.autoapply) {
elems.push(new FixedUiElement(p.text).SetClass("block"))
elems.push(new VariableUiElement(p.featureIds.map(featureIds =>
t.autoApply.Subs({
attr_names: p.keysToApply.join(", "),
count: "" + featureIds.length
}))).SetClass("block subtle text-sm"))
elems.push(
new VariableUiElement(
p.featureIds.map((featureIds) =>
t.autoApply.Subs({
attr_names: p.keysToApply.join(", "),
count: "" + featureIds.length,
})
)
).SetClass("block subtle text-sm")
)
} else {
elems.push(
new SubtleButton(undefined, p.text).onClick(() => applicator.applyTaggingOnOtherFeatures())
new SubtleButton(undefined, p.text).onClick(() =>
applicator.applyTaggingOnOtherFeatures()
)
)
}
const isShown: Store<boolean> = p.state.osmConnection.isLoggedIn.map(loggedIn => {
return loggedIn && p.featureIds.data.length > 0
}, [p.featureIds])
super(new Combine(elems), undefined, isShown);
const isShown: Store<boolean> = p.state.osmConnection.isLoggedIn.map(
(loggedIn) => {
return loggedIn && p.featureIds.data.length > 0
},
[p.featureIds]
)
super(new Combine(elems), undefined, isShown)
}
}
}

View file

@ -1,94 +1,90 @@
import Combine from "../Base/Combine";
import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource";
import {SlideShow} from "../Image/SlideShow";
import {ClickableToggle} from "../Input/Toggle";
import Loading from "../Base/Loading";
import {AttributedImage} from "../Image/AttributedImage";
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders";
import Svg from "../../Svg";
import BaseUIElement from "../BaseUIElement";
import {InputElement} from "../Input/InputElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {Mapillary} from "../../Logic/ImageProviders/Mapillary";
import {SubtleButton} from "../Base/SubtleButton";
import {GeoOperations} from "../../Logic/GeoOperations";
import {ElementStorage} from "../../Logic/ElementStorage";
import Lazy from "../Base/Lazy";
import Combine from "../Base/Combine"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import { SlideShow } from "../Image/SlideShow"
import { ClickableToggle } from "../Input/Toggle"
import Loading from "../Base/Loading"
import { AttributedImage } from "../Image/AttributedImage"
import AllImageProviders from "../../Logic/ImageProviders/AllImageProviders"
import Svg from "../../Svg"
import BaseUIElement from "../BaseUIElement"
import { InputElement } from "../Input/InputElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import Translations from "../i18n/Translations"
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
import { SubtleButton } from "../Base/SubtleButton"
import { GeoOperations } from "../../Logic/GeoOperations"
import { ElementStorage } from "../../Logic/ElementStorage"
import Lazy from "../Base/Lazy"
export interface P4CPicture {
pictureUrl: string,
date?: number,
coordinates: { lat: number, lng: number },
provider: "Mapillary" | string,
author?,
license?,
detailsUrl?: string,
direction?,
pictureUrl: string
date?: number
coordinates: { lat: number; lng: number }
provider: "Mapillary" | string
author?
license?
detailsUrl?: string
direction?
osmTags?: object /*To copy straight into OSM!*/
,
thumbUrl: string,
thumbUrl: string
details: {
isSpherical: boolean,
isSpherical: boolean
}
}
export interface NearbyImageOptions {
lon: number,
lat: number,
lon: number
lat: number
// Radius of the upstream search
searchRadius?: 500 | number,
maxDaysOld?: 1095 | number,
blacklist: Store<{ url: string }[]>,
shownImagesCount?: UIEventSource<number>,
towardscenter?: UIEventSource<boolean>;
searchRadius?: 500 | number
maxDaysOld?: 1095 | number
blacklist: Store<{ url: string }[]>
shownImagesCount?: UIEventSource<number>
towardscenter?: UIEventSource<boolean>
allowSpherical?: UIEventSource<boolean>
// Radius of what is shown. Useless to select a value > searchRadius; defaults to searchRadius
shownRadius?: UIEventSource<number>
}
class ImagesInLoadedDataFetcher {
private allElements: ElementStorage;
private allElements: ElementStorage
constructor(state: { allElements: ElementStorage }) {
this.allElements = state.allElements
}
public fetchAround(loc: { lon: number, lat: number, searchRadius?: number }): P4CPicture[] {
public fetchAround(loc: { lon: number; lat: number; searchRadius?: number }): P4CPicture[] {
const foundImages: P4CPicture[] = []
this.allElements.ContainingFeatures.forEach((feature) => {
const props = feature.properties;
const props = feature.properties
const images = []
if (props.image) {
images.push(props.image)
}
for (let i = 0; i < 10; i++) {
if (props["image:" + i]) {
images.push(props["image:" + i])
}
}
if (images.length == 0) {
return;
return
}
const centerpoint = GeoOperations.centerpointCoordinates(feature)
const d = GeoOperations.distanceBetween(centerpoint, [loc.lon, loc.lat])
if (loc.searchRadius !== undefined && d > loc.searchRadius) {
return;
return
}
for (const image of images) {
foundImages.push({
pictureUrl: image,
thumbUrl: image,
coordinates: {lng: centerpoint[0], lat: centerpoint[1]},
coordinates: { lng: centerpoint[0], lat: centerpoint[1] },
provider: "OpenStreetMap",
details: {
isSpherical: false
}
isSpherical: false,
},
})
}
})
const cleaned: P4CPicture[] = []
const seen = new Set<string>()
@ -104,171 +100,207 @@ class ImagesInLoadedDataFetcher {
}
export default class NearbyImages extends Lazy {
constructor(options: NearbyImageOptions, state?: { allElements: ElementStorage }) {
super(() => {
const t = Translations.t.image.nearbyPictures
const shownImages = options.shownImagesCount ?? new UIEventSource(25);
const shownImages = options.shownImagesCount ?? new UIEventSource(25)
const loadedPictures = NearbyImages.buildPictureFetcher(options, state)
const loadMoreButton = new Combine([new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => {
shownImages.setData(shownImages.data + 25)
})]).SetClass("flex flex-col justify-center")
const loadMoreButton = new Combine([
new SubtleButton(Svg.add_svg(), t.loadMore).onClick(() => {
shownImages.setData(shownImages.data + 25)
}),
]).SetClass("flex flex-col justify-center")
const imageElements = loadedPictures.map(imgs => {
if(imgs === undefined){
return []
}
const elements = (imgs.images ?? []).slice(0, shownImages.data).map(i => this.prepareElement(i));
if (imgs.images !== undefined && elements.length < imgs.images.length) {
// We effectively sliced some items, so we can increase the count
elements.push(loadMoreButton)
}
return elements;
}, [shownImages]);
const imageElements = loadedPictures.map(
(imgs) => {
if (imgs === undefined) {
return []
}
const elements = (imgs.images ?? [])
.slice(0, shownImages.data)
.map((i) => this.prepareElement(i))
if (imgs.images !== undefined && elements.length < imgs.images.length) {
// We effectively sliced some items, so we can increase the count
elements.push(loadMoreButton)
}
return elements
},
[shownImages]
)
return new VariableUiElement(loadedPictures.map(loaded => {
if (loaded?.images === undefined) {
return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass("animate-pulse")
}
const images = loaded.images
const beforeFilter = loaded?.beforeFilter
if (beforeFilter === 0) {
return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block"))
}else if(images.length === 0){
const removeFiltersButton = new SubtleButton(Svg.filter_disable_svg(), t.removeFilters).onClick(() => {
options.shownRadius.setData(options.searchRadius)
options.allowSpherical.setData(true)
options.towardscenter.setData(false)
});
return NearbyImages.NoImagesView(
t.allFiltered.SetClass("font-bold"),
removeFiltersButton
)
}
return new SlideShow(imageElements)
},));
return new VariableUiElement(
loadedPictures.map((loaded) => {
if (loaded?.images === undefined) {
return NearbyImages.NoImagesView(new Loading(t.loading)).SetClass(
"animate-pulse"
)
}
const images = loaded.images
const beforeFilter = loaded?.beforeFilter
if (beforeFilter === 0) {
return NearbyImages.NoImagesView(t.nothingFound.SetClass("alert block"))
} else if (images.length === 0) {
const removeFiltersButton = new SubtleButton(
Svg.filter_disable_svg(),
t.removeFilters
).onClick(() => {
options.shownRadius.setData(options.searchRadius)
options.allowSpherical.setData(true)
options.towardscenter.setData(false)
})
return NearbyImages.NoImagesView(
t.allFiltered.SetClass("font-bold"),
removeFiltersButton
)
}
return new SlideShow(imageElements)
})
)
})
}
private static NoImagesView(...elems: BaseUIElement[]){
return new Combine(elems).SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg")
.SetStyle("height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );")
private static NoImagesView(...elems: BaseUIElement[]) {
return new Combine(elems)
.SetClass("flex flex-col justify-center items-center bg-gray-200 mb-2 rounded-lg")
.SetStyle(
"height: calc( var(--image-carousel-height) - 0.5rem ) ; max-height: calc( var(--image-carousel-height) - 0.5rem );"
)
}
private static buildPictureFetcher(options: NearbyImageOptions, state?: { allElements: ElementStorage }) {
private static buildPictureFetcher(
options: NearbyImageOptions,
state?: { allElements: ElementStorage }
) {
const P4C = require("../../vendor/P4C.min")
const picManager = new P4C.PicturesManager({});
const searchRadius = options.searchRadius ?? 500;
const nearbyImages = state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : []
const picManager = new P4C.PicturesManager({})
const searchRadius = options.searchRadius ?? 500
const nearbyImages =
state !== undefined ? new ImagesInLoadedDataFetcher(state).fetchAround(options) : []
return Stores.FromPromise<P4CPicture[]>(
picManager.startPicsRetrievalAround(new P4C.LatLng(options.lat, options.lon), options.searchRadius ?? 500, {
mindate: new Date().getTime() - (options.maxDaysOld ?? (3 * 365)) * 24 * 60 * 60 * 1000,
towardscenter: false
})
).map(images => {
if (images === undefined) {
return undefined
}
images = (images ?? []).concat(nearbyImages)
const blacklisted = options.blacklist?.data
images = images?.filter(i => !blacklisted?.some(notAllowed => Mapillary.sameUrl(i.pictureUrl, notAllowed.url)));
picManager.startPicsRetrievalAround(
new P4C.LatLng(options.lat, options.lon),
options.searchRadius ?? 500,
{
mindate:
new Date().getTime() -
(options.maxDaysOld ?? 3 * 365) * 24 * 60 * 60 * 1000,
towardscenter: false,
}
)
).map(
(images) => {
if (images === undefined) {
return undefined
}
images = (images ?? []).concat(nearbyImages)
const blacklisted = options.blacklist?.data
images = images?.filter(
(i) =>
!blacklisted?.some((notAllowed) =>
Mapillary.sameUrl(i.pictureUrl, notAllowed.url)
)
)
const beforeFilterCount = images.length
const beforeFilterCount = images.length
if (!(options?.allowSpherical?.data)) {
images = images?.filter(i => i.details.isSpherical !== true)
}
const shownRadius = options?.shownRadius?.data ?? searchRadius;
if (shownRadius !== searchRadius) {
images = images.filter(i => {
const d = GeoOperations.distanceBetween([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat])
return d <= shownRadius
if (!options?.allowSpherical?.data) {
images = images?.filter((i) => i.details.isSpherical !== true)
}
const shownRadius = options?.shownRadius?.data ?? searchRadius
if (shownRadius !== searchRadius) {
images = images.filter((i) => {
const d = GeoOperations.distanceBetween(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
return d <= shownRadius
})
}
if (options.towardscenter?.data) {
images = images.filter((i) => {
if (i.direction === undefined || isNaN(i.direction)) {
return false
}
const bearing = GeoOperations.bearing(
[i.coordinates.lng, i.coordinates.lat],
[options.lon, options.lat]
)
const diff = Math.abs((i.direction - bearing) % 360)
return diff < 40
})
}
images?.sort((a, b) => {
const distanceA = GeoOperations.distanceBetween(
[a.coordinates.lng, a.coordinates.lat],
[options.lon, options.lat]
)
const distanceB = GeoOperations.distanceBetween(
[b.coordinates.lng, b.coordinates.lat],
[options.lon, options.lat]
)
return distanceA - distanceB
})
}
if (options.towardscenter?.data) {
images = images.filter(i => {
if (i.direction === undefined || isNaN(i.direction)) {
return false
}
const bearing = GeoOperations.bearing([i.coordinates.lng, i.coordinates.lat], [options.lon, options.lat])
const diff = Math.abs((i.direction - bearing) % 360);
return diff < 40
})
}
images?.sort((a, b) => {
const distanceA = GeoOperations.distanceBetween([a.coordinates.lng, a.coordinates.lat], [options.lon, options.lat])
const distanceB = GeoOperations.distanceBetween([b.coordinates.lng, b.coordinates.lat], [options.lon, options.lat])
return distanceA - distanceB
})
return {images, beforeFilter: beforeFilterCount};
}, [options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius])
return { images, beforeFilter: beforeFilterCount }
},
[options.blacklist, options.allowSpherical, options.towardscenter, options.shownRadius]
)
}
protected prepareElement(info: P4CPicture): BaseUIElement {
const provider = AllImageProviders.byName(info.provider);
return new AttributedImage({url: info.pictureUrl, provider})
const provider = AllImageProviders.byName(info.provider)
return new AttributedImage({ url: info.pictureUrl, provider })
}
private static asAttributedImage(info: P4CPicture): AttributedImage {
const provider = AllImageProviders.byName(info.provider);
return new AttributedImage({url: info.thumbUrl, provider, date: new Date(info.date)})
const provider = AllImageProviders.byName(info.provider)
return new AttributedImage({ url: info.thumbUrl, provider, date: new Date(info.date) })
}
protected asToggle(info: P4CPicture): ClickableToggle {
const imgNonSelected = NearbyImages.asAttributedImage(info);
const imageSelected = NearbyImages.asAttributedImage(info);
const imgNonSelected = NearbyImages.asAttributedImage(info)
const imageSelected = NearbyImages.asAttributedImage(info)
const nonSelected = new Combine([imgNonSelected]).SetClass("relative block")
const hoveringCheckmark =
new Combine([Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12")]).SetClass("absolute left-1/2 top-1/2 w-0")
const selected = new Combine([
imageSelected,
hoveringCheckmark,
]).SetClass("relative block")
return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick();
const hoveringCheckmark = new Combine([
Svg.confirm_svg().SetClass("block w-24 h-24 -ml-12 -mt-12"),
]).SetClass("absolute left-1/2 top-1/2 w-0")
const selected = new Combine([imageSelected, hoveringCheckmark]).SetClass("relative block")
return new ClickableToggle(selected, nonSelected).SetClass("").ToggleOnClick()
}
}
export class SelectOneNearbyImage extends NearbyImages implements InputElement<P4CPicture> {
private readonly value: UIEventSource<P4CPicture>;
private readonly value: UIEventSource<P4CPicture>
constructor(options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> }, state?: { allElements: ElementStorage }) {
constructor(
options: NearbyImageOptions & { value?: UIEventSource<P4CPicture> },
state?: { allElements: ElementStorage }
) {
super(options, state)
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined);
this.value = options.value ?? new UIEventSource<P4CPicture>(undefined)
}
GetValue(): UIEventSource<P4CPicture> {
return this.value;
return this.value
}
IsValid(t: P4CPicture): boolean {
return false;
return false
}
protected prepareElement(info: P4CPicture): BaseUIElement {
const toggle = super.asToggle(info)
toggle.isEnabled.addCallback(enabled => {
toggle.isEnabled.addCallback((enabled) => {
if (enabled) {
this.value.setData(info)
} else if (this.value.data === info) {
@ -276,7 +308,7 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P
}
})
this.value.addCallback(inf => {
this.value.addCallback((inf) => {
if (inf !== info) {
toggle.isEnabled.setData(false)
}
@ -284,5 +316,4 @@ export class SelectOneNearbyImage extends NearbyImages implements InputElement<P
return toggle
}
}

View file

@ -1,34 +1,34 @@
import Combine from "../Base/Combine";
import {UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Translations from "../i18n/Translations";
import Title from "../Base/Title";
import ValidatedTextField from "../Input/ValidatedTextField";
import {SubtleButton} from "../Base/SubtleButton";
import Svg from "../../Svg";
import {LocalStorageSource} from "../../Logic/Web/LocalStorageSource";
import Toggle from "../Input/Toggle";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
import FilteredLayer from "../../Models/FilteredLayer";
import Combine from "../Base/Combine"
import { UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Translations from "../i18n/Translations"
import Title from "../Base/Title"
import ValidatedTextField from "../Input/ValidatedTextField"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import Toggle from "../Input/Toggle"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
import FilteredLayer from "../../Models/FilteredLayer"
export default class NewNoteUi extends Toggle {
constructor(noteLayer: FilteredLayer,
isShown: UIEventSource<boolean>,
state: {
LastClickLocation: UIEventSource<{ lat: number, lon: number }>,
osmConnection: OsmConnection,
layoutToUse: LayoutConfig,
featurePipeline: FeaturePipeline,
selectedElement: UIEventSource<any>
}) {
const t = Translations.t.notes;
const isCreated = new UIEventSource(false);
state.LastClickLocation.addCallbackAndRun(_ => isCreated.setData(false)) // Reset 'isCreated' on every click
constructor(
noteLayer: FilteredLayer,
isShown: UIEventSource<boolean>,
state: {
LastClickLocation: UIEventSource<{ lat: number; lon: number }>
osmConnection: OsmConnection
layoutToUse: LayoutConfig
featurePipeline: FeaturePipeline
selectedElement: UIEventSource<any>
}
) {
const t = Translations.t.notes
const isCreated = new UIEventSource(false)
state.LastClickLocation.addCallbackAndRun((_) => isCreated.setData(false)) // Reset 'isCreated' on every click
const text = ValidatedTextField.ForType("text").ConstructInputElement({
value: LocalStorageSource.Get("note-text")
value: LocalStorageSource.Get("note-text"),
})
text.SetClass("border rounded-sm border-grey-500")
@ -36,29 +36,31 @@ export default class NewNoteUi extends Toggle {
postNote.onClick(async () => {
let txt = text.GetValue().data
if (txt === undefined || txt === "") {
return;
return
}
txt += "\n\n #MapComplete #" + state?.layoutToUse?.id
const loc = state.LastClickLocation.data;
const loc = state.LastClickLocation.data
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
const feature = {
type:"Feature",
geometry:{
type:"Point",
coordinates: [loc.lon, loc.lat]
type: "Feature",
geometry: {
type: "Point",
coordinates: [loc.lon, loc.lat],
},
properties: {
id: ""+id.id,
id: "" + id.id,
date_created: new Date().toISOString(),
_first_comment : txt,
comments:JSON.stringify( [{
text: txt,
html: txt,
user: state.osmConnection?.userDetails?.data?.name,
uid: state.osmConnection?.userDetails?.data?.uid
}]),
_first_comment: txt,
comments: JSON.stringify([
{
text: txt,
html: txt,
user: state.osmConnection?.userDetails?.data?.name,
uid: state.osmConnection?.userDetails?.data?.uid,
},
]),
},
};
}
state?.featurePipeline?.InjectNewPoint(feature)
state.selectedElement?.setData(feature)
text.GetValue().setData("")
@ -68,56 +70,53 @@ export default class NewNoteUi extends Toggle {
new Title(t.createNoteTitle),
t.createNoteIntro,
text,
new Combine([new Toggle(undefined, t.warnAnonymous.SetClass("alert"), state?.osmConnection?.isLoggedIn),
new Toggle(postNote,
new Combine([
new Toggle(
undefined,
t.warnAnonymous.SetClass("alert"),
state?.osmConnection?.isLoggedIn
),
new Toggle(
postNote,
t.textNeeded.SetClass("alert"),
text.GetValue().map(txt => txt?.length > 3)
)
]).SetClass("flex justify-end items-center")
]).SetClass("flex flex-col border-2 border-black rounded-xl p-4");
text.GetValue().map((txt) => txt?.length > 3)
),
]).SetClass("flex justify-end items-center"),
]).SetClass("flex flex-col border-2 border-black rounded-xl p-4")
const newNoteUi = new Toggle(
new Toggle(t.isCreated.SetClass("thanks"),
createNoteDialog,
isCreated
),
new Toggle(t.isCreated.SetClass("thanks"), createNoteDialog, isCreated),
undefined,
new UIEventSource<boolean>(true)
)
super(
new Toggle(
new Combine(
[
t.noteLayerHasFilters.SetClass("alert"),
new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => {
const filters = noteLayer.appliedFilters.data;
for (const key of Array.from(filters.keys())) {
filters.set(key, undefined)
}
noteLayer.appliedFilters.ping()
isShown.setData(false);
})
]
).SetClass("flex flex-col"),
new Combine([
t.noteLayerHasFilters.SetClass("alert"),
new SubtleButton(Svg.filter_svg(), t.disableAllNoteFilters).onClick(() => {
const filters = noteLayer.appliedFilters.data
for (const key of Array.from(filters.keys())) {
filters.set(key, undefined)
}
noteLayer.appliedFilters.ping()
isShown.setData(false)
}),
]).SetClass("flex flex-col"),
newNoteUi,
noteLayer.appliedFilters.map(filters => {
noteLayer.appliedFilters.map((filters) => {
console.log("Applied filters for notes are: ", filters)
return Array.from(filters.values()).some(v => v?.currentFilter !== undefined);
return Array.from(filters.values()).some((v) => v?.currentFilter !== undefined)
})
),
new Combine([
t.noteLayerNotEnabled.SetClass("alert"),
new SubtleButton(Svg.layers_svg(), t.noteLayerDoEnable).onClick(() => {
noteLayer.isDisplayed.setData(true);
isShown.setData(false);
})
noteLayer.isDisplayed.setData(true)
isShown.setData(false)
}),
]).SetClass("flex flex-col"),
noteLayer.isDisplayed
);
)
}
}

View file

@ -1,30 +1,29 @@
import Combine from "../Base/Combine";
import BaseUIElement from "../BaseUIElement";
import Svg from "../../Svg";
import Link from "../Base/Link";
import {FixedUiElement} from "../Base/FixedUiElement";
import Translations from "../i18n/Translations";
import {Utils} from "../../Utils";
import Img from "../Base/Img";
import {SlideShow} from "../Image/SlideShow";
import {Stores, UIEventSource} from "../../Logic/UIEventSource";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {VariableUiElement} from "../Base/VariableUIElement";
import Combine from "../Base/Combine"
import BaseUIElement from "../BaseUIElement"
import Svg from "../../Svg"
import Link from "../Base/Link"
import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Img from "../Base/Img"
import { SlideShow } from "../Image/SlideShow"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../Base/VariableUIElement"
export default class NoteCommentElement extends Combine {
constructor(comment: {
"date": string,
"uid": number,
"user": string,
"user_url": string,
"action": "closed" | "opened" | "reopened" | "commented",
"text": string, "html": string
date: string
uid: number
user: string
user_url: string
action: "closed" | "opened" | "reopened" | "commented"
text: string
html: string
}) {
const t = Translations.t.notes;
const t = Translations.t.notes
let actionIcon: BaseUIElement;
let actionIcon: BaseUIElement
if (comment.action === "opened" || comment.action === "reopened") {
actionIcon = Svg.note_svg()
} else if (comment.action === "closed") {
@ -40,30 +39,37 @@ export default class NoteCommentElement extends Combine {
user = new Link(comment.user, comment.user_url ?? "", true)
}
let userinfo = Stores.FromPromise( Utils.downloadJsonCached("https://www.openstreetmap.org/api/0.6/user/"+comment.uid, 24*60*60*1000))
let userImg = new VariableUiElement( userinfo.map(userinfo => {
const href = userinfo?.user?.img?.href;
if(href !== undefined){
return new Img(href).SetClass("rounded-full w-8 h-8 mr-4")
}
return undefined
}))
let userinfo = Stores.FromPromise(
Utils.downloadJsonCached(
"https://www.openstreetmap.org/api/0.6/user/" + comment.uid,
24 * 60 * 60 * 1000
)
)
let userImg = new VariableUiElement(
userinfo.map((userinfo) => {
const href = userinfo?.user?.img?.href
if (href !== undefined) {
return new Img(href).SetClass("rounded-full w-8 h-8 mr-4")
}
return undefined
})
)
const htmlElement = document.createElement("div")
htmlElement.innerHTML = comment.html
const images = Array.from(htmlElement.getElementsByTagName("a"))
.map(link => link.href)
.filter(link => {
.map((link) => link.href)
.filter((link) => {
link = link.toLowerCase()
const lastDotIndex = link.lastIndexOf('.')
const lastDotIndex = link.lastIndexOf(".")
const extension = link.substring(lastDotIndex + 1, link.length)
return Utils.imageExtensions.has(extension)
})
let imagesEl: BaseUIElement = undefined;
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map(i => new Img(i)
.SetClass("w-full block")
.SetStyle("min-width: 50px; background: grey;"));
const imageEls = images.map((i) =>
new Img(i).SetClass("w-full block").SetStyle("min-width: 50px; background: grey;")
)
imagesEl = new SlideShow(new UIEventSource<BaseUIElement[]>(imageEls)).SetClass("mb-1")
}
@ -73,32 +79,36 @@ export default class NoteCommentElement extends Combine {
new FixedUiElement(comment.html).SetClass("flex flex-col").SetStyle("margin: 0"),
]).SetClass("flex"),
imagesEl,
new Combine([userImg, user.SetClass("mr-2"), comment.date]).SetClass("flex justify-end items-center subtle")
new Combine([userImg, user.SetClass("mr-2"), comment.date]).SetClass(
"flex justify-end items-center subtle"
),
])
this.SetClass("flex flex-col pb-2 mb-2 border-gray-500 border-b")
}
public static addCommentTo(txt: string, tags: UIEventSource<any>, state: { osmConnection: OsmConnection }) {
public static addCommentTo(
txt: string,
tags: UIEventSource<any>,
state: { osmConnection: OsmConnection }
) {
const comments: any[] = JSON.parse(tags.data["comments"])
const username = state.osmConnection.userDetails.data.name
var urlRegex = /(https?:\/\/[^\s]+)/g;
var urlRegex = /(https?:\/\/[^\s]+)/g
const html = txt.replace(urlRegex, function (url) {
return '<a href="' + url + '">' + url + '</a>';
return '<a href="' + url + '">' + url + "</a>"
})
comments.push({
"date": new Date().toISOString(),
"uid": state.osmConnection.userDetails.data.uid,
"user": username,
"user_url": "https://www.openstreetmap.org/user/" + username,
"action": "commented",
"text": txt,
"html": html
date: new Date().toISOString(),
uid: state.osmConnection.userDetails.data.uid,
user: username,
user_url: "https://www.openstreetmap.org/user/" + username,
action: "commented",
text: txt,
html: html,
})
tags.data["comments"] = JSON.stringify(comments)
tags.ping()
}
}
}

View file

@ -1,115 +1,126 @@
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingQuestion from "./TagRenderingQuestion";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import {Unit} from "../../Models/Unit";
import Lazy from "../Base/Lazy";
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import TagRenderingQuestion from "./TagRenderingQuestion"
import Translations from "../i18n/Translations"
import Combine from "../Base/Combine"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import { Unit } from "../../Models/Unit"
import Lazy from "../Base/Lazy"
/**
* Generates all the questions, one by one
*/
export default class QuestionBox extends VariableUiElement {
public readonly skippedQuestions: UIEventSource<number[]>;
public readonly restingQuestions: Store<BaseUIElement[]>;
constructor(state, options: {
tagsSource: UIEventSource<any>,
tagRenderings: TagRenderingConfig[], units: Unit[],
showAllQuestionsAtOnce?: boolean | UIEventSource<boolean>
}) {
public readonly skippedQuestions: UIEventSource<number[]>
public readonly restingQuestions: Store<BaseUIElement[]>
constructor(
state,
options: {
tagsSource: UIEventSource<any>
tagRenderings: TagRenderingConfig[]
units: Unit[]
showAllQuestionsAtOnce?: boolean | UIEventSource<boolean>
}
) {
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
const tagsSource = options.tagsSource
const units = options.units
options.showAllQuestionsAtOnce = options.showAllQuestionsAtOnce ?? false
const tagRenderings = options.tagRenderings
.filter(tr => tr.question !== undefined)
.filter(tr => tr.question !== null)
.filter((tr) => tr.question !== undefined)
.filter((tr) => tr.question !== null)
const tagRenderingQuestions = tagRenderings.map(
(tagRendering, i) =>
new Lazy(
() =>
new TagRenderingQuestion(tagsSource, tagRendering, state, {
units: units,
afterSave: () => {
// We save and indicate progress by pinging and recalculating
skippedQuestions.ping()
},
cancelButton: Translations.t.general.skip
.Clone()
.SetClass("btn btn-secondary")
.onClick(() => {
skippedQuestions.data.push(i)
skippedQuestions.ping()
}),
})
)
)
const tagRenderingQuestions = tagRenderings
.map((tagRendering, i) =>
new Lazy(() => new TagRenderingQuestion(tagsSource, tagRendering, state,
{
units: units,
afterSave: () => {
// We save and indicate progress by pinging and recalculating
skippedQuestions.ping();
},
cancelButton: Translations.t.general.skip.Clone()
.SetClass("btn btn-secondary")
.onClick(() => {
skippedQuestions.data.push(i);
skippedQuestions.ping();
})
const skippedQuestionsButton = Translations.t.general.skippedQuestions.onClick(() => {
skippedQuestions.setData([])
})
tagsSource.map(
(tags) => {
if (tags === undefined) {
return undefined
}
for (let i = 0; i < tagRenderingQuestions.length; i++) {
let tagRendering = tagRenderings[i]
if (skippedQuestions.data.indexOf(i) >= 0) {
continue
}
if (tagRendering.IsKnown(tags)) {
continue
}
if (tagRendering.condition) {
if (!tagRendering.condition.matchesProperties(tags)) {
// Filtered away by the condition, so it is kindof known
continue
}
}
)));
const skippedQuestionsButton = Translations.t.general.skippedQuestions
.onClick(() => {
skippedQuestions.setData([]);
})
tagsSource.map(tags => {
if (tags === undefined) {
return undefined;
}
for (let i = 0; i < tagRenderingQuestions.length; i++) {
let tagRendering = tagRenderings[i];
if (skippedQuestions.data.indexOf(i) >= 0) {
continue;
// this value is NOT known - this is the question we have to show!
return i
}
if (tagRendering.IsKnown(tags)) {
continue;
return undefined // The questions are depleted
},
[skippedQuestions]
)
const questionsToAsk: Store<BaseUIElement[]> = tagsSource.map(
(tags) => {
if (tags === undefined) {
return []
}
if (tagRendering.condition) {
if (!tagRendering.condition.matchesProperties(tags)) {
const qs = []
for (let i = 0; i < tagRenderingQuestions.length; i++) {
let tagRendering = tagRenderings[i]
if (skippedQuestions.data.indexOf(i) >= 0) {
continue
}
if (tagRendering.IsKnown(tags)) {
continue
}
if (tagRendering.condition && !tagRendering.condition.matchesProperties(tags)) {
// Filtered away by the condition, so it is kindof known
continue;
continue
}
// this value is NOT known - this is the question we have to show!
qs.push(tagRenderingQuestions[i])
}
return qs
},
[skippedQuestions]
)
// this value is NOT known - this is the question we have to show!
return i
}
return undefined; // The questions are depleted
}, [skippedQuestions]);
const questionsToAsk: Store<BaseUIElement[]> = tagsSource.map(tags => {
if (tags === undefined) {
return [];
}
const qs = []
for (let i = 0; i < tagRenderingQuestions.length; i++) {
let tagRendering = tagRenderings[i];
if (skippedQuestions.data.indexOf(i) >= 0) {
continue;
}
if (tagRendering.IsKnown(tags)) {
continue;
}
if (tagRendering.condition &&
!tagRendering.condition.matchesProperties(tags)) {
// Filtered away by the condition, so it is kindof known
continue;
}
// this value is NOT known - this is the question we have to show!
qs.push(tagRenderingQuestions[i])
}
return qs
}, [skippedQuestions])
super(questionsToAsk.map(allQuestions => {
super(
questionsToAsk.map((allQuestions) => {
const els: BaseUIElement[] = []
if (options.showAllQuestionsAtOnce === true || options.showAllQuestionsAtOnce["data"]) {
if (
options.showAllQuestionsAtOnce === true ||
options.showAllQuestionsAtOnce["data"]
) {
els.push(...questionsToAsk.data)
} else {
els.push(allQuestions[0])
@ -123,10 +134,7 @@ export default class QuestionBox extends VariableUiElement {
})
)
this.skippedQuestions = skippedQuestions;
this.skippedQuestions = skippedQuestions
this.restingQuestions = questionsToAsk
}
}
}

View file

@ -1,37 +1,33 @@
import {ImmutableStore, Store} from "../../Logic/UIEventSource";
import Translations from "../i18n/Translations";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import Toggle from "../Input/Toggle";
import BaseUIElement from "../BaseUIElement";
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Toggle from "../Input/Toggle"
import BaseUIElement from "../BaseUIElement"
export class SaveButton extends Toggle {
constructor(value: Store<any>, osmConnection: OsmConnection, textEnabled ?: BaseUIElement, textDisabled ?: BaseUIElement) {
constructor(
value: Store<any>,
osmConnection: OsmConnection,
textEnabled?: BaseUIElement,
textDisabled?: BaseUIElement
) {
if (value === undefined) {
throw "No event source for savebutton, something is wrong"
}
const pleaseLogin = Translations.t.general.loginToStart.Clone()
const pleaseLogin = Translations.t.general.loginToStart
.Clone()
.SetClass("login-button-friendly")
.onClick(() => osmConnection?.AttemptLogin())
const isSaveable = value.map((v) => v !== false && (v ?? "") !== "")
const isSaveable = value.map(v => v !== false && (v ?? "") !== "")
const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`);
const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(`btn btn-disabled`);
const save = new Toggle(
saveEnabled,
saveDisabled,
isSaveable
)
super(
save,
pleaseLogin,
osmConnection?.isLoggedIn ?? new ImmutableStore(false)
const saveEnabled = (textEnabled ?? Translations.t.general.save.Clone()).SetClass(`btn`)
const saveDisabled = (textDisabled ?? Translations.t.general.save.Clone()).SetClass(
`btn btn-disabled`
)
const save = new Toggle(saveEnabled, saveDisabled, isSaveable)
super(save, pleaseLogin, osmConnection?.isLoggedIn ?? new ImmutableStore(false))
}
}
}

View file

@ -1,31 +1,35 @@
import Toggle from "../Input/Toggle";
import Svg from "../../Svg";
import {UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Minimap from "../Base/Minimap";
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
import {GeoOperations} from "../../Logic/GeoOperations";
import {LeafletMouseEvent} from "leaflet";
import Combine from "../Base/Combine";
import {Button} from "../Base/Button";
import Translations from "../i18n/Translations";
import SplitAction from "../../Logic/Osm/Actions/SplitAction";
import Title from "../Base/Title";
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
import {BBox} from "../../Logic/BBox";
import Toggle from "../Input/Toggle"
import Svg from "../../Svg"
import { UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Minimap from "../Base/Minimap"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import { GeoOperations } from "../../Logic/GeoOperations"
import { LeafletMouseEvent } from "leaflet"
import Combine from "../Base/Combine"
import { Button } from "../Base/Button"
import Translations from "../i18n/Translations"
import SplitAction from "../../Logic/Osm/Actions/SplitAction"
import Title from "../Base/Title"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox"
import * as split_point from "../../assets/layers/split_point/split_point.json"
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {Changes} from "../../Logic/Osm/Changes";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {ElementStorage} from "../../Logic/ElementStorage";
import BaseLayer from "../../Models/BaseLayer";
import FilteredLayer from "../../Models/FilteredLayer";
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Changes } from "../../Logic/Osm/Changes"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ElementStorage } from "../../Logic/ElementStorage"
import BaseLayer from "../../Models/BaseLayer"
import FilteredLayer from "../../Models/FilteredLayer"
export default class SplitRoadWizard extends Toggle {
// @ts-ignore
private static splitLayerStyling = new LayerConfig(split_point, "(BUILTIN) SplitRoadWizard.ts", true)
private static splitLayerStyling = new LayerConfig(
split_point,
"(BUILTIN) SplitRoadWizard.ts",
true
)
public dialogIsOpened: UIEventSource<boolean>
@ -35,42 +39,42 @@ export default class SplitRoadWizard extends Toggle {
* @param id: The id of the road to remove
* @param state: the state of the application
*/
constructor(id: string, state: {
filteredLayers: UIEventSource<FilteredLayer[]>;
backgroundLayer: UIEventSource<BaseLayer>;
featureSwitchIsTesting: UIEventSource<boolean>;
featureSwitchIsDebugging: UIEventSource<boolean>;
featureSwitchShowAllQuestions: UIEventSource<boolean>;
osmConnection: OsmConnection,
featureSwitchUserbadge: UIEventSource<boolean>,
changes: Changes,
layoutToUse: LayoutConfig,
allElements: ElementStorage
}) {
const t = Translations.t.split;
constructor(
id: string,
state: {
filteredLayers: UIEventSource<FilteredLayer[]>
backgroundLayer: UIEventSource<BaseLayer>
featureSwitchIsTesting: UIEventSource<boolean>
featureSwitchIsDebugging: UIEventSource<boolean>
featureSwitchShowAllQuestions: UIEventSource<boolean>
osmConnection: OsmConnection
featureSwitchUserbadge: UIEventSource<boolean>
changes: Changes
layoutToUse: LayoutConfig
allElements: ElementStorage
}
) {
const t = Translations.t.split
// Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
const splitPoints = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([])
const hasBeenSplit = new UIEventSource(false)
// Toggle variable between show split button and map
const splitClicked = new UIEventSource<boolean>(false);
const splitClicked = new UIEventSource<boolean>(false)
// Load the road with given id on the minimap
const roadElement = state.allElements.ContainingFeatures.get(id)
// Minimap on which you can select the points to be splitted
const miniMap = Minimap.createMiniMap(
{
background: state.backgroundLayer,
allowMoving: true,
leafletOptions: {
minZoom: 14
}
});
miniMap.SetStyle("width: 100%; height: 24rem")
.SetClass("rounded-xl overflow-hidden");
const miniMap = Minimap.createMiniMap({
background: state.backgroundLayer,
allowMoving: true,
leafletOptions: {
minZoom: 14,
},
})
miniMap.SetStyle("width: 100%; height: 24rem").SetClass("rounded-xl overflow-hidden")
miniMap.installBounds(BBox.get(roadElement).pad(0.25), false)
@ -82,7 +86,7 @@ export default class SplitRoadWizard extends Toggle {
layers: state.filteredLayers,
leafletMap: miniMap.leafletMap,
zoomToFeatures: true,
state
state,
})
new ShowDataLayer({
@ -90,10 +94,9 @@ export default class SplitRoadWizard extends Toggle {
leafletMap: miniMap.leafletMap,
zoomToFeatures: false,
layerToShow: SplitRoadWizard.splitLayerStyling,
state
state,
})
/**
* Handles a click on the overleaf map.
* Finds the closest intersection with the road and adds a point there, ready to confirm the cut.
@ -101,9 +104,12 @@ export default class SplitRoadWizard extends Toggle {
*/
function onMapClick(coordinates) {
// First, we check if there is another, already existing point nearby
const points = splitPoints.data.map((f, i) => [f.feature, i])
.filter(p => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5)
.map(p => p[1])
const points = splitPoints.data
.map((f, i) => [f.feature, i])
.filter(
(p) => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5
)
.map((p) => p[1])
.sort((a, b) => a - b)
.reverse(/*Copy/derived list, inplace reverse is fine*/)
if (points.length > 0) {
@ -111,70 +117,87 @@ export default class SplitRoadWizard extends Toggle {
splitPoints.data.splice(point, 1)
}
splitPoints.ping()
return;
return
}
// Get nearest point on the road
const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates); // pointOnRoad is a geojson
const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates) // pointOnRoad is a geojson
// Update point properties to let it match the layer
pointOnRoad.properties["_split_point"] = "yes";
pointOnRoad.properties["_split_point"] = "yes"
// Add it to the list of all points and notify observers
splitPoints.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer
splitPoints.ping(); // not updated using .setData, so manually ping observers
splitPoints.data.push({ feature: pointOnRoad, freshness: new Date() }) // show the point on the data layer
splitPoints.ping() // not updated using .setData, so manually ping observers
}
// When clicked, pass clicked location coordinates to onMapClick function
miniMap.leafletMap.addCallbackAndRunD(
(leafletMap) => leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => {
miniMap.leafletMap.addCallbackAndRunD((leafletMap) =>
leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => {
onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat])
}))
// Toggle between splitmap
const splitButton = new SubtleButton(Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), t.inviteToSplit.Clone().SetClass("text-lg font-bold"));
splitButton.onClick(
() => {
splitClicked.setData(true)
}
})
)
// Toggle between splitmap
const splitButton = new SubtleButton(
Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"),
t.inviteToSplit.Clone().SetClass("text-lg font-bold")
)
splitButton.onClick(() => {
splitClicked.setData(true)
})
// Only show the splitButton if logged in, else show login prompt
const loginBtn = t.loginToSplit.Clone()
const loginBtn = t.loginToSplit
.Clone()
.onClick(() => state.osmConnection.AttemptLogin())
.SetClass("login-button-friendly");
.SetClass("login-button-friendly")
const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn)
// Save button
const saveButton = new Button(t.split.Clone(), () => {
hasBeenSplit.setData(true)
state.changes.applyAction(new SplitAction(id, splitPoints.data.map(ff => ff.feature.geometry.coordinates), {
theme: state?.layoutToUse?.id
}))
state.changes.applyAction(
new SplitAction(
id,
splitPoints.data.map((ff) => ff.feature.geometry.coordinates),
{
theme: state?.layoutToUse?.id,
}
)
)
})
saveButton.SetClass("btn btn-primary mr-3");
const disabledSaveButton = new Button("Split", undefined);
disabledSaveButton.SetClass("btn btn-disabled mr-3");
saveButton.SetClass("btn btn-primary mr-3")
const disabledSaveButton = new Button("Split", undefined)
disabledSaveButton.SetClass("btn btn-disabled mr-3")
// Only show the save button if there are split points defined
const saveToggle = new Toggle(disabledSaveButton, saveButton, splitPoints.map((data) => data.length === 0))
const saveToggle = new Toggle(
disabledSaveButton,
saveButton,
splitPoints.map((data) => data.length === 0)
)
const cancelButton = Translations.t.general.cancel.Clone() // Not using Button() element to prevent full width button
const cancelButton = Translations.t.general.cancel
.Clone() // Not using Button() element to prevent full width button
.SetClass("btn btn-secondary mr-3")
.onClick(() => {
splitPoints.setData([]);
splitClicked.setData(false);
});
splitPoints.setData([])
splitClicked.setData(false)
})
cancelButton.SetClass("btn btn-secondary block");
cancelButton.SetClass("btn btn-secondary block")
const splitTitle = new Title(t.splitTitle);
const splitTitle = new Title(t.splitTitle)
const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle]).SetClass("flex flex-row")]);
const mapView = new Combine([
splitTitle,
miniMap,
new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"),
])
mapView.SetClass("question")
const confirm = new Toggle(mapView, splitToggle, splitClicked);
const confirm = new Toggle(mapView, splitToggle, splitClicked)
super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit)
this.dialogIsOpened = splitClicked
}
}
}

View file

@ -1,73 +1,94 @@
import {AutoAction} from "./AutoApplyButton";
import Translations from "../i18n/Translations";
import {VariableUiElement} from "../Base/VariableUIElement";
import BaseUIElement from "../BaseUIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Store, UIEventSource} from "../../Logic/UIEventSource";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import {And} from "../../Logic/Tags/And";
import Toggle from "../Input/Toggle";
import {Utils} from "../../Utils";
import {Tag} from "../../Logic/Tags/Tag";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import {Changes} from "../../Logic/Osm/Changes";
import { AutoAction } from "./AutoApplyButton"
import Translations from "../i18n/Translations"
import { VariableUiElement } from "../Base/VariableUIElement"
import BaseUIElement from "../BaseUIElement"
import { FixedUiElement } from "../Base/FixedUiElement"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import { And } from "../../Logic/Tags/And"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import { Tag } from "../../Logic/Tags/Tag"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes"
export default class TagApplyButton implements AutoAction {
public readonly funcName = "tag_apply";
public readonly docs = "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + Utils.Special_visualizations_tagsToApplyHelpText;
public readonly supportsAutoAction = true;
public readonly funcName = "tag_apply"
public readonly docs =
"Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" +
Utils.Special_visualizations_tagsToApplyHelpText
public readonly supportsAutoAction = true
public readonly args = [
{
name: "tags_to_apply",
doc: "A specification of the tags to apply"
doc: "A specification of the tags to apply",
},
{
name: "message",
doc: "The text to show to the contributor"
doc: "The text to show to the contributor",
},
{
name: "image",
doc: "An image to show to the contributor on the button"
doc: "An image to show to the contributor on the button",
},
{
name: "id_of_object_to_apply_this_one",
defaultValue: undefined,
doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element"
}
];
public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)";
doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element",
},
]
public readonly example =
"`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> {
/**
* Parses a tag specification
*
* TagApplyButton.parseTagSpec("key=value;key0=value0") // => [["key","value"],["key0","value0"]]
*
* // Should handle escaped ";"
* TagApplyButton.parseTagSpec("key=value;key0=value0\\;value1") // => [["key","value"],["key0","value0;value1"]]
*/
private static parseTagSpec(spec: string): [string, string][]{
const tgsSpec : [string, string][] = []
// Check whether we need to look up a single value
if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")){
// We seem to be dealing with a single value, fetch it
spec = tagSource.data[spec.replace("$","")]
}
const tgsSpec = spec.split(";").map(spec => {
const kv = spec.split("=").map(s => s.trim());
if (kv.length != 2) {
while(spec.length > 0){
const [part] = spec.match(/((\\;)|[^;])*/)
spec = spec.substring(part.length + 1) // +1 to remove the pending ';' as well
const kv = part.split("=").map((s) => s.trim().replace("\\;",";"))
if (kv.length == 2) {
tgsSpec.push(<[string, string]> kv)
}else if (kv.length < 2) {
throw "Invalid key spec: no '=' found in " + spec
}else{
throw "Invalid key spec: multiple '=' found in " + spec
}
return kv
})
for (const spec of tgsSpec) {
if (spec[0].endsWith(':')) {
throw "A tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
}
}
return tagSource.map(tags => {
const newTags: Tag [] = []
for (const [key, value] of tgsSpec) {
if (value.indexOf('$') >= 0) {
for (const spec of tgsSpec) {
if (spec[0].endsWith(":")) {
throw "The key for a tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey"
}
}
return tgsSpec
}
public static generateTagsToApply(spec: string, tagSource: Store<any>): Store<Tag[]> {
// Check whether we need to look up a single value
if (!spec.includes(";") && !spec.includes("=") && spec.includes("$")) {
// We seem to be dealing with a single value, fetch it
spec = tagSource.data[spec.replace("$", "")]
}
const tgsSpec = TagApplyButton.parseTagSpec(spec)
return tagSource.map((tags) => {
const newTags: Tag[] = []
for (const [key, value] of tgsSpec) {
if (value.indexOf("$") >= 0) {
let parts = value.split("$")
// THe first of the split won't start with a '$', so no substitution needed
let actualValue = parts[0]
@ -84,29 +105,37 @@ export default class TagApplyButton implements AutoAction {
}
return newTags
})
}
async applyActionOn(state: {
layoutToUse: LayoutConfig,
changes: Changes
}, tags: UIEventSource<any>, args: string[]): Promise<void> {
async applyActionOn(
state: {
layoutToUse: LayoutConfig
changes: Changes
},
tags: UIEventSource<any>,
args: string[]
): Promise<void> {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
const targetIdKey = args[3]
const targetId = tags.data[targetIdKey] ?? tags.data.id
const changeAction = new ChangeTagAction(targetId,
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!
{
theme: state.layoutToUse.id,
changeType: "answer"
changeType: "answer",
}
)
await state.changes.applyAction(changeAction)
}
public constr(state: FeaturePipelineState, tags: UIEventSource<any>, args: string[]): BaseUIElement {
public constr(
state: FeaturePipelineState,
tags: UIEventSource<any>,
args: string[]
): BaseUIElement {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
const msg = args[1]
let image = args[2]?.trim()
@ -116,32 +145,31 @@ export default class TagApplyButton implements AutoAction {
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("&");
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})
el = t.appliedOnAnotherObject.Subs({ tags: tagsStr, id: targetId })
}
return el;
}
)).SetClass("subtle")
return el
})
).SetClass("subtle")
const self = this
const applied = new UIEventSource(false)
const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col"))
.onClick(() => {
self.applyActionOn(state, tags, args)
applied.setData(true)
})
const applyButton = new SubtleButton(
image,
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(() => {
self.applyActionOn(state, tags, args)
applied.setData(true)
})
return new Toggle(
new Toggle(
t.isApplied.SetClass("thanks"),
applyButton,
applied
),
undefined, state.osmConnection.isLoggedIn)
new Toggle(t.isApplied.SetClass("thanks"), applyButton, applied),
undefined,
state.osmConnection.isLoggedIn
)
}
}

View file

@ -1,61 +1,78 @@
import {UIEventSource} from "../../Logic/UIEventSource";
import {Utils} from "../../Utils";
import BaseUIElement from "../BaseUIElement";
import {VariableUiElement} from "../Base/VariableUIElement";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import Combine from "../Base/Combine";
import Img from "../Base/Img";
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import Combine from "../Base/Combine"
import Img from "../Base/Img"
/***
* Displays the correct value for a known tagrendering
*/
export default class TagRenderingAnswer extends VariableUiElement {
constructor(tagsSource: UIEventSource<any>, configuration: TagRenderingConfig,
state: any,
contentClasses: string = "", contentStyle: string = "", options?: {
constructor(
tagsSource: UIEventSource<any>,
configuration: TagRenderingConfig,
state: any,
contentClasses: string = "",
contentStyle: string = "",
options?: {
specialViz: Map<string, BaseUIElement>
}) {
}
) {
if (configuration === undefined) {
throw "Trying to generate a tagRenderingAnswer without configuration..."
}
if (tagsSource === undefined) {
throw "Trying to generate a tagRenderingAnswer without tagSource..."
}
super(tagsSource.map(tags => {
if (tags === undefined) {
return undefined;
}
super(
tagsSource
.map((tags) => {
if (tags === undefined) {
return undefined
}
if (configuration.condition) {
if (!configuration.condition.matchesProperties(tags)) {
return undefined;
}
}
if (configuration.condition) {
if (!configuration.condition.matchesProperties(tags)) {
return undefined
}
}
const trs = Utils.NoNull(configuration.GetRenderValues(tags));
if (trs.length === 0) {
return undefined;
}
const trs = Utils.NoNull(configuration.GetRenderValues(tags))
if (trs.length === 0) {
return undefined
}
const valuesToRender: BaseUIElement[] = trs.map(tr => {
const text = new SubstitutedTranslation(tr.then, tagsSource, state, options?.specialViz);
if(tr.icon === undefined){
return text
}
return new Combine([new Img(tr.icon).SetClass("mapping-icon-"+(tr.iconClass ?? "small")), text]).SetClass("flex items-center")
})
if (valuesToRender.length === 1) {
return valuesToRender[0];
} else if (valuesToRender.length > 1) {
return new Combine(valuesToRender).SetClass("flex flex-col")
}
return undefined;
}).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle)))
const valuesToRender: BaseUIElement[] = trs.map((tr) => {
const text = new SubstitutedTranslation(
tr.then,
tagsSource,
state,
options?.specialViz
)
if (tr.icon === undefined) {
return text
}
return new Combine([
new Img(tr.icon).SetClass("mapping-icon-" + (tr.iconClass ?? "small")),
text,
]).SetClass("flex items-center")
})
if (valuesToRender.length === 1) {
return valuesToRender[0]
} else if (valuesToRender.length > 1) {
return new Combine(valuesToRender).SetClass("flex flex-col")
}
return undefined
})
.map((element: BaseUIElement) =>
element?.SetClass(contentClasses)?.SetStyle(contentStyle)
)
)
this.SetClass("flex items-center flex-row text-lg link-underline")
this.SetStyle("word-wrap: anywhere;");
this.SetStyle("word-wrap: anywhere;")
}
}
}

View file

@ -1,59 +1,58 @@
import {Store, Stores, UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import {InputElement, ReadonlyInputElement} from "../Input/InputElement";
import ValidatedTextField from "../Input/ValidatedTextField";
import {FixedInputElement} from "../Input/FixedInputElement";
import {RadioButton} from "../Input/RadioButton";
import {Utils} from "../../Utils";
import CheckBoxes from "../Input/Checkboxes";
import InputElementMap from "../Input/InputElementMap";
import {SaveButton} from "./SaveButton";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation";
import Constants from "../../Models/Constants";
import {SubstitutedTranslation} from "../SubstitutedTranslation";
import {TagsFilter} from "../../Logic/Tags/TagsFilter";
import {Tag} from "../../Logic/Tags/Tag";
import {And} from "../../Logic/Tags/And";
import {TagUtils} from "../../Logic/Tags/TagUtils";
import BaseUIElement from "../BaseUIElement";
import {DropDown} from "../Input/DropDown";
import InputElementWrapper from "../Input/InputElementWrapper";
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
import TagRenderingConfig, {Mapping} from "../../Models/ThemeConfig/TagRenderingConfig";
import {Unit} from "../../Models/Unit";
import VariableInputElement from "../Input/VariableInputElement";
import Toggle from "../Input/Toggle";
import Img from "../Base/Img";
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
import Title from "../Base/Title";
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
import {GeoOperations} from "../../Logic/GeoOperations";
import {SearchablePillsSelector} from "../Input/SearchableMappingsSelector";
import {OsmTags} from "../../Models/OsmFeature";
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import Combine from "../Base/Combine"
import { InputElement, ReadonlyInputElement } from "../Input/InputElement"
import ValidatedTextField from "../Input/ValidatedTextField"
import { FixedInputElement } from "../Input/FixedInputElement"
import { RadioButton } from "../Input/RadioButton"
import { Utils } from "../../Utils"
import CheckBoxes from "../Input/Checkboxes"
import InputElementMap from "../Input/InputElementMap"
import { SaveButton } from "./SaveButton"
import { VariableUiElement } from "../Base/VariableUIElement"
import Translations from "../i18n/Translations"
import { FixedUiElement } from "../Base/FixedUiElement"
import { Translation } from "../i18n/Translation"
import Constants from "../../Models/Constants"
import { SubstitutedTranslation } from "../SubstitutedTranslation"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import { Tag } from "../../Logic/Tags/Tag"
import { And } from "../../Logic/Tags/And"
import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils"
import BaseUIElement from "../BaseUIElement"
import { DropDown } from "../Input/DropDown"
import InputElementWrapper from "../Input/InputElementWrapper"
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig"
import { Unit } from "../../Models/Unit"
import VariableInputElement from "../Input/VariableInputElement"
import Toggle from "../Input/Toggle"
import Img from "../Base/Img"
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
import Title from "../Base/Title"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { GeoOperations } from "../../Logic/GeoOperations"
import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector"
import { OsmTags } from "../../Models/OsmFeature"
/**
* Shows the question element.
* Note that the value _migh_ already be known, e.g. when selected or when changing the value
*/
export default class TagRenderingQuestion extends Combine {
constructor(tags: UIEventSource<Record<string, string> & { id: string }>,
configuration: TagRenderingConfig,
state?: FeaturePipelineState,
options?: {
units?: Unit[],
afterSave?: () => void,
cancelButton?: BaseUIElement,
saveButtonConstr?: (src: Store<TagsFilter>) => BaseUIElement,
bottomText?: (src: Store<TagsFilter>) => BaseUIElement
}
constructor(
tags: UIEventSource<Record<string, string> & { id: string }>,
configuration: TagRenderingConfig,
state?: FeaturePipelineState,
options?: {
units?: Unit[]
afterSave?: () => void
cancelButton?: BaseUIElement
saveButtonConstr?: (src: Store<TagsFilter>) => BaseUIElement
bottomText?: (src: Store<TagsFilter>) => BaseUIElement
}
) {
const applicableMappingsSrc =
Stores.ListStabilized(tags.map(tags => {
const applicableMappingsSrc = Stores.ListStabilized(
tags.map((tags) => {
const applicableMappings: Mapping[] = []
for (const mapping of configuration.mappings ?? []) {
if (mapping.hideInAnswer === true) {
@ -63,83 +62,107 @@ export default class TagRenderingQuestion extends Combine {
applicableMappings.push(mapping)
continue
}
const condition = <TagsFilter>mapping.hideInAnswer;
const condition = <TagsFilter>mapping.hideInAnswer
const isShown = !condition.matchesProperties(tags)
if (isShown) {
applicableMappings.push(mapping)
}
}
return applicableMappings
}));
})
)
if (configuration === undefined) {
throw "A question is needed for a question visualization"
}
options = options ?? {}
const applicableUnit = (options.units ?? []).filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0];
const question = new Title(new SubstitutedTranslation(configuration.question, tags, state)
.SetClass("question-text"), 3);
const applicableUnit = (options.units ?? []).filter((unit) =>
unit.isApplicableToKey(configuration.freeform?.key)
)[0]
const question = new Title(
new SubstitutedTranslation(configuration.question, tags, state).SetClass(
"question-text"
),
3
)
const feedback = new UIEventSource<Translation>(undefined)
const inputElement: ReadonlyInputElement<TagsFilter> =
new VariableInputElement(applicableMappingsSrc.map(applicableMappings => {
return TagRenderingQuestion.GenerateInputElement(state, configuration, applicableMappings, applicableUnit, tags, feedback)
}
))
const inputElement: ReadonlyInputElement<UploadableTag> = new VariableInputElement(
applicableMappingsSrc.map((applicableMappings) => {
return TagRenderingQuestion.GenerateInputElement(
state,
configuration,
applicableMappings,
applicableUnit,
tags,
feedback
)
})
)
const save = () => {
const selection = TagUtils.FlattenMultiAnswer([inputElement.GetValue().data]);
const selection = TagUtils.FlattenMultiAnswer(
TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data)
)
if (selection) {
(state?.changes)
.applyAction(new ChangeTagAction(
tags.data.id, selection, tags.data, {
;(state?.changes)
.applyAction(
new ChangeTagAction(tags.data.id, selection, tags.data, {
theme: state?.layoutToUse?.id ?? "unkown",
changeType: "answer",
}
)).then(_ => {
console.log("Tagchanges applied")
})
})
)
.then((_) => {
console.log("Tagchanges applied")
})
if (options.afterSave) {
options.afterSave();
options.afterSave()
}
}
}
if (options.saveButtonConstr === undefined) {
options.saveButtonConstr = v => new SaveButton(v,
state?.osmConnection)
.onClick(save)
options.saveButtonConstr = (v) => new SaveButton(v, state?.osmConnection).onClick(save)
}
const saveButton = new Combine([
options.saveButtonConstr(inputElement.GetValue()),
])
const saveButton = new Combine([options.saveButtonConstr(inputElement.GetValue())])
let bottomTags: BaseUIElement;
let bottomTags: BaseUIElement
if (options.bottomText !== undefined) {
bottomTags = options.bottomText(inputElement.GetValue())
} else {
bottomTags = TagRenderingQuestion.CreateTagExplanation(inputElement.GetValue(), tags, state)
bottomTags = TagRenderingQuestion.CreateTagExplanation(
inputElement.GetValue(),
tags,
state
)
}
super([
question,
inputElement,
new Combine([
new VariableUiElement(feedback.map(t => t?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem")?.SetClass("alert flex") ?? bottomTags)),
new Combine([
new Combine([options.cancelButton]),
saveButton]).SetClass("flex justify-end flex-wrap-reverse")
new VariableUiElement(
feedback.map(
(t) =>
t
?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem")
?.SetClass("alert flex") ?? bottomTags
)
),
new Combine([new Combine([options.cancelButton]), saveButton]).SetClass(
"flex justify-end flex-wrap-reverse"
),
]).SetClass("flex mt-2 justify-between"),
new Toggle(Translations.t.general.testing.SetClass("alert"), undefined, state?.featureSwitchIsTesting)
new Toggle(
Translations.t.general.testing.SetClass("alert"),
undefined,
state?.featureSwitchIsTesting
),
])
this.SetClass("question disable-links")
}
private static GenerateInputElement(
state: FeaturePipelineState,
configuration: TagRenderingConfig,
@ -147,26 +170,36 @@ export default class TagRenderingQuestion extends Combine {
applicableUnit: Unit,
tagsSource: UIEventSource<any>,
feedback: UIEventSource<Translation>
): ReadonlyInputElement<TagsFilter> {
): ReadonlyInputElement<UploadableTag> {
const hasImages = applicableMappings.findIndex((mapping) => mapping.icon !== undefined) >= 0
let inputEls: InputElement<UploadableTag>[]
const ifNotsPresent = applicableMappings.some((mapping) => mapping.ifnot !== undefined)
const hasImages = applicableMappings.findIndex(mapping => mapping.icon !== undefined) >= 0
let inputEls: InputElement<TagsFilter>[];
const ifNotsPresent = applicableMappings.some(mapping => mapping.ifnot !== undefined)
if (applicableMappings.length > 8 &&
(configuration.freeform?.type === undefined || configuration.freeform?.type === "string") &&
(!configuration.multiAnswer || configuration.freeform === undefined)) {
return TagRenderingQuestion.GenerateSearchableSelector(state, configuration, applicableMappings, tagsSource)
if (
applicableMappings.length > 8 &&
(configuration.freeform?.type === undefined ||
configuration.freeform?.type === "string") &&
(!configuration.multiAnswer || configuration.freeform === undefined)
) {
return TagRenderingQuestion.GenerateSearchableSelector(
state,
configuration,
applicableMappings,
tagsSource
)
}
// FreeForm input will be undefined if not present; will already contain a special input element if applicable
const ff = TagRenderingQuestion.GenerateFreeform(state, configuration, applicableUnit, tagsSource, feedback);
const ff = TagRenderingQuestion.GenerateFreeform(
state,
configuration,
applicableUnit,
tagsSource,
feedback
)
function allIfNotsExcept(excludeIndex: number): TagsFilter[] {
function allIfNotsExcept(excludeIndex: number): UploadableTag[] {
if (configuration.mappings === undefined || configuration.mappings.length === 0) {
return undefined
}
@ -181,75 +214,109 @@ export default class TagRenderingQuestion extends Combine {
const negativeMappings = []
for (let i = 0; i < applicableMappings.length; i++) {
const mapping = applicableMappings[i];
const mapping = applicableMappings[i]
if (i === excludeIndex || mapping.ifnot === undefined) {
continue
}
negativeMappings.push(mapping.ifnot)
}
return Utils.NoNull(negativeMappings)
}
if (applicableMappings.length < 8 || configuration.multiAnswer || (hasImages && applicableMappings.length < 16) || ifNotsPresent) {
inputEls = (applicableMappings ?? []).map((mapping, i) => TagRenderingQuestion.GenerateMappingElement(state, tagsSource, mapping, allIfNotsExcept(i)));
inputEls = Utils.NoNull(inputEls);
if (
applicableMappings.length < 8 ||
configuration.multiAnswer ||
(hasImages && applicableMappings.length < 16) ||
ifNotsPresent
) {
inputEls = (applicableMappings ?? []).map((mapping, i) =>
TagRenderingQuestion.GenerateMappingElement(
state,
tagsSource,
mapping,
allIfNotsExcept(i)
)
)
inputEls = Utils.NoNull(inputEls)
} else {
const dropdown: InputElement<TagsFilter> = new DropDown("",
const dropdown: InputElement<UploadableTag> = new DropDown(
"",
applicableMappings.map((mapping, i) => {
return {
value: new And([mapping.if, ...allIfNotsExcept(i)]),
shown: mapping.then.Subs(tagsSource.data)
shown: mapping.then.Subs(tagsSource.data),
}
})
)
if (ff == undefined) {
return dropdown;
return dropdown
} else {
inputEls = [dropdown]
}
}
if (inputEls.length == 0) {
if (ff === undefined) {
throw "Error: could not generate a question: freeform and all mappings are undefined"
}
return ff;
return ff
}
if (ff) {
inputEls.push(ff);
inputEls.push(ff)
}
if (configuration.multiAnswer) {
return TagRenderingQuestion.GenerateMultiAnswer(configuration, inputEls, ff, applicableMappings.map(mp => mp.ifnot))
return TagRenderingQuestion.GenerateMultiAnswer(
configuration,
inputEls,
ff,
applicableMappings.map((mp) => mp.ifnot)
)
} else {
return new RadioButton(inputEls, {selectFirstAsDefault: false})
return new RadioButton(inputEls, { selectFirstAsDefault: false })
}
}
private static MappingToPillValue(applicableMappings: Mapping[], tagsSource: UIEventSource<OsmTags>, state: FeaturePipelineState): { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] {
const values: { show: BaseUIElement, value: number, mainTerm: Record<string, string>, searchTerms?: Record<string, string[]>, original: Mapping }[] = []
const addIcons = applicableMappings.some(m => m.icon !== undefined)
private static MappingToPillValue(
applicableMappings: Mapping[],
tagsSource: UIEventSource<OsmTags>,
state: FeaturePipelineState
): {
show: BaseUIElement
value: number
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
original: Mapping
}[] {
const values: {
show: BaseUIElement
value: number
mainTerm: Record<string, string>
searchTerms?: Record<string, string[]>
original: Mapping
}[] = []
const addIcons = applicableMappings.some((m) => m.icon !== undefined)
for (let i = 0; i < applicableMappings.length; i++) {
const mapping = applicableMappings[i];
const mapping = applicableMappings[i]
const tr = mapping.then.Subs(tagsSource.data)
const patchedMapping = <Mapping>{
...mapping,
iconClass: mapping.iconClass ?? `small-height`,
icon: mapping.icon ?? (addIcons ? "./assets/svg/none.svg" : undefined)
}
const fancy = TagRenderingQuestion.GenerateMappingContent(patchedMapping, tagsSource, state).SetClass("normal-background")
const fancy = TagRenderingQuestion.GenerateMappingContent(
patchedMapping,
tagsSource,
state
).SetClass("normal-background")
values.push({
show: fancy,
value: i,
mainTerm: tr.translations,
searchTerms: mapping.searchTerms,
original: mapping
original: mapping,
})
}
return values
@ -265,7 +332,6 @@ export default class TagRenderingQuestion extends Combine {
* freeform: {
* key:"key"
* },
*
* mappings: [
* {
* if:"x=y",
@ -296,7 +362,6 @@ export default class TagRenderingQuestion extends Combine {
* freeform: {
* key:"key"
* },
*
* mappings: [
* {
* if:"x=y",
@ -327,41 +392,57 @@ export default class TagRenderingQuestion extends Combine {
tagsSource: UIEventSource<OsmTags>,
options?: {
search: UIEventSource<string>
}): InputElement<TagsFilter> {
}
): InputElement<UploadableTag> {
const values = TagRenderingQuestion.MappingToPillValue(
applicableMappings,
tagsSource,
state
)
const values = TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state)
const searchValue: UIEventSource<string> = options?.search ?? new UIEventSource<string>(undefined)
const searchValue: UIEventSource<string> =
options?.search ?? new UIEventSource<string>(undefined)
const ff = configuration.freeform
let onEmpty: BaseUIElement = undefined
if (ff !== undefined) {
onEmpty = new VariableUiElement(searchValue.map(search => configuration.render.Subs({[ff.key]: search})))
onEmpty = new VariableUiElement(
searchValue.map((search) => configuration.render.Subs({ [ff.key]: search }))
)
}
const mode = configuration.multiAnswer ? "select-many" : "select-one";
const mode = configuration.multiAnswer ? "select-many" : "select-one"
const tooMuchElementsValue = new UIEventSource<number[]>([]);
const tooMuchElementsValue = new UIEventSource<number[]>([])
let priorityPresets: BaseUIElement = undefined;
let priorityPresets: BaseUIElement = undefined
const classes = "h-64 overflow-scroll"
if (applicableMappings.some(m => m.priorityIf !== undefined)) {
const priorityValues = tagsSource.map(tags =>
TagRenderingQuestion.MappingToPillValue(applicableMappings, tagsSource, state)
.filter(v => v.original.priorityIf?.matchesProperties(tags)))
priorityPresets = new VariableUiElement(priorityValues.map(priority => {
if (priority.length === 0) {
return Translations.t.general.useSearch;
}
return new Combine([
Translations.t.general.useSearchForMore.Subs({total: applicableMappings.length}),
new SearchablePillsSelector(priority, {
selectedElements: tooMuchElementsValue,
hideSearchBar: true,
mode
})]).SetClass("flex flex-col items-center ").SetClass(classes);
}));
if (applicableMappings.some((m) => m.priorityIf !== undefined)) {
const priorityValues = tagsSource.map((tags) =>
TagRenderingQuestion.MappingToPillValue(
applicableMappings,
tagsSource,
state
).filter((v) => v.original.priorityIf?.matchesProperties(tags))
)
priorityPresets = new VariableUiElement(
priorityValues.map((priority) => {
if (priority.length === 0) {
return Translations.t.general.useSearch
}
return new Combine([
Translations.t.general.useSearchForMore.Subs({
total: applicableMappings.length,
}),
new SearchablePillsSelector(priority, {
selectedElements: tooMuchElementsValue,
hideSearchBar: true,
mode,
}),
])
.SetClass("flex flex-col items-center ")
.SetClass(classes)
})
)
}
const presetSearch = new SearchablePillsSelector<number>(values, {
selectIfSingle: true,
@ -370,54 +451,60 @@ export default class TagRenderingQuestion extends Combine {
onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"),
searchAreaClass: classes,
onManyElementsValue: tooMuchElementsValue,
onManyElements: priorityPresets
onManyElements: priorityPresets,
})
const fallbackTag = searchValue.map(s => {
const fallbackTag = searchValue.map((s) => {
if (s === undefined || ff?.key === undefined) {
return undefined
}
return new Tag(ff.key, s)
});
return new InputElementMap<number[], And>(presetSearch,
})
return new InputElementMap<number[], And>(
presetSearch,
(x0, x1) => {
if (x0 == x1) {
return true;
return true
}
if (x0 === undefined || x1 === undefined) {
return false;
return false
}
if (x0.and.length !== x1.and.length) {
return false;
return false
}
for (let i = 0; i < x0.and.length; i++) {
if (x1.and[i] != x0.and[i]) {
return false
}
}
return true;
return true
},
(selected) => {
if (ff !== undefined && searchValue.data?.length > 0 && !presetSearch.someMatchFound.data) {
const t = fallbackTag.data;
if (
ff !== undefined &&
searchValue.data?.length > 0 &&
!presetSearch.someMatchFound.data
) {
const t = fallbackTag.data
if (ff.addExtraTags) {
return new And([t, ...ff.addExtraTags])
}
return new And([t]);
return new And([t])
}
if (selected === undefined || selected.length == 0) {
return undefined;
return undefined
}
const tfs = Utils.NoNull(applicableMappings.map((mapping, i) => {
if (selected.indexOf(i) >= 0) {
return mapping.if
} else {
return mapping.ifnot
}
}))
console.log("Got tags", tfs)
return new And(tfs);
const tfs = Utils.NoNull(
applicableMappings.map((mapping, i) => {
if (selected.indexOf(i) >= 0) {
return mapping.if
} else {
return mapping.ifnot
}
})
)
return new And(tfs)
},
(tf) => {
if (tf === undefined) {
@ -426,48 +513,52 @@ export default class TagRenderingQuestion extends Combine {
const selected: number[] = []
for (let i = 0; i < applicableMappings.length; i++) {
const mapping = applicableMappings[i]
if (tf.and.some(t => mapping.if == t)) {
if (tf.and.some((t) => mapping.if == t)) {
selected.push(i)
}
}
return selected;
return selected
},
[searchValue, presetSearch.someMatchFound]
);
)
}
private static GenerateMultiAnswer(
configuration: TagRenderingConfig,
elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>, ifNotSelected: TagsFilter[]): InputElement<TagsFilter> {
const checkBoxes = new CheckBoxes(elements);
elements: InputElement<UploadableTag>[],
freeformField: InputElement<UploadableTag>,
ifNotSelected: UploadableTag[]
): InputElement<UploadableTag> {
const checkBoxes = new CheckBoxes(elements)
const inputEl = new InputElementMap<number[], TagsFilter>(
const inputEl = new InputElementMap<number[], UploadableTag>(
checkBoxes,
(t0, t1) => {
return t0?.shadows(t1) ?? false
},
(indices) => {
if (indices.length === 0) {
return undefined;
return undefined
}
const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data);
const oppositeTags: TagsFilter[] = [];
const tags: UploadableTag[] = indices.map((i) => elements[i].GetValue().data)
const oppositeTags: UploadableTag[] = []
for (let i = 0; i < ifNotSelected.length; i++) {
if (indices.indexOf(i) >= 0) {
continue;
continue
}
const notSelected = ifNotSelected[i];
const notSelected = ifNotSelected[i]
if (notSelected === undefined) {
continue;
continue
}
oppositeTags.push(notSelected);
oppositeTags.push(notSelected)
}
tags.push(TagUtils.FlattenMultiAnswer(oppositeTags));
return TagUtils.FlattenMultiAnswer(tags);
tags.push(TagUtils.FlattenMultiAnswer(oppositeTags))
return TagUtils.FlattenMultiAnswer(tags)
},
(tags: TagsFilter) => {
(tags: UploadableTag) => {
// {key --> values[]}
const presentTags = TagUtils.SplitKeys([tags]);
const presentTags = TagUtils.SplitKeys([tags])
const indices: number[] = []
// We also collect the values that have to be added to the freeform field
let freeformExtras: string[] = []
@ -476,67 +567,67 @@ export default class TagRenderingQuestion extends Combine {
}
for (let j = 0; j < elements.length; j++) {
const inputElement = elements[j];
const inputElement = elements[j]
if (inputElement === freeformField) {
continue;
continue
}
const val = inputElement.GetValue();
const neededTags = TagUtils.SplitKeys([val.data]);
const val = inputElement.GetValue()
const neededTags = TagUtils.SplitKeys([val.data])
// if every 'neededKeys'-value is present in presentKeys, we have a match and enable the index
if (TagUtils.AllKeysAreContained(presentTags, neededTags)) {
indices.push(j);
indices.push(j)
if (freeformExtras.length > 0) {
const freeformsToRemove: string[] = (neededTags[configuration.freeform.key] ?? []);
const freeformsToRemove: string[] =
neededTags[configuration.freeform.key] ?? []
for (const toRm of freeformsToRemove) {
const i = freeformExtras.indexOf(toRm);
const i = freeformExtras.indexOf(toRm)
if (i >= 0) {
freeformExtras.splice(i, 1);
freeformExtras.splice(i, 1)
}
}
}
}
}
if (freeformField) {
if (freeformExtras.length > 0) {
freeformField.GetValue().setData(new Tag(configuration.freeform.key, freeformExtras.join(";")));
freeformField
.GetValue()
.setData(new Tag(configuration.freeform.key, freeformExtras.join(";")))
indices.push(elements.indexOf(freeformField))
} else {
freeformField.GetValue().setData(undefined);
freeformField.GetValue().setData(undefined)
}
}
return indices;
return indices
},
elements.map(el => el.GetValue())
);
elements.map((el) => el.GetValue())
)
freeformField?.GetValue()?.addCallbackAndRun(value => {
freeformField?.GetValue()?.addCallbackAndRun((value) => {
// The list of indices of the selected elements
const es = checkBoxes.GetValue();
const i = elements.length - 1;
const es = checkBoxes.GetValue()
const i = elements.length - 1
// The actual index of the freeform-element
const index = es.data.indexOf(i);
const index = es.data.indexOf(i)
if (value === undefined) {
// No data is set in the freeform text field; so we delete the checkmark if it is selected
if (index >= 0) {
es.data.splice(index, 1);
es.ping();
es.data.splice(index, 1)
es.ping()
}
} else if (index < 0) {
// There is data defined in the checkmark, but the checkmark isn't checked, so we check it
// This is of course because the data changed
es.data.push(i);
es.ping();
es.data.push(i)
es.ping()
}
});
})
return inputEl;
return inputEl
}
/**
* Generates a (Fixed) input element for this mapping.
* Note that the mapping might hide itself if the condition is not met anymore.
@ -546,9 +637,10 @@ export default class TagRenderingQuestion extends Combine {
private static GenerateMappingElement(
state,
tagsSource: UIEventSource<any>,
mapping: Mapping, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
let tagging: TagsFilter = mapping.if;
mapping: Mapping,
ifNot?: UploadableTag[]
): InputElement<UploadableTag> {
let tagging: UploadableTag = mapping.if
if (ifNot !== undefined) {
tagging = new And([mapping.if, ...ifNot])
}
@ -556,69 +648,78 @@ export default class TagRenderingQuestion extends Combine {
tagging = new And([tagging, ...mapping.addExtraTags])
}
return new FixedInputElement(
TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state),
tagging,
(t0, t1) => t1.shadows(t0));
(t0, t1) => t1.shadows(t0)
)
}
private static GenerateMappingContent(mapping: Mapping, tagsSource: UIEventSource<any>, state: FeaturePipelineState): BaseUIElement {
private static GenerateMappingContent(
mapping: Mapping,
tagsSource: UIEventSource<any>,
state: FeaturePipelineState
): BaseUIElement {
const text = new SubstitutedTranslation(mapping.then, tagsSource, state)
if (mapping.icon === undefined) {
return text;
return text
}
return new Combine([new Img(mapping.icon).SetClass("mr-1 mapping-icon-" + (mapping.iconClass ?? "small")), text]).SetClass("flex items-center")
return new Combine([
new Img(mapping.icon).SetClass("mr-1 mapping-icon-" + (mapping.iconClass ?? "small")),
text,
]).SetClass("flex items-center")
}
private static GenerateFreeform(state: FeaturePipelineState, configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>, feedback: UIEventSource<Translation>)
: InputElement<TagsFilter> {
const freeform = configuration.freeform;
private static GenerateFreeform(
state: FeaturePipelineState,
configuration: TagRenderingConfig,
applicableUnit: Unit,
tags: UIEventSource<any>,
feedback: UIEventSource<Translation>
): InputElement<UploadableTag> {
const freeform = configuration.freeform
if (freeform === undefined) {
return undefined;
return undefined
}
const pickString =
(string: any) => {
if (string === "" || string === undefined) {
return undefined;
}
if (string.length >= 255) {
return undefined
}
const pickString = (string: any) => {
if (string === "" || string === undefined) {
return undefined
}
if (string.length >= 255) {
return undefined
}
const tag = new Tag(freeform.key, string);
const tag = new Tag(freeform.key, string)
if (freeform.addExtraTags === undefined) {
return tag;
}
return new And([
tag,
...freeform.addExtraTags
]
);
};
if (freeform.addExtraTags === undefined) {
return tag
}
return new And([tag, ...freeform.addExtraTags])
}
const toString = (tag) => {
if (tag instanceof And) {
for (const subtag of tag.and) {
if (subtag instanceof Tag && subtag.key === freeform.key) {
return subtag.value;
return subtag.value
}
}
return undefined;
return undefined
} else if (tag instanceof Tag) {
return tag.value
}
return undefined;
return undefined
}
const tagsData = tags.data;
const tagsData = tags.data
const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id)
const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0, 0]
console.log("Creating a tr-question with applicableUnit", applicableUnit)
const input: InputElement<string> = ValidatedTextField.ForType(configuration.freeform.type)?.ConstructInputElement({
const input: InputElement<string> = ValidatedTextField.ForType(
configuration.freeform.type
)?.ConstructInputElement({
country: () => tagsData._country,
location: [center[1], center[0]],
mapBackgroundLayer: state?.backgroundLayer,
@ -626,56 +727,66 @@ export default class TagRenderingQuestion extends Combine {
args: configuration.freeform.helperArgs,
feature,
placeholder: configuration.freeform.placeholder,
feedback
});
feedback,
})
// Init with correct value
input?.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
input?.GetValue().setData(tagsData[freeform.key] ?? freeform.default)
// Add a length check
input?.GetValue().addCallbackD((v: string | undefined) => {
if (v?.length >= 255) {
feedback.setData(Translations.t.validation.tooLong.Subs({count: v.length}))
feedback.setData(Translations.t.validation.tooLong.Subs({ count: v.length }))
}
})
let inputTagsFilter: InputElement<TagsFilter> = new InputElementMap(
input, (a, b) => a === b || (a?.shadows(b) ?? false),
pickString, toString
);
let inputTagsFilter: InputElement<UploadableTag> = new InputElementMap(
input,
(a, b) => a === b || (a?.shadows(b) ?? false),
pickString,
toString
)
if (freeform.inline) {
inputTagsFilter.SetClass("w-48-imp")
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags, state)
inputTagsFilter = new InputElementWrapper(
inputTagsFilter,
configuration.render,
freeform.key,
tags,
state
)
inputTagsFilter.SetClass("block")
}
return inputTagsFilter;
return inputTagsFilter
}
public static CreateTagExplanation(selectedValue: Store<TagsFilter>,
tags: Store<object>,
state?: { osmConnection?: OsmConnection }) {
public static CreateTagExplanation(
selectedValue: Store<TagsFilter>,
tags: Store<object>,
state?: { osmConnection?: OsmConnection }
) {
return new VariableUiElement(
selectedValue.map(
(tagsFilter: TagsFilter) => {
const csCount = state?.osmConnection?.userDetails?.data?.csCount ?? Constants.userJourney.tagsVisibleAndWikiLinked + 1;
const csCount =
state?.osmConnection?.userDetails?.data?.csCount ??
Constants.userJourney.tagsVisibleAndWikiLinked + 1
if (csCount < Constants.userJourney.tagsVisibleAt) {
return "";
return ""
}
if (tagsFilter === undefined) {
return Translations.t.general.noTagsSelected.SetClass("subtle");
return Translations.t.general.noTagsSelected.SetClass("subtle")
}
if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
const tagsStr = tagsFilter.asHumanString(false, true, tags.data);
return new FixedUiElement(tagsStr).SetClass("subtle");
const tagsStr = tagsFilter.asHumanString(false, true, tags.data)
return new FixedUiElement(tagsStr).SetClass("subtle")
}
return tagsFilter.asHumanString(true, true, tags.data);
return tagsFilter.asHumanString(true, true, tags.data)
},
[state?.osmConnection?.userDetails]
)
).SetClass("block break-all")
}
}
}