Add question box as special rendering

This commit is contained in:
Pieter Vander Vennet 2023-03-31 03:28:11 +02:00
parent 15664df63f
commit d47fd7e746
42 changed files with 956 additions and 311 deletions

51
UI/Popup/QuestionViz.ts Normal file
View file

@ -0,0 +1,51 @@
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson"
import BaseUIElement from "../BaseUIElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import SvelteUIElement from "../Base/SvelteUIElement"
import Questionbox from "./TagRendering/Questionbox.svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
/**
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
*/
export default class QuestionViz implements SpecialVisualization {
funcName = "questions"
docs =
"The special element which shows the questions which are unkown. Added by default if not yet there"
args = [
{
name: "labels",
doc: "One or more ';'-separated labels. If these are given, only questions with these labels will be given. Use `unlabeled` for all questions that don't have an explicit label. If none given, all questions will be shown",
},
{
name: "blacklisted-labels",
doc: "One or more ';'-separated labels of questions which should _not_ be included",
},
]
constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement {
const labels = args[0]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
const blacklist = args[1]
?.split(";")
?.map((s) => s.trim())
?.filter((s) => s !== "")
return new SvelteUIElement(Questionbox, {
layer,
tags,
selectedElement: feature,
state,
onlyForLabels: labels,
notForLabels: blacklist,
})
}
}

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource";
import { Translation } from "../../i18n/Translation";
import ValidatedInput from "../../InputElement/ValidatedInput.svelte";
import Tr from "../../Base/Tr.svelte";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import Inline from "./Inline.svelte";
import { createEventDispatcher } from "svelte";
export let value: UIEventSource<string>;
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
let dispatch = createEventDispatcher<{ "selected" }>();
</script>
<Inline key={config.freeform.key} {tags} template={config.render}>
<ValidatedInput {feedback} type={config.freeform.type}
{value} on:selected={() => dispatch("selected")}></ValidatedInput>
</Inline>
{#if $feedback !== undefined}
<div class="alert">
<Tr t={$feedback} />
</div>
{/if}

View file

@ -0,0 +1,26 @@
<script lang="ts">
import { Utils } from "../../../Utils.js";
import { UIEventSource } from "../../../Logic/UIEventSource";
import { onDestroy } from "svelte";
import { Translation } from "../../i18n/Translation";
import Locale from "../../i18n/Locale";
export let template: Translation;
let _template: string
onDestroy(Locale.language.addCallbackAndRunD(l => {
_template = template.textFor(l)
}))
export let key: string;
export let tags: UIEventSource<Record<string, string>>;
let _tags = tags.data;
onDestroy(tags.addCallbackAndRunD(tags => {
_tags = tags;
}));
let [before, after] = _template.split("{" + key + "}");
</script>
<span>
{Utils.SubstituteKeys(before, _tags)}
<slot />
{Utils.SubstituteKeys(after, _tags)}
</span>

View file

@ -0,0 +1,108 @@
<script lang="ts">
/**
* Shows all questions for which the answers are unknown.
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
*/
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
import If from "../../Base/If.svelte";
import { onDestroy } from "svelte";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
export let layer: LayerConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature;
export let state: SpecialVisualizationState;
/**
* If set, only questions for these labels will be shown
*/
export let onlyForLabels: string[] | undefined = undefined;
const _onlyForLabels = new Set(onlyForLabels);
/**
* If set, only questions _not_ having these labels will be shown
*/
export let notForLabels: string[] | undefined = undefined;
const _notForLabels = new Set(notForLabels);
function allowed(labels: string[]) {
if (onlyForLabels?.length > 0 && !labels.some(l => _onlyForLabels.has(l))) {
return false;
}
if (notForLabels?.length > 0 && labels.some(l => _notForLabels.has(l))) {
return false;
}
return true;
}
console.log("Got layer", layer, onlyForLabels, notForLabels);
const baseQuestions = (layer.tagRenderings ?? [])?.filter(tr => allowed(tr.labels) && tr.question !== undefined);
console.log("BaseQuestions are", baseQuestions);
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>());
let answered : number = 0
let questionsToAsk = tags.map(tags => {
const questionsToAsk: TagRenderingConfig[] = [];
for (const baseQuestion of baseQuestions) {
if (skippedQuestions.data.has(baseQuestion.id) > 0) {
continue;
}
if (baseQuestion.condition !== undefined && !baseQuestion.condition.matchesProperties(tags)) {
continue;
}
questionsToAsk.push(baseQuestion);
}
return questionsToAsk;
}, [skippedQuestions]);
let _questionsToAsk: TagRenderingConfig[];
let _firstQuestion: TagRenderingConfig
onDestroy(questionsToAsk.subscribe(qta => {
_questionsToAsk = qta;
_firstQuestion = qta[0]
}));
function skip(question: TagRenderingConfig, didAnswer: boolean = false) {
skippedQuestions.data.add(question.id);
skippedQuestions.ping();
if(didAnswer ){
answered ++
}
}
</script>
{#if _questionsToAsk.length === 0}
All done! You answered {answered} questions and skipped {$skippedQuestions.size} questions.
{#if $skippedQuestions.size > 0 }
<button on:click={() => skippedQuestions.setData(new Set())}>Re-activate skipped questions</button>
{/if}
{:else }
<div>
<If condition={state.userRelatedState.showAllQuestionsAtOnce}>
<div>
{#each _questionsToAsk as question (question.id)}
<TagRenderingQuestion config={question} {tags} {selectedElement} {state} {layer}></TagRenderingQuestion>
{/each}
</div>
<div slot="else">
<TagRenderingQuestion
config={_firstQuestion} {layer} {selectedElement} {state} {tags}
on:saved={() => {skip(_firstQuestion, true)}}>
<button on:click={() => {skip(_firstQuestion)} }
slot="cancel">
<Tr t={Translations.t.general.skip}></Tr>
</button>
</TagRenderingQuestion>
</div>
</If>
</div>
{/if}

View file

@ -1,14 +1,15 @@
<script lang="ts">
import { Translation } from "../i18n/Translation";
import SpecialVisualizations from "../SpecialVisualizations";
import { Translation } from "../../i18n/Translation";
import SpecialVisualizations from "../../SpecialVisualizations";
import { onDestroy } from "svelte";
import Locale from "../i18n/Locale";
import type { RenderingSpecification, SpecialVisualizationState } from "../SpecialVisualization";
import { Utils } from "../../Utils.js";
import Locale from "../../i18n/Locale";
import type { RenderingSpecification, SpecialVisualizationState } from "../../SpecialVisualization";
import { Utils } from "../../../Utils.js";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource.js";
import ToSvelte from "../Base/ToSvelte.svelte";
import FromHtml from "../Base/FromHtml.svelte";
import { UIEventSource } from "../../../Logic/UIEventSource.js";
import ToSvelte from "../../Base/ToSvelte.svelte";
import FromHtml from "../../Base/FromHtml.svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
/**
* The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well
@ -17,18 +18,18 @@
export let state: SpecialVisualizationState;
export let tags: UIEventSource<Record<string, string>>;
export let feature: Feature;
export let layer: LayerConfig
let txt: string;
onDestroy(Locale.language.addCallbackAndRunD(l => {
txt = t.textFor(l);
}));
let specs: RenderingSpecification[];
specs = SpecialVisualizations.constructSpecification(txt);
let specs: RenderingSpecification[] = SpecialVisualizations.constructSpecification(txt);
</script>
{#each specs as specpart}
{#if typeof specpart === "string"}
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
{:else if $tags !== undefined }
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature)}></ToSvelte>
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte>
{/if}
{/each}

View file

@ -0,0 +1,38 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { Utils } from "../../../Utils";
import { Translation } from "../../i18n/Translation";
import TagRenderingMapping from "./TagRenderingMapping.svelte";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import type { Feature } from "geojson";
import { UIEventSource } from "../../../Logic/UIEventSource";
import { onDestroy } from "svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export let tags: UIEventSource<Record<string, string> | undefined>;
let _tags: Record<string, string>;
onDestroy(tags.addCallbackAndRun(tags => {
_tags = tags;
}));
export let state: SpecialVisualizationState;
export let selectedElement: Feature;
export let config: TagRenderingConfig;
export let layer: LayerConfig
let trs: { then: Translation; icon?: string; iconClass?: string }[];
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))}
{#if trs.length === 1}
<TagRenderingMapping mapping={trs[0]} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
{/if}
{#if trs.length > 1}
<ul>
{#each trs as mapping}
<li>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
</li>
{/each}
</ul>
{/if}
{/if}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { Feature } from "geojson";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid";
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
import { onDestroy } from "svelte";
import Tr from "../../Base/Tr.svelte";
import Translations from "../../i18n/Translations.js";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature;
export let state: SpecialVisualizationState;
export let layer: LayerConfig
export let showQuestionIfUnknown : boolean= false
let editMode = false
onDestroy(tags.addCallbackAndRunD(tags => {
editMode = showQuestionIfUnknown && !config.IsKnown(tags)
}))
</script>
{#if config.question}
{#if editMode}
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer} >
<button slot="cancel" on:click={() => {editMode = false}}>
<Tr t={Translations.t.general.cancel}/>
</button>
</TagRenderingQuestion>
{:else}
<div class="flex justify-between">
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
<button on:click={() => {editMode = true}} class="w-6 h-6 rounded-full subtle-background p-1">
<PencilAltIcon></PencilAltIcon>
</button>
</div>
{/if}
{:else }
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
{/if}

View file

@ -1,13 +1,15 @@
<script lang="ts">
import { Translation } from "../i18n/Translation";
import { Translation } from "../../i18n/Translation";
import SpecialTranslation from "./SpecialTranslation.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource";
import { UIEventSource } from "../../../Logic/UIEventSource";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export let selectedElement: Feature
export let tags: UIEventSource<Record<string, string>>;
export let state: SpecialVisualizationState
export let layer: LayerConfig
export let mapping: {
then: Translation; icon?: string; iconClass?: | "small"
| "medium"
@ -22,11 +24,11 @@
</script>
{#if mapping.icon !== undefined}
<div class="flex">
<div class="inline-flex">
<img class={iconclass+" mr-1"} src={mapping.icon}>
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
</div>
{:else if mapping.then !== undefined}
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
{/if}

View file

@ -0,0 +1,147 @@
<script lang="ts">
import { UIEventSource } from "../../../Logic/UIEventSource";
import type { SpecialVisualizationState } from "../../SpecialVisualization";
import Tr from "../../Base/Tr.svelte";
import If from "../../Base/If.svelte";
import TagRenderingMapping from "./TagRenderingMapping.svelte";
import type { Feature } from "geojson";
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
import FreeformInput from "./FreeformInput.svelte";
import Translations from "../../i18n/Translations.js";
import FromHtml from "../../Base/FromHtml.svelte";
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
import { createEventDispatcher } from "svelte";
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature;
export let state: SpecialVisualizationState;
export let layer: LayerConfig;
// Will be bound if a freeform is available
let freeformInput = new UIEventSource<string>(undefined);
let selectedMapping: number = 0;
let checkedMappings: boolean[];
if (config.mappings?.length > 0) {
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
}
let selectedTags: TagsFilter = undefined;
$:selectedTags = config?.constructChangeSpecification($freeformInput, selectedMapping, checkedMappings);
function mappingIsHidden(mapping: Mapping): boolean {
if (mapping.hideInAnswer === undefined || mapping.hideInAnswer === false) {
return false;
}
if (mapping.hideInAnswer === true) {
return true;
}
return (<TagsFilter>mapping.hideInAnswer).matchesProperties(tags.data);
}
let dispatch = createEventDispatcher<{
"saved": {
config: TagRenderingConfig,
applied: TagsFilter
}
}>();
function onSave() {
dispatch("saved", { config, applied: selectedTags });
const change = new ChangeTagAction(
tags.data.id,
selectedTags,
tags.data,
{
theme: state.layout.id,
changeType: "answer"
}
);
change.CreateChangeDescriptions().then(changes =>
state.changes.applyChanges(changes)
).catch(console.error);
}
</script>
{#if config.question !== undefined}
<div class="border border-black subtle-background flex flex-col">
<If condition={state.featureSwitchIsTesting}>
<div class="flex justify-between">
<Tr t={config.question}></Tr>
<span class="alert">{config.id}</span>
</div>
<Tr slot="else" t={config.question}></Tr>
</If>
{#if config.questionhint}
<div class="subtle">
<Tr t={config.questionHint}></Tr>
</div>
{/if}
{#if config.freeform?.key && !(config.mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput {config} {tags} value={freeformInput} />
{/if}
{#if config.mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
{#if !mappingIsHidden(mapping) }
<label>
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} value={i}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
</label>
{/if}
{/each}
{#if config.freeform?.key}
<label>
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
value={config.mappings.length}>
<FreeformInput {config} {tags} value={freeformInput}
on:selected={() => selectedMapping = config.mappings.length } />
</label>
{/if}
</div>
{/if}
{#if config.mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
{#if !mappingIsHidden(mapping) }
<label>
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i} bind:checked={checkedMappings[i]}>
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
</label>
{/if}
{/each}
{#if config.freeform?.key}
<label>
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length}
bind:checked={checkedMappings[config.mappings.length]}>
<FreeformInput {config} {tags} value={freeformInput}
on:selected={() => checkedMappings[config.mappings.length] = true} />
</label>
{/if}
</div>
{/if}
<FromHtml src={selectedTags?.asHumanString(true, true, {})} />
<div>
<!-- TagRenderingQuestion-buttons -->
<slot name="cancel"></slot>
<button on:click={onSave}>
<Tr t={Translations.t.general.save}></Tr>
</button>
</div>
</div>
{/if}

View file

@ -1,34 +0,0 @@
<script lang="ts">
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
import { Utils } from "../../Utils";
import { Translation } from "../i18n/Translation";
import TagRenderingMapping from "./TagRenderingMapping.svelte";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import type { Feature } from "geojson";
import { UIEventSource } from "../../Logic/UIEventSource";
import { onDestroy } from "svelte";
export let tags: UIEventSource<Record<string, string> | undefined>;
let _tags : Record<string, string>
onDestroy(tags.addCallbackAndRun(tags => {
_tags = tags
}))
export let state: SpecialVisualizationState
export let selectedElement: Feature
export let config: TagRenderingConfig;
let trs: { then: Translation; icon?: string; iconClass?: string }[];
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
</script>
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))}
<div>
{#if trs.length === 1}
<TagRenderingMapping mapping={trs[0]} {tags} {state} feature={selectedElement}></TagRenderingMapping>
{/if}
{#if trs.length > 1}
{#each trs as mapping}
<TagRenderingMapping mapping={trs} {tags} {state} feature=""{selectedElement}></TagRenderingMapping>
{/each}
{/if}
</div>
{/if}

View file

@ -1,53 +0,0 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource";
import type { SpecialVisualizationState } from "../SpecialVisualization";
import Tr from "../Base/Tr.svelte";
import If from "../Base/If.svelte";
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
import TagRenderingMapping from "./TagRenderingMapping.svelte";
import type { Feature } from "geojson";
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
export let config: TagRenderingConfig;
export let tags: UIEventSource<Record<string, string>>;
export let selectedElement: Feature;
export let state: SpecialVisualizationState;
state.featureSwitchIsTesting;
let freeformInput = new UIEventSource<string>(undefined);
</script>
{#if config.question !== undefined}
<div class="border border-black subtle-background">
<If condition={state.featureSwitchIsTesting}>
<div class="flex justify-between">
<Tr t={config.question}></Tr>
{config.id}
</div>
<Tr slot="else" t={config.question}></Tr>
</If>
{#if config.questionhint}
<div class="subtle">
<Tr t={config.question}></Tr>
</div>
{/if}
{#if config.freeform?.key && !(config.mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<ValidatedInput type={config.freeform.type} value={freeformInput}></ValidatedInput>
{/if}
{#if config.mappings !== undefined}
<div class="flex flex-col">
{#each config.mappings as mapping}
{#if mapping.hideInAnswer === true || !(mapping.hideInAnswer) || (console.log(tags) || true) || !(mapping.hideInAnswer?.matchesProperties($tags)) }
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
{/if}
{/each}
</div>
{/if}
</div>
{/if}