forked from MapComplete/MapComplete
Add support for units to clean up tags when they enter mapcomplete; add example of this usage in the climbing theme, add climbing theme title icons with length and needed number of carabiners
This commit is contained in:
parent
89f6f606c8
commit
966fcda8d1
20 changed files with 302 additions and 111 deletions
|
@ -3,30 +3,48 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
|||
import Combine from "../Base/Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class CombinedInputElement<T> extends InputElement<T> {
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._combined.ConstructElement();
|
||||
}
|
||||
private readonly _a: InputElement<T>;
|
||||
private readonly _b: BaseUIElement;
|
||||
private readonly _combined: BaseUIElement;
|
||||
export default class CombinedInputElement<T, J, X> extends InputElement<X> {
|
||||
|
||||
public readonly IsSelected: UIEventSource<boolean>;
|
||||
constructor(a: InputElement<T>, b: InputElement<T>) {
|
||||
private readonly _a: InputElement<T>;
|
||||
private readonly _b: InputElement<J>;
|
||||
private readonly _combined: BaseUIElement;
|
||||
private readonly _value: UIEventSource<X>
|
||||
private readonly _split: (x: X) => [T, J];
|
||||
|
||||
constructor(a: InputElement<T>, b: InputElement<J>,
|
||||
combine: (t: T, j: J) => X,
|
||||
split: (x: X) => [T, J]) {
|
||||
super();
|
||||
this._a = a;
|
||||
this._b = b;
|
||||
this._split = split;
|
||||
this.IsSelected = this._a.IsSelected.map((isSelected) => {
|
||||
return isSelected || b.IsSelected.data
|
||||
}, [b.IsSelected])
|
||||
this._combined = new Combine([this._a, this._b]);
|
||||
this._value = this._a.GetValue().map(
|
||||
t => combine(t, this._b.GetValue().data),
|
||||
[this._b.GetValue()],
|
||||
)
|
||||
.addCallback(x => {
|
||||
const [t, j] = split(x)
|
||||
this._a.GetValue().setData(t)
|
||||
this._b.GetValue().setData(j)
|
||||
})
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<T> {
|
||||
return this._a.GetValue();
|
||||
GetValue(): UIEventSource<X> {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
IsValid(t: T): boolean {
|
||||
return this._a.IsValid(t);
|
||||
IsValid(x: X): boolean {
|
||||
const [t, j] = this._split(x)
|
||||
return this._a.IsValid(t) && this._b.IsValid(j);
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
return this._combined.ConstructElement();
|
||||
}
|
||||
|
||||
}
|
|
@ -270,7 +270,10 @@ export default class ValidatedTextField {
|
|||
if (tp.inputHelper) {
|
||||
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(), {
|
||||
location: options.location
|
||||
}));
|
||||
}),
|
||||
(a, b) => a, // We can ignore b, as they are linked earlier
|
||||
a => [a, a]
|
||||
);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
|
|
@ -8,11 +8,13 @@ import State from "../../State";
|
|||
import Svg from "../../Svg";
|
||||
import Toggle from "../Input/Toggle";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
|
||||
export default class EditableTagRendering extends Toggle {
|
||||
|
||||
constructor(tags: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
units: Unit [],
|
||||
editMode = new UIEventSource<boolean>(false)
|
||||
) {
|
||||
const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration)
|
||||
|
@ -41,7 +43,7 @@ export default class EditableTagRendering extends Toggle {
|
|||
editMode.setData(false)
|
||||
});
|
||||
|
||||
const question = new TagRenderingQuestion(tags, configuration,
|
||||
const question = new TagRenderingQuestion(tags, configuration,units,
|
||||
() => {
|
||||
editMode.setData(false)
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
|
||||
public constructor(
|
||||
tags: UIEventSource<any>,
|
||||
layerConfig: LayerConfig
|
||||
layerConfig: LayerConfig,
|
||||
) {
|
||||
super(() => FeatureInfoBox.GenerateTitleBar(tags, layerConfig),
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
|
||||
|
@ -35,7 +35,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
.SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2");
|
||||
const titleIcons = new Combine(
|
||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon,
|
||||
"block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem !important;")
|
||||
"block w-8 h-8 align-baseline box-content sm:p-0.5")
|
||||
))
|
||||
.SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||
|
||||
|
@ -49,7 +49,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
let questionBox: UIElement = undefined;
|
||||
|
||||
if (State.state.featureSwitchUserbadge.data) {
|
||||
questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
|
||||
questionBox = new QuestionBox(tags, layerConfig.tagRenderings, layerConfig.units);
|
||||
}
|
||||
|
||||
let questionBoxIsUsed = false;
|
||||
|
@ -59,7 +59,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
|||
questionBoxIsUsed = true;
|
||||
return questionBox;
|
||||
}
|
||||
return new EditableTagRendering(tags, tr);
|
||||
return new EditableTagRendering(tags, tr, layerConfig.units);
|
||||
});
|
||||
if (!questionBoxIsUsed) {
|
||||
renderings.push(questionBox);
|
||||
|
|
|
@ -5,6 +5,8 @@ import TagRenderingQuestion from "./TagRenderingQuestion";
|
|||
import Translations from "../i18n/Translations";
|
||||
import State from "../../State";
|
||||
import Combine from "../Base/Combine";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -14,12 +16,12 @@ export default class QuestionBox extends UIElement {
|
|||
private readonly _tags: UIEventSource<any>;
|
||||
|
||||
private readonly _tagRenderings: TagRenderingConfig[];
|
||||
private _tagRenderingQuestions: UIElement[];
|
||||
private _tagRenderingQuestions: BaseUIElement[];
|
||||
|
||||
private _skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||
private _skippedQuestionsButton: UIElement;
|
||||
private _skippedQuestionsButton: BaseUIElement;
|
||||
|
||||
constructor(tags: UIEventSource<any>, tagRenderings: TagRenderingConfig[]) {
|
||||
constructor(tags: UIEventSource<any>, tagRenderings: TagRenderingConfig[], units: Unit[]) {
|
||||
super(tags);
|
||||
this.ListenTo(this._skippedQuestions);
|
||||
this._tags = tags;
|
||||
|
@ -28,7 +30,7 @@ export default class QuestionBox extends UIElement {
|
|||
.filter(tr => tr.question !== undefined)
|
||||
.filter(tr => tr.question !== null);
|
||||
this._tagRenderingQuestions = this._tagRenderings
|
||||
.map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering,
|
||||
.map((tagRendering, i) => new TagRenderingQuestion(this._tags, tagRendering,units,
|
||||
() => {
|
||||
// We save
|
||||
self._skippedQuestions.ping();
|
||||
|
@ -49,7 +51,7 @@ export default class QuestionBox extends UIElement {
|
|||
}
|
||||
|
||||
InnerRender() {
|
||||
const allQuestions : UIElement[] = []
|
||||
const allQuestions : BaseUIElement[] = []
|
||||
for (let i = 0; i < this._tagRenderingQuestions.length; i++) {
|
||||
let tagRendering = this._tagRenderings[i];
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import {And} from "../../Logic/Tags/And";
|
|||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import {DropDown} from "../Input/DropDown";
|
||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||
import CombinedInputElement from "../Input/CombinedInputElement";
|
||||
|
||||
/**
|
||||
* Shows the question element.
|
||||
|
@ -38,14 +40,17 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
private _inputElement: InputElement<TagsFilter>;
|
||||
private _cancelButton: BaseUIElement;
|
||||
private _appliedTags: BaseUIElement;
|
||||
private readonly _applicableUnit: Unit;
|
||||
private _question: BaseUIElement;
|
||||
|
||||
constructor(tags: UIEventSource<any>,
|
||||
configuration: TagRenderingConfig,
|
||||
units: Unit[],
|
||||
afterSave?: () => void,
|
||||
cancelButton?: BaseUIElement
|
||||
) {
|
||||
super(tags);
|
||||
this._applicableUnit = units.filter(unit => unit.isApplicableToKey(configuration.freeform?.key))[0];
|
||||
this._tags = tags;
|
||||
this._configuration = configuration;
|
||||
this._cancelButton = cancelButton;
|
||||
|
@ -114,9 +119,9 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
const self = this;
|
||||
let inputEls: InputElement<TagsFilter>[];
|
||||
|
||||
const mappings = (this._configuration.mappings??[])
|
||||
.filter( mapping => {
|
||||
if(mapping.hideInAnswer === true){
|
||||
const mappings = (this._configuration.mappings ?? [])
|
||||
.filter(mapping => {
|
||||
if (mapping.hideInAnswer === true) {
|
||||
return false;
|
||||
}
|
||||
if (typeof (mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matchesProperties(this._tags.data)) {
|
||||
|
@ -124,9 +129,9 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
|
||||
let allIfNots: TagsFilter[] = Utils.NoNull(this._configuration.mappings?.map(m => m.ifnot) ?? [] );
|
||||
|
||||
|
||||
let allIfNots: TagsFilter[] = Utils.NoNull(this._configuration.mappings?.map(m => m.ifnot) ?? []);
|
||||
const ff = this.GenerateFreeform();
|
||||
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
||||
|
||||
|
@ -272,7 +277,7 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
then: Translation,
|
||||
hideInAnswer: boolean | TagsFilter
|
||||
}, ifNot?: TagsFilter[]): InputElement<TagsFilter> {
|
||||
|
||||
|
||||
let tagging = mapping.if;
|
||||
if (ifNot.length > 0) {
|
||||
tagging = new And([tagging, ...ifNot])
|
||||
|
@ -323,16 +328,41 @@ export default class TagRenderingQuestion extends UIElement {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
|
||||
let input: InputElement<string> = ValidatedTextField.InputForType(this._configuration.freeform.type, {
|
||||
isValid: (str) => (str.length <= 255),
|
||||
country: () => this._tags.data._country,
|
||||
location: [this._tags.data._lat, this._tags.data._lon]
|
||||
});
|
||||
|
||||
textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]);
|
||||
if (this._applicableUnit) {
|
||||
// We need to apply a unit.
|
||||
// This implies:
|
||||
// We have to create a dropdown with applicable denominations, and fuse those values
|
||||
const unit = this._applicableUnit
|
||||
const unitDropDown = new DropDown("",
|
||||
unit.denominations.map(denom => {
|
||||
return {
|
||||
shown: denom.human,
|
||||
value: denom
|
||||
}
|
||||
})
|
||||
)
|
||||
unitDropDown.GetValue().setData(this._applicableUnit.defaultDenom)
|
||||
unitDropDown.SetStyle("width: min-content")
|
||||
|
||||
input = new CombinedInputElement(
|
||||
input,
|
||||
unitDropDown,
|
||||
(text, denom) => denom?.canonicalValue(text, true) ?? text,
|
||||
(valueWithDenom: string) => unit.findDenomination(valueWithDenom)
|
||||
).SetClass("flex")
|
||||
}
|
||||
|
||||
|
||||
input.GetValue().setData(this._tags.data[this._configuration.freeform.key]);
|
||||
|
||||
return new InputElementMap(
|
||||
textField, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
||||
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
||||
pickString, toString
|
||||
);
|
||||
|
||||
|
|
|
@ -33,23 +33,24 @@ export default class SpecialVisualizations {
|
|||
args: { name: string, defaultValue?: string, doc: string }[]
|
||||
}[] =
|
||||
|
||||
[{
|
||||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: ((state: State, tags: UIEventSource<any>) => {
|
||||
return new VariableUiElement(tags.map(tags => {
|
||||
const parts = [];
|
||||
for (const key in tags) {
|
||||
if (!tags.hasOwnProperty(key)) {
|
||||
continue;
|
||||
[
|
||||
{
|
||||
funcName: "all_tags",
|
||||
docs: "Prints all key-value pairs of the object - used for debugging",
|
||||
args: [],
|
||||
constr: ((state: State, tags: UIEventSource<any>) => {
|
||||
return new VariableUiElement(tags.map(tags => {
|
||||
const parts = [];
|
||||
for (const key in tags) {
|
||||
if (!tags.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
parts.push(key + "=" + tags[key]);
|
||||
}
|
||||
parts.push(key + "=" + tags[key]);
|
||||
}
|
||||
return parts.join("<br/>")
|
||||
})).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;")
|
||||
})
|
||||
},
|
||||
return parts.join("<br/>")
|
||||
})).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;")
|
||||
})
|
||||
},
|
||||
|
||||
{
|
||||
funcName: "image_carousel",
|
||||
|
@ -252,13 +253,40 @@ export default class SpecialVisualizations {
|
|||
}
|
||||
}
|
||||
|
||||
return new ShareButton(Svg.share_ui(), generateShareData)
|
||||
return new ShareButton(Svg.share_svg().SetClass("w-8 h-8"), generateShareData)
|
||||
} else {
|
||||
return new FixedUiElement("")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
{funcName: "canonical",
|
||||
docs: "Converts a short, canonical value into the long, translated text",
|
||||
example: "{canonical(length)} will give 42 metre (in french)",
|
||||
args:[{
|
||||
name:"key",
|
||||
doc: "The key of the tag to give the canonical text for"
|
||||
}],
|
||||
constr: (state, tagSource, args) => {
|
||||
const key = args [0]
|
||||
return new VariableUiElement(
|
||||
tagSource.map(tags => tags[key]).map(value => {
|
||||
if(value === undefined){
|
||||
return undefined
|
||||
}
|
||||
const unit = state.layoutToUse.data.units.filter(unit => unit.isApplicableToKey(key))[0]
|
||||
if(unit === undefined){
|
||||
return value;
|
||||
}
|
||||
|
||||
return unit.asHumanLongValue(value);
|
||||
|
||||
},
|
||||
[ state.layoutToUse])
|
||||
|
||||
|
||||
)
|
||||
}}
|
||||
|
||||
]
|
||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue