Add theme for 'notes'
This commit is contained in:
parent
677a07e3d2
commit
a58ce564c2
20 changed files with 678 additions and 314 deletions
|
@ -4,6 +4,7 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource";
|
||||||
import Hash from "../../Web/Hash";
|
import Hash from "../../Web/Hash";
|
||||||
import {BBox} from "../../BBox";
|
import {BBox} from "../../BBox";
|
||||||
import {ElementStorage} from "../../ElementStorage";
|
import {ElementStorage} from "../../ElementStorage";
|
||||||
|
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||||
|
|
||||||
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
|
export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled {
|
||||||
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
public features: UIEventSource<{ feature: any; freshness: Date }[]> =
|
||||||
|
@ -87,10 +88,10 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagsFilter = layer.appliedFilters.data;
|
const tagsFilter = Array.from(layer.appliedFilters.data.values());
|
||||||
for (const filter of tagsFilter ?? []) {
|
for (const filter of tagsFilter ?? []) {
|
||||||
const neededTags = filter.filter.options[filter.selected].osmTags
|
const neededTags : TagsFilter = filter?.currentFilter
|
||||||
if (!neededTags.matchesProperties(f.feature.properties)) {
|
if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) {
|
||||||
// Hidden by the filter on the layer itself - we want to hide it no matter wat
|
// Hidden by the filter on the layer itself - we want to hide it no matter wat
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,13 +218,58 @@ export class OsmConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public closeNote(id: number | string): Promise<any> {
|
public closeNote(id: number | string, text?: string): Promise<any> {
|
||||||
|
let textSuffix = ""
|
||||||
|
if((text ?? "") !== "" ){
|
||||||
|
textSuffix = "?text="+encodeURIComponent(text)
|
||||||
|
}
|
||||||
return new Promise((ok, error) => {
|
return new Promise((ok, error) => {
|
||||||
this.auth.xhr({
|
this.auth.xhr({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `/api/0.6/notes/${id}/close`
|
path: `/api/0.6/notes/${id}/close${textSuffix}`
|
||||||
|
}, function (err, response) {
|
||||||
|
if (err !== null) {
|
||||||
|
error(err)
|
||||||
|
} else {
|
||||||
|
ok()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public reopenNote(id: number | string, text?: string): Promise<any> {
|
||||||
|
let textSuffix = ""
|
||||||
|
if((text ?? "") !== "" ){
|
||||||
|
textSuffix = "?text="+encodeURIComponent(text)
|
||||||
|
}
|
||||||
|
return new Promise((ok, error) => {
|
||||||
|
this.auth.xhr({
|
||||||
|
method: 'POST',
|
||||||
|
path: `/api/0.6/notes/${id}/reopen${textSuffix}`
|
||||||
|
}, function (err, response) {
|
||||||
|
if (err !== null) {
|
||||||
|
error(err)
|
||||||
|
} else {
|
||||||
|
ok()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public addCommentToNode(id: number | string, text: string): Promise<any> {
|
||||||
|
if ((text ?? "") === "") {
|
||||||
|
throw "Invalid text!"
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((ok, error) => {
|
||||||
|
this.auth.xhr({
|
||||||
|
method: 'POST',
|
||||||
|
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
|
||||||
}, function (err, response) {
|
}, function (err, response) {
|
||||||
console.log("Closing note gave:", err, response)
|
|
||||||
if (err !== null) {
|
if (err !== null) {
|
||||||
error(err)
|
error(err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,11 +7,10 @@ import Attribution from "../../UI/BigComponents/Attribution";
|
||||||
import Minimap, {MinimapObj} from "../../UI/Base/Minimap";
|
import Minimap, {MinimapObj} from "../../UI/Base/Minimap";
|
||||||
import {Tiles} from "../../Models/TileRange";
|
import {Tiles} from "../../Models/TileRange";
|
||||||
import BaseUIElement from "../../UI/BaseUIElement";
|
import BaseUIElement from "../../UI/BaseUIElement";
|
||||||
import FilteredLayer from "../../Models/FilteredLayer";
|
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||||
import {QueryParameters} from "../Web/QueryParameters";
|
import {QueryParameters} from "../Web/QueryParameters";
|
||||||
import * as personal from "../../assets/themes/personal/personal.json";
|
import * as personal from "../../assets/themes/personal/personal.json";
|
||||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
|
||||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
|
||||||
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
|
import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource";
|
||||||
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
|
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource";
|
||||||
|
@ -339,7 +338,6 @@ export default class MapState extends UserRelatedState {
|
||||||
private InitializeFilteredLayers() {
|
private InitializeFilteredLayers() {
|
||||||
|
|
||||||
const layoutToUse = this.layoutToUse;
|
const layoutToUse = this.layoutToUse;
|
||||||
const empty = []
|
|
||||||
const flayers: FilteredLayer[] = [];
|
const flayers: FilteredLayer[] = [];
|
||||||
for (const layer of layoutToUse.layers) {
|
for (const layer of layoutToUse.layers) {
|
||||||
let isDisplayed: UIEventSource<boolean>
|
let isDisplayed: UIEventSource<boolean>
|
||||||
|
@ -355,26 +353,18 @@ export default class MapState extends UserRelatedState {
|
||||||
"Wether or not layer " + layer.id + " is shown"
|
"Wether or not layer " + layer.id + " is shown"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const flayer = {
|
const flayer : FilteredLayer = {
|
||||||
isDisplayed: isDisplayed,
|
isDisplayed: isDisplayed,
|
||||||
layerDef: layer,
|
layerDef: layer,
|
||||||
appliedFilters: new UIEventSource<{ filter: FilterConfig, selected: number }[]>([]),
|
appliedFilters: new UIEventSource<Map<string, FilterState>>(new Map<string, FilterState>())
|
||||||
};
|
};
|
||||||
|
layer.filters.forEach(filterConfig => {
|
||||||
|
const stateSrc = filterConfig.initState()
|
||||||
|
|
||||||
if (layer.filters.length > 0) {
|
stateSrc .addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state))
|
||||||
const filtersPerName = new Map<string, FilterConfig>()
|
flayer.appliedFilters.map(dict => dict.get(filterConfig.id))
|
||||||
layer.filters.forEach(f => filtersPerName.set(f.id, f))
|
.addCallback(state => stateSrc.setData(state))
|
||||||
const qp = QueryParameters.GetQueryParameter("filter-" + layer.id, "", "Filtering state for a layer")
|
})
|
||||||
flayer.appliedFilters.map(filters => (filters ?? []).map(f => f.filter.id + "." + f.selected).join(","), [], textual => {
|
|
||||||
if (textual.length === 0) {
|
|
||||||
return empty
|
|
||||||
}
|
|
||||||
return textual.split(",").map(part => {
|
|
||||||
const [filterId, selected] = part.split(".");
|
|
||||||
return {filter: filtersPerName.get(filterId), selected: Number(selected)}
|
|
||||||
}).filter(f => f.filter !== undefined && !isNaN(f.selected))
|
|
||||||
}).syncWith(qp, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
flayers.push(flayer);
|
flayers.push(flayer);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
import LayerConfig from "./ThemeConfig/LayerConfig";
|
import LayerConfig from "./ThemeConfig/LayerConfig";
|
||||||
import FilterConfig from "./ThemeConfig/FilterConfig";
|
import {TagsFilter} from "../Logic/Tags/TagsFilter";
|
||||||
|
|
||||||
|
export interface FilterState {
|
||||||
|
currentFilter: TagsFilter, state: string | number
|
||||||
|
}
|
||||||
|
|
||||||
export default interface FilteredLayer {
|
export default interface FilteredLayer {
|
||||||
readonly isDisplayed: UIEventSource<boolean>;
|
readonly isDisplayed: UIEventSource<boolean>;
|
||||||
readonly appliedFilters: UIEventSource<{ filter: FilterConfig, selected: number }[]>;
|
readonly appliedFilters: UIEventSource<Map<string, FilterState>>;
|
||||||
readonly layerDef: LayerConfig;
|
readonly layerDef: LayerConfig;
|
||||||
}
|
}
|
|
@ -4,15 +4,17 @@ import FilterConfigJson from "./Json/FilterConfigJson";
|
||||||
import Translations from "../../UI/i18n/Translations";
|
import Translations from "../../UI/i18n/Translations";
|
||||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
||||||
import {Utils} from "../../Utils";
|
|
||||||
import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson";
|
|
||||||
import {AndOrTagConfigJson} from "./Json/TagConfigJson";
|
import {AndOrTagConfigJson} from "./Json/TagConfigJson";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import {FilterState} from "../FilteredLayer";
|
||||||
|
import {QueryParameters} from "../../Logic/Web/QueryParameters";
|
||||||
|
import {Utils} from "../../Utils";
|
||||||
|
|
||||||
export default class FilterConfig {
|
export default class FilterConfig {
|
||||||
public readonly id: string
|
public readonly id: string
|
||||||
public readonly options: {
|
public readonly options: {
|
||||||
question: Translation;
|
question: Translation;
|
||||||
osmTags: TagsFilter;
|
osmTags: TagsFilter | undefined;
|
||||||
originalTagsSpec: string | AndOrTagConfigJson
|
originalTagsSpec: string | AndOrTagConfigJson
|
||||||
fields: { name: string, type: string }[]
|
fields: { name: string, type: string }[]
|
||||||
}[];
|
}[];
|
||||||
|
@ -39,11 +41,14 @@ export default class FilterConfig {
|
||||||
option.question,
|
option.question,
|
||||||
`${ctx}.question`
|
`${ctx}.question`
|
||||||
);
|
);
|
||||||
let osmTags = TagUtils.Tag(
|
let osmTags = undefined;
|
||||||
option.osmTags ?? {and: []},
|
if (option.osmTags !== undefined) {
|
||||||
|
osmTags = TagUtils.Tag(
|
||||||
|
option.osmTags,
|
||||||
`${ctx}.osmTags`
|
`${ctx}.osmTags`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
}
|
||||||
if (question === undefined) {
|
if (question === undefined) {
|
||||||
throw `Invalid filter: no question given at ${ctx}`
|
throw `Invalid filter: no question given at ${ctx}`
|
||||||
}
|
}
|
||||||
|
@ -62,9 +67,9 @@ export default class FilterConfig {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if(fields.length > 0){
|
if (fields.length > 0) {
|
||||||
// erase the tags, they aren't needed
|
// erase the tags, they aren't needed
|
||||||
osmTags = TagUtils.Tag({and:[]})
|
osmTags = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
|
return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags};
|
||||||
|
@ -74,9 +79,87 @@ export default class FilterConfig {
|
||||||
throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.`
|
throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.length > 1 && this.options[0].osmTags["and"]?.length !== 0) {
|
if (this.options.length > 1 && this.options[0].osmTags !== undefined) {
|
||||||
throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters"
|
throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public initState(): UIEventSource<FilterState> {
|
||||||
|
|
||||||
|
function reset(state: FilterState): string {
|
||||||
|
if (state === undefined) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "" + state.state
|
||||||
|
}
|
||||||
|
const defaultValue = this.options.length > 1 ? "0" : ""
|
||||||
|
const qp = QueryParameters.GetQueryParameter("filter-" + this.id, defaultValue, "State of filter " + this.id)
|
||||||
|
|
||||||
|
if (this.options.length > 1) {
|
||||||
|
// This is a multi-option filter; state should be a number which selects the correct entry
|
||||||
|
const possibleStates: FilterState [] = this.options.map((opt, i) => ({
|
||||||
|
currentFilter: opt.osmTags,
|
||||||
|
state: i
|
||||||
|
}))
|
||||||
|
|
||||||
|
// We map the query parameter for this case
|
||||||
|
return qp.map(str => {
|
||||||
|
const parsed = Number(str)
|
||||||
|
if (isNaN(parsed)) {
|
||||||
|
// Nope, not a correct number!
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return possibleStates[parsed]
|
||||||
|
}, [], reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const option = this.options[0]
|
||||||
|
|
||||||
|
if (option.fields.length > 0) {
|
||||||
|
return qp.map(str => {
|
||||||
|
// There are variables in play!
|
||||||
|
// str should encode a json-hash
|
||||||
|
try {
|
||||||
|
const props = JSON.parse(str)
|
||||||
|
|
||||||
|
const origTags = option.originalTagsSpec
|
||||||
|
const rewrittenTags = Utils.WalkJson(origTags,
|
||||||
|
v => {
|
||||||
|
if (typeof v !== "string") {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
for (const key in props) {
|
||||||
|
v = (<string>v).replace("{"+key+"}", props[key])
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return <FilterState>{
|
||||||
|
currentFilter: TagUtils.Tag(rewrittenTags),
|
||||||
|
state: str
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [], reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last case is pretty boring: it is checked or it isn't
|
||||||
|
const filterState: FilterState = {
|
||||||
|
currentFilter: option.osmTags,
|
||||||
|
state: "true"
|
||||||
|
}
|
||||||
|
return qp.map(
|
||||||
|
str => {
|
||||||
|
// Only a single option exists here
|
||||||
|
if (str === "true") {
|
||||||
|
return filterState
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}, [],
|
||||||
|
reset
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -29,7 +29,7 @@ abstract class Conversion<TIn, TOut> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
|
public static strict<T>(fixed: { errors: string[], warnings: string[], result?: T }): T {
|
||||||
if (fixed.errors?.length > 0) {
|
if (fixed?.errors?.length > 0) {
|
||||||
throw fixed.errors.join("\n");
|
throw fixed.errors.join("\n");
|
||||||
}
|
}
|
||||||
fixed.warnings?.forEach(w => console.warn(w))
|
fixed.warnings?.forEach(w => console.warn(w))
|
||||||
|
|
|
@ -10,13 +10,14 @@ import Svg from "../../Svg";
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import FilteredLayer from "../../Models/FilteredLayer";
|
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||||
import BackgroundSelector from "./BackgroundSelector";
|
import BackgroundSelector from "./BackgroundSelector";
|
||||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
|
||||||
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig";
|
||||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||||
import ValidatedTextField from "../Input/ValidatedTextField";
|
import ValidatedTextField from "../Input/ValidatedTextField";
|
||||||
import {QueryParameters} from "../../Logic/Web/QueryParameters";
|
import {QueryParameters} from "../../Logic/Web/QueryParameters";
|
||||||
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
|
|
||||||
export default class FilterView extends VariableUiElement {
|
export default class FilterView extends VariableUiElement {
|
||||||
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
|
constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) {
|
||||||
|
@ -143,63 +144,33 @@ export default class FilterView extends VariableUiElement {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterIndexes = new Map<string, number>()
|
|
||||||
layer.filters.forEach((f, i) => filterIndexes.set(f.id, i))
|
|
||||||
|
|
||||||
let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map(
|
const toShow : BaseUIElement [] = []
|
||||||
filter => FilterView.createFilter(filter)
|
|
||||||
);
|
|
||||||
|
|
||||||
listFilterElements.forEach((inputElement, i) =>
|
for (const filter of layer.filters) {
|
||||||
inputElement[1].addCallback((changed) => {
|
|
||||||
const oldValue = flayer.appliedFilters.data
|
|
||||||
|
|
||||||
if (changed === undefined) {
|
const [ui, actualTags] = FilterView.createFilter(filter)
|
||||||
// Lets figure out which filter should be removed
|
|
||||||
// We know this inputElement corresponds with layer.filters[i]
|
|
||||||
// SO, if there is a value in 'oldValue' with this filter, we have to recalculated
|
|
||||||
if (!oldValue.some(f => f.filter === layer.filters[i])) {
|
|
||||||
// The filter to remove is already gone, we can stop
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (oldValue.some(f => f.filter === changed.filter && f.selected === changed.selected)) {
|
|
||||||
// The changed value is already there
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const listTagsFilters = Utils.NoNull(
|
|
||||||
listFilterElements.map((input) => input[1].data)
|
|
||||||
);
|
|
||||||
|
|
||||||
flayer.appliedFilters.setData(listTagsFilters);
|
ui.SetClass("mt-3")
|
||||||
|
toShow.push(ui)
|
||||||
|
actualTags.addCallback(tagsToFilterFor => {
|
||||||
|
flayer.appliedFilters.data.set(filter.id, tagsToFilterFor)
|
||||||
|
flayer.appliedFilters.ping()
|
||||||
})
|
})
|
||||||
);
|
flayer.appliedFilters.map(dict => dict.get(filter.id))
|
||||||
|
.addCallbackAndRun(filters => actualTags.setData(filters))
|
||||||
|
|
||||||
|
|
||||||
flayer.appliedFilters.addCallbackAndRun(appliedFilters => {
|
|
||||||
for (let i = 0; i < layer.filters.length; i++) {
|
|
||||||
const filter = layer.filters[i];
|
|
||||||
let foundMatch = undefined
|
|
||||||
for (const appliedFilter of appliedFilters) {
|
|
||||||
if (appliedFilter.filter === filter) {
|
|
||||||
foundMatch = appliedFilter
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listFilterElements[i][1].setData(foundMatch)
|
return new Combine(toShow)
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
return new Combine(listFilterElements.map(input => input[0].SetClass("mt-3")))
|
|
||||||
.SetClass("flex flex-col ml-8 bg-gray-300 rounded-xl p-2")
|
.SetClass("flex flex-col ml-8 bg-gray-300 rounded-xl p-2")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] {
|
|
||||||
|
|
||||||
if (filterConfig.options[0].fields.length > 0) {
|
|
||||||
|
|
||||||
// Filter which uses one or more textfields
|
// Filter which uses one or more textfields
|
||||||
|
private static createFilterWithFields(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||||
|
|
||||||
const filter = filterConfig.options[0]
|
const filter = filterConfig.options[0]
|
||||||
const mappings = new Map<string, BaseUIElement>()
|
const mappings = new Map<string, BaseUIElement>()
|
||||||
let allValid = new UIEventSource(true)
|
let allValid = new UIEventSource(true)
|
||||||
|
@ -218,50 +189,36 @@ export default class FilterView extends VariableUiElement {
|
||||||
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
|
allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable])
|
||||||
}
|
}
|
||||||
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings)
|
const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings)
|
||||||
const neutral = {
|
const trigger : UIEventSource<FilterState>= allValid.map(isValid => {
|
||||||
filter: new FilterConfig({
|
|
||||||
id: filterConfig.id,
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
question: "--",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, "While dynamically constructing a filterconfig"),
|
|
||||||
selected: 0
|
|
||||||
}
|
|
||||||
const trigger = allValid.map(isValid => {
|
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
return neutral
|
return undefined
|
||||||
}
|
}
|
||||||
|
const props = properties.data
|
||||||
// Replace all the field occurences in the tags...
|
// Replace all the field occurences in the tags...
|
||||||
const osmTags = Utils.WalkJson(filter.originalTagsSpec,
|
const tagsSpec = Utils.WalkJson(filter.originalTagsSpec,
|
||||||
v => {
|
v => {
|
||||||
if (typeof v !== "string") {
|
if (typeof v !== "string") {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return Utils.SubstituteKeys(v, properties.data)
|
|
||||||
|
for (const key in props) {
|
||||||
|
v = (<string>v).replace("{"+key+"}", props[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// ... which we use below to construct a filter!
|
const tagsFilter = TagUtils.Tag(tagsSpec)
|
||||||
return {
|
return {
|
||||||
filter: new FilterConfig({
|
currentFilter: tagsFilter,
|
||||||
id: filterConfig.id,
|
state: JSON.stringify(props)
|
||||||
options: [
|
|
||||||
{
|
|
||||||
question: "--",
|
|
||||||
osmTags
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}, "While dynamically constructing a filterconfig"),
|
|
||||||
selected: 0
|
|
||||||
}
|
}
|
||||||
}, [properties])
|
}, [properties])
|
||||||
|
|
||||||
return [tr, trigger];
|
return [tr, trigger];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static createCheckboxFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||||
if (filterConfig.options.length === 1) {
|
|
||||||
let option = filterConfig.options[0];
|
let option = filterConfig.options[0];
|
||||||
|
|
||||||
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6");
|
const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6");
|
||||||
|
@ -274,19 +231,16 @@ export default class FilterView extends VariableUiElement {
|
||||||
.ToggleOnClick()
|
.ToggleOnClick()
|
||||||
.SetClass("block m-1")
|
.SetClass("block m-1")
|
||||||
|
|
||||||
const selected = {
|
return [toggle, toggle.isEnabled.map(enabled => enabled ? {currentFilter: option.osmTags, state: "true"} : undefined, [],
|
||||||
filter: filterConfig,
|
f => f !== undefined)
|
||||||
selected: 0
|
|
||||||
}
|
|
||||||
return [toggle, toggle.isEnabled.map(enabled => enabled ? selected : undefined, [],
|
|
||||||
f => f?.filter === filterConfig && f?.selected === 0)
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
private static createMultiFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||||
|
|
||||||
let options = filterConfig.options;
|
let options = filterConfig.options;
|
||||||
|
|
||||||
const values = options.map((f, i) => ({
|
const values : FilterState[] = options.map((f, i) => ({
|
||||||
filter: filterConfig, selected: i
|
currentFilter: f.osmTags, state: i
|
||||||
}))
|
}))
|
||||||
const radio = new RadioButton(
|
const radio = new RadioButton(
|
||||||
options.map(
|
options.map(
|
||||||
|
@ -302,8 +256,25 @@ export default class FilterView extends VariableUiElement {
|
||||||
i => values[i],
|
i => values[i],
|
||||||
[],
|
[],
|
||||||
selected => {
|
selected => {
|
||||||
return selected?.selected
|
const v = selected?.state
|
||||||
|
if(v === undefined || typeof v === "string"){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
}
|
}
|
||||||
|
private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] {
|
||||||
|
|
||||||
|
if (filterConfig.options[0].fields.length > 0) {
|
||||||
|
return FilterView.createFilterWithFields(filterConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (filterConfig.options.length === 1) {
|
||||||
|
return FilterView.createCheckboxFilter(filterConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FilterView.createMultiFilter(filterConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,7 +111,10 @@ export default class SimpleAddUI extends Toggle {
|
||||||
message,
|
message,
|
||||||
state.LastClickLocation.data,
|
state.LastClickLocation.data,
|
||||||
confirm,
|
confirm,
|
||||||
cancel)
|
cancel,
|
||||||
|
() => {
|
||||||
|
isShown.setData(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
import {QueryParameters} from "../Logic/Web/QueryParameters";
|
||||||
import Constants from "../Models/Constants";
|
|
||||||
import Hash from "../Logic/Web/Hash";
|
import Hash from "../Logic/Web/Hash";
|
||||||
|
|
||||||
export class DefaultGuiState {
|
export class DefaultGuiState {
|
||||||
|
@ -46,18 +45,19 @@ export class DefaultGuiState {
|
||||||
"false",
|
"false",
|
||||||
"Whether or not the current view box is shown"
|
"Whether or not the current view box is shown"
|
||||||
)
|
)
|
||||||
if (Hash.hash.data === "download") {
|
const states = {
|
||||||
this.downloadControlIsOpened.setData(true)
|
download: this.downloadControlIsOpened,
|
||||||
|
filters: this.filterViewIsOpened,
|
||||||
|
copyright: this.copyrightViewIsOpened,
|
||||||
|
currentview: this.currentViewControlIsOpened,
|
||||||
|
welcome: this.welcomeMessageIsOpened
|
||||||
}
|
}
|
||||||
if (Hash.hash.data === "filters") {
|
Hash.hash.addCallbackAndRunD(hash => {
|
||||||
this.filterViewIsOpened.setData(true)
|
hash = hash.toLowerCase()
|
||||||
}
|
states[hash]?.setData(true)
|
||||||
if (Hash.hash.data === "copyright") {
|
})
|
||||||
this.copyrightViewIsOpened.setData(true)
|
|
||||||
}if (Hash.hash.data === "currentview") {
|
if (Hash.hash.data === "" || Hash.hash.data === undefined) {
|
||||||
this.currentViewControlIsOpened.setData(true)
|
|
||||||
}
|
|
||||||
if (Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome") {
|
|
||||||
this.welcomeMessageIsOpened.setData(true)
|
this.welcomeMessageIsOpened.setData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
loc: { lon: number, lat: number },
|
loc: { lon: number, lat: number },
|
||||||
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
|
confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void,
|
||||||
cancel: () => void,
|
cancel: () => void,
|
||||||
|
closePopup: () => void
|
||||||
) {
|
) {
|
||||||
|
|
||||||
let preciseInput: LocationInput = undefined
|
let preciseInput: LocationInput = undefined
|
||||||
|
@ -137,33 +138,26 @@ export default class ConfirmLocationOfPoint extends Combine {
|
||||||
]
|
]
|
||||||
).SetClass("flex flex-col")
|
).SetClass("flex flex-col")
|
||||||
).onClick(() => {
|
).onClick(() => {
|
||||||
preset.layerToAddTo.appliedFilters.setData([])
|
|
||||||
|
const appliedFilters = preset.layerToAddTo.appliedFilters;
|
||||||
|
appliedFilters.data.forEach((_, k) => appliedFilters.data.set(k, undefined))
|
||||||
|
appliedFilters.ping()
|
||||||
cancel()
|
cancel()
|
||||||
|
closePopup()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hasActiveFilter = preset.layerToAddTo.appliedFilters
|
||||||
|
.map(appliedFilters => {
|
||||||
|
const activeFilters = Array.from(appliedFilters.values()).filter(f => f?.currentFilter !== undefined);
|
||||||
|
return activeFilters.length === 0;
|
||||||
|
})
|
||||||
|
|
||||||
|
// If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled
|
||||||
const disableFiltersOrConfirm = new Toggle(
|
const disableFiltersOrConfirm = new Toggle(
|
||||||
openLayerOrConfirm,
|
openLayerOrConfirm,
|
||||||
disableFilter,
|
disableFilter,
|
||||||
preset.layerToAddTo.appliedFilters.map(filters => {
|
hasActiveFilter)
|
||||||
if (filters === undefined || filters.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for (const filter of filters) {
|
|
||||||
if (filter.selected === 0 && filter.filter.options.length === 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (filter.selected !== undefined) {
|
|
||||||
const tags = filter.filter.options[filter.selected].osmTags
|
|
||||||
if (tags !== undefined && tags["and"]?.length !== 0) {
|
|
||||||
// This actually doesn't filter anything at all
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
|
const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection);
|
||||||
|
|
|
@ -520,7 +520,8 @@ export class ImportPointButton extends AbstractImportButton {
|
||||||
guiState: DefaultGuiState,
|
guiState: DefaultGuiState,
|
||||||
originalFeatureTags: UIEventSource<any>,
|
originalFeatureTags: UIEventSource<any>,
|
||||||
feature: any,
|
feature: any,
|
||||||
onCancel: () => void): BaseUIElement {
|
onCancel: () => void,
|
||||||
|
close: () => void): BaseUIElement {
|
||||||
|
|
||||||
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
||||||
|
|
||||||
|
@ -559,7 +560,7 @@ export class ImportPointButton extends AbstractImportButton {
|
||||||
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
|
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
|
||||||
lon,
|
lon,
|
||||||
lat
|
lat
|
||||||
}, confirm, onCancel)
|
}, confirm, onCancel, close)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -567,7 +568,7 @@ export class ImportPointButton extends AbstractImportButton {
|
||||||
originalFeatureTags,
|
originalFeatureTags,
|
||||||
guiState,
|
guiState,
|
||||||
feature,
|
feature,
|
||||||
onCancel): BaseUIElement {
|
onCancel: () => void): BaseUIElement {
|
||||||
|
|
||||||
|
|
||||||
const geometry = feature.geometry
|
const geometry = feature.geometry
|
||||||
|
@ -579,7 +580,11 @@ export class ImportPointButton extends AbstractImportButton {
|
||||||
guiState,
|
guiState,
|
||||||
originalFeatureTags,
|
originalFeatureTags,
|
||||||
feature,
|
feature,
|
||||||
onCancel
|
onCancel,
|
||||||
|
() => {
|
||||||
|
// Close the current popup
|
||||||
|
state.selectedElement.setData(undefined)
|
||||||
|
}
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,9 @@ import AutoApplyButton from "./Popup/AutoApplyButton";
|
||||||
import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json";
|
import * as left_right_style_json from "../assets/layers/left_right_style/left_right_style.json";
|
||||||
import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
|
import {OpenIdEditor} from "./BigComponents/CopyrightPanel";
|
||||||
import Toggle from "./Input/Toggle";
|
import Toggle from "./Input/Toggle";
|
||||||
|
import Img from "./Base/Img";
|
||||||
|
import ValidatedTextField from "./Input/ValidatedTextField";
|
||||||
|
import Link from "./Base/Link";
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
funcName: string,
|
funcName: string,
|
||||||
|
@ -53,7 +56,40 @@ export default class SpecialVisualizations {
|
||||||
|
|
||||||
public static specialVisualizations = SpecialVisualizations.init()
|
public static specialVisualizations = SpecialVisualizations.init()
|
||||||
|
|
||||||
private static init(){
|
public static HelpMessage() {
|
||||||
|
|
||||||
|
const helpTexts =
|
||||||
|
SpecialVisualizations.specialVisualizations.map(viz => new Combine(
|
||||||
|
[
|
||||||
|
new Title(viz.funcName, 3),
|
||||||
|
viz.docs,
|
||||||
|
viz.args.length > 0 ? new Table(["name", "default", "description"],
|
||||||
|
viz.args.map(arg => {
|
||||||
|
let defaultArg = arg.defaultValue ?? "_undefined_"
|
||||||
|
if (defaultArg == "") {
|
||||||
|
defaultArg = "_empty string_"
|
||||||
|
}
|
||||||
|
return [arg.name, defaultArg, arg.doc];
|
||||||
|
})
|
||||||
|
) : undefined,
|
||||||
|
new Title("Example usage of " + viz.funcName, 4),
|
||||||
|
new FixedUiElement(
|
||||||
|
viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`"
|
||||||
|
).SetClass("literal-code"),
|
||||||
|
|
||||||
|
]
|
||||||
|
));
|
||||||
|
|
||||||
|
return new Combine([
|
||||||
|
new Title("Special tag renderings", 1),
|
||||||
|
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
||||||
|
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
||||||
|
...helpTexts
|
||||||
|
]
|
||||||
|
).SetClass("flex flex-col");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static init() {
|
||||||
const specialVisualizations: SpecialVisualization[] =
|
const specialVisualizations: SpecialVisualization[] =
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
@ -590,7 +626,7 @@ export default class SpecialVisualizations {
|
||||||
funcName: "open_in_iD",
|
funcName: "open_in_iD",
|
||||||
docs: "Opens the current view in the iD-editor",
|
docs: "Opens the current view in the iD-editor",
|
||||||
args: [],
|
args: [],
|
||||||
constr: (state, feature ) => {
|
constr: (state, feature) => {
|
||||||
return new OpenIdEditor(state, undefined, feature.data.id)
|
return new OpenIdEditor(state, undefined, feature.data.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -611,29 +647,51 @@ export default class SpecialVisualizations {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
funcName: "close_note",
|
funcName: "close_note",
|
||||||
docs: "Button to close a note",
|
docs: "Button to close a note - eventually with a prefixed text",
|
||||||
args:[
|
args: [
|
||||||
{
|
{
|
||||||
name:"text",
|
name: "text",
|
||||||
doc: "Text to show on this button",
|
doc: "Text to show on this button",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name:"Id-key",
|
name: "icon",
|
||||||
|
doc: "Icon to show",
|
||||||
|
defaultValue: "checkmark.svg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Id-key",
|
||||||
doc: "The property name where the ID of the note to close can be found",
|
doc: "The property name where the ID of the note to close can be found",
|
||||||
defaultValue: "id"
|
defaultValue: "id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comment",
|
||||||
|
doc: "Text to add onto the note when closing",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
constr: (state, tags, args, guiState) => {
|
constr: (state, tags, args, guiState) => {
|
||||||
const t = Translations.t.notes;
|
const t = Translations.t.notes;
|
||||||
const closeButton = new SubtleButton( Svg.checkmark_svg(), t.closeNote)
|
|
||||||
const isClosed = new UIEventSource(false);
|
let icon = Svg.checkmark_svg()
|
||||||
|
if (args[2] !== "checkmark.svg" && (args[2] ?? "") !== "") {
|
||||||
|
icon = new Img(args[2])
|
||||||
|
}
|
||||||
|
let textToShow = t.closeNote;
|
||||||
|
if ((args[0] ?? "") !== "") {
|
||||||
|
textToShow = Translations.T(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeButton = new SubtleButton(icon, textToShow)
|
||||||
|
const isClosed = tags.map(tags => (tags["closed_at"] ?? "") === "");
|
||||||
closeButton.onClick(() => {
|
closeButton.onClick(() => {
|
||||||
const id = tags.data[args[1] ?? "id"]
|
const id = tags.data[args[1] ?? "id"]
|
||||||
if(state.featureSwitchIsTesting.data){
|
if (state.featureSwitchIsTesting.data) {
|
||||||
console.log("Not actually closing note...")
|
console.log("Not actually closing note...")
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.osmConnection.closeNote(id).then(_ => isClosed.setData(true))
|
state.osmConnection.closeNote(id, args[3]).then(_ => {
|
||||||
|
tags.data["closed_at"] = new Date().toISOString();
|
||||||
|
tags.ping()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
return new Toggle(
|
return new Toggle(
|
||||||
t.isClosed.SetClass("thanks"),
|
t.isClosed.SetClass("thanks"),
|
||||||
|
@ -641,6 +699,151 @@ export default class SpecialVisualizations {
|
||||||
isClosed
|
isClosed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
funcName: "add_note_comment",
|
||||||
|
docs: "A textfield to add a comment to a node (with the option to close the note).",
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: "Id-key",
|
||||||
|
doc: "The property name where the ID of the note to close can be found",
|
||||||
|
defaultValue: "id"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
constr: (state, tags, args, guiState) => {
|
||||||
|
|
||||||
|
const t = Translations.t.notes;
|
||||||
|
const textField = ValidatedTextField.InputForType("text", {placeholder: t.addCommentPlaceholder})
|
||||||
|
textField.SetClass("rounded-l border border-grey")
|
||||||
|
const txt = textField.GetValue()
|
||||||
|
|
||||||
|
const addCommentButton = new SubtleButton(undefined, t.addCommentPlaceholder)
|
||||||
|
.onClick(async () => {
|
||||||
|
const id = tags.data[args[1] ?? "id"]
|
||||||
|
|
||||||
|
if (isClosed.data) {
|
||||||
|
await state.osmConnection.reopenNote(id, txt.data)
|
||||||
|
await state.osmConnection.closeNote(id)
|
||||||
|
} else {
|
||||||
|
await state.osmConnection.addCommentToNode(id, txt.data)
|
||||||
|
}
|
||||||
|
const comments: any[] = JSON.parse(tags.data["comments"])
|
||||||
|
const username = state.osmConnection.userDetails.data.name
|
||||||
|
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.data
|
||||||
|
})
|
||||||
|
tags.data["comments"] = JSON.stringify(comments)
|
||||||
|
tags.ping()
|
||||||
|
txt.setData("")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const close = new SubtleButton(undefined, new VariableUiElement(txt.map(txt => {
|
||||||
|
if (txt === undefined || txt === "") {
|
||||||
|
return t.closeNote
|
||||||
|
}
|
||||||
|
return t.addCommentAndClose
|
||||||
|
}))).onClick(() => {
|
||||||
|
const id = tags.data[args[1] ?? "id"]
|
||||||
|
if (state.featureSwitchIsTesting.data) {
|
||||||
|
console.log("Testmode: Not actually closing note...")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.osmConnection.closeNote(id, txt.data).then(_ => {
|
||||||
|
tags.data["closed_at"] = new Date().toISOString();
|
||||||
|
tags.ping()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const reopen = new SubtleButton(undefined, new VariableUiElement(txt.map(txt => {
|
||||||
|
if (txt === undefined || txt === "") {
|
||||||
|
return t.reopenNote
|
||||||
|
}
|
||||||
|
return t.reopenNoteAndComment
|
||||||
|
}))).onClick(() => {
|
||||||
|
const id = tags.data[args[1] ?? "id"]
|
||||||
|
if (state.featureSwitchIsTesting.data) {
|
||||||
|
console.log("Testmode: Not actually reopening note...")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.osmConnection.reopenNote(id, txt.data).then(_ => {
|
||||||
|
tags.data["closed_at"] = undefined;
|
||||||
|
tags.ping()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isClosed = tags.map(tags => (tags["closed_at"] ?? "") !== "");
|
||||||
|
const stateButtons = new Toggle(reopen, close, isClosed)
|
||||||
|
|
||||||
|
return new Combine([
|
||||||
|
new Title("Add a comment"),
|
||||||
|
textField,
|
||||||
|
new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end")
|
||||||
|
]).SetClass("border-2 border-black rounded-xl p-4 block");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
funcName: "visualize_note_comments",
|
||||||
|
docs: "Visualises the comments for nodes",
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: "commentsKey",
|
||||||
|
doc: "The property name of the comments, which should be stringified json",
|
||||||
|
defaultValue: "comments"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
, constr: (state, tags, args) => {
|
||||||
|
const t = Translations.t.notes;
|
||||||
|
return new VariableUiElement(
|
||||||
|
tags.map(tags => tags[args[0]])
|
||||||
|
.map(commentsStr => {
|
||||||
|
const comments:
|
||||||
|
{
|
||||||
|
"date": string,
|
||||||
|
"uid": number,
|
||||||
|
"user": string,
|
||||||
|
"user_url": string,
|
||||||
|
"action": "closed" | "opened" | "reopened" | "commented",
|
||||||
|
"text": string, "html": string
|
||||||
|
}[] = JSON.parse(commentsStr)
|
||||||
|
|
||||||
|
|
||||||
|
return new Combine(comments
|
||||||
|
.filter(c => c.text !== "")
|
||||||
|
.map(c => {
|
||||||
|
let actionIcon: BaseUIElement = undefined;
|
||||||
|
if (c.action === "opened" || c.action === "reopened") {
|
||||||
|
actionIcon = Svg.note_svg()
|
||||||
|
} else if (c.action === "closed") {
|
||||||
|
actionIcon = Svg.resolved_svg()
|
||||||
|
} else {
|
||||||
|
actionIcon = Svg.addSmall_svg()
|
||||||
|
}
|
||||||
|
|
||||||
|
let user: BaseUIElement
|
||||||
|
if (c.user === undefined) {
|
||||||
|
user = t.anonymous
|
||||||
|
} else {
|
||||||
|
user = new Link(c.user, c.user_url ?? "", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Combine([new Combine([
|
||||||
|
actionIcon.SetClass("mr-4 w-6").SetStyle("flex-shrink: 0"),
|
||||||
|
new FixedUiElement(c.html).SetClass("flex flex-col").SetStyle("margin: 0"),
|
||||||
|
]).SetClass("flex"),
|
||||||
|
new Combine([user.SetClass("mr-2"), c.date]).SetClass("flex justify-end subtle")
|
||||||
|
]).SetClass("flex flex-col")
|
||||||
|
|
||||||
|
})).SetClass("flex flex-col")
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -649,38 +852,4 @@ export default class SpecialVisualizations {
|
||||||
return specialVisualizations;
|
return specialVisualizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static HelpMessage() {
|
|
||||||
|
|
||||||
const helpTexts =
|
|
||||||
SpecialVisualizations.specialVisualizations.map(viz => new Combine(
|
|
||||||
[
|
|
||||||
new Title(viz.funcName, 3),
|
|
||||||
viz.docs,
|
|
||||||
viz.args.length > 0 ? new Table(["name", "default", "description"],
|
|
||||||
viz.args.map(arg => {
|
|
||||||
let defaultArg = arg.defaultValue ?? "_undefined_"
|
|
||||||
if (defaultArg == "") {
|
|
||||||
defaultArg = "_empty string_"
|
|
||||||
}
|
|
||||||
return [arg.name, defaultArg, arg.doc];
|
|
||||||
})
|
|
||||||
) : undefined,
|
|
||||||
new Title("Example usage of "+viz.funcName, 4),
|
|
||||||
new FixedUiElement(
|
|
||||||
viz.example ?? "`{" + viz.funcName + "(" + viz.args.map(arg => arg.defaultValue).join(",") + ")}`"
|
|
||||||
).SetClass("literal-code"),
|
|
||||||
|
|
||||||
]
|
|
||||||
));
|
|
||||||
|
|
||||||
return new Combine([
|
|
||||||
new Title("Special tag renderings", 1),
|
|
||||||
"In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.",
|
|
||||||
"General usage is `{func_name()}`, `{func_name(arg, someotherarg)}` or `{func_name(args):cssStyle}`. Note that you _do not_ need to use quotes around your arguments, the comma is enough to separate them. This also implies you cannot use a comma in your args",
|
|
||||||
...helpTexts
|
|
||||||
]
|
|
||||||
).SetClass("flex flex-col");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -823,6 +823,14 @@
|
||||||
"authors": [],
|
"authors": [],
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "note.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "osm-logo-us.svg",
|
"path": "osm-logo-us.svg",
|
||||||
"license": "Logo",
|
"license": "Logo",
|
||||||
|
@ -965,6 +973,14 @@
|
||||||
],
|
],
|
||||||
"sources": []
|
"sources": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "resolved.svg",
|
||||||
|
"license": "CC0",
|
||||||
|
"authors": [
|
||||||
|
"Pieter Vander Vennet"
|
||||||
|
],
|
||||||
|
"sources": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "ring.svg",
|
"path": "ring.svg",
|
||||||
"license": "CC0; trivial",
|
"license": "CC0; trivial",
|
||||||
|
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -1,18 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"path": "note.svg",
|
|
||||||
"license": "CC0",
|
|
||||||
"authors": [
|
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"sources": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "resolved.svg",
|
|
||||||
"license": "CC0",
|
|
||||||
"authors": [
|
|
||||||
"Pieter Vander Vennet"
|
|
||||||
],
|
|
||||||
"sources": []
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -9,8 +9,8 @@
|
||||||
"startZoom": 0,
|
"startZoom": 0,
|
||||||
"title": "Notes on OpenStreetMap",
|
"title": "Notes on OpenStreetMap",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"description": "Notes from OpenStreetMap",
|
"description": "A note is a pin on the map with some text to indicate something wrong.<br/><br/>Make sure to checkout the <a href='#filters'>filter view</a> to search for users and text.",
|
||||||
"icon": "./assets/themes/notes/resolved.svg",
|
"icon": "./assets/svg/resolved.svg",
|
||||||
"clustering": false,
|
"clustering": false,
|
||||||
"enableDownload": true,
|
"enableDownload": true,
|
||||||
"layers": [
|
"layers": [
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"en": "OpenStreetMap notes"
|
"en": "OpenStreetMap notes"
|
||||||
},
|
},
|
||||||
"description": "Notes on OpenStreetMap.org",
|
"description": "This layer shows notes on OpenStreetMap.",
|
||||||
"source": {
|
"source": {
|
||||||
"osmTags": "id~*",
|
"osmTags": "id~*",
|
||||||
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}",
|
"geoJson": "https://api.openstreetmap.org/api/0.6/notes.json?closed=7&bbox={x_min},{y_min},{x_max},{y_max}",
|
||||||
|
@ -42,7 +42,10 @@
|
||||||
},
|
},
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_first_comment:=feat.get('comments')[0].text.toLowerCase()",
|
"_first_comment:=feat.get('comments')[0].text.toLowerCase()",
|
||||||
"_conversation=feat.get('comments').map(c => { let user = 'anonymous user'; if(c.user_url !== undefined){user = '<a href=\"'+c.user_url+'\" target=\"_blank\">'+c.user+'</a>'}; return c.html +'<div class=\"subtle flex justify-end border-t border-gray-500\">' + user + ' '+c.date+'</div>' }).join('')"
|
"_opened_by_anonymous_user:=feat.get('comments')[0].user === undefined",
|
||||||
|
"_first_user:=feat.get('comments')[0].user",
|
||||||
|
"_first_user_lc:=feat.get('comments')[0].user?.toLowerCase()",
|
||||||
|
"_first_user_id:=feat.get('comments')[0].uid"
|
||||||
],
|
],
|
||||||
"titleIcons": [
|
"titleIcons": [
|
||||||
{
|
{
|
||||||
|
@ -52,18 +55,18 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"id": "conversation",
|
"id": "conversation",
|
||||||
"render": "{_conversation}"
|
"render": "{visualize_note_comments()}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "date_created",
|
"id": "comment",
|
||||||
|
"render": "{add_note_comment()}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Spam",
|
||||||
"render": {
|
"render": {
|
||||||
"en": "Opened on {date_created}"
|
"en": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Report {_first_user} as spam</a>"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
"condition": "_opened_by_anonymous_user=false"
|
||||||
"id": "close",
|
|
||||||
"render": "{close_note()}",
|
|
||||||
"condition": "closed_at="
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
|
@ -73,11 +76,11 @@
|
||||||
"centroid"
|
"centroid"
|
||||||
],
|
],
|
||||||
"icon": {
|
"icon": {
|
||||||
"render": "./assets/themes/notes/note.svg",
|
"render": "./assets/svg/note.svg",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": "closed_at~*",
|
"if": "closed_at~*",
|
||||||
"then": "./assets/themes/notes/resolved.svg"
|
"then": "./assets/svg/resolved.svg"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -116,6 +119,49 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "opened_by",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"osmTags": "_first_user_lc~.*{search}.*",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "search"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"question": {
|
||||||
|
"en": "Opened by {search}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "not_opened_by",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"osmTags": "_first_user_lc!~.*{search}.*",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "search"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"question": {
|
||||||
|
"en": "<b>Not</b> opened by {search}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "anonymous",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"osmTags": "_opened_by_anonymous_user=true",
|
||||||
|
"question": {
|
||||||
|
"en": "Opened by anonymous user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -864,14 +864,18 @@ video {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt-1 {
|
.mr-2 {
|
||||||
margin-top: 0.25rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-4 {
|
.mr-4 {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mt-1 {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-2 {
|
.mt-2 {
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -888,10 +892,6 @@ video {
|
||||||
margin-left: 2rem;
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-2 {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-10 {
|
.mb-10 {
|
||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
@ -1068,6 +1068,10 @@ video {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-6 {
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-0 {
|
.w-0 {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
@ -1080,10 +1084,6 @@ video {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-6 {
|
|
||||||
width: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-16 {
|
.w-16 {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
}
|
}
|
||||||
|
@ -1283,26 +1283,31 @@ video {
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-lg {
|
.rounded-lg {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-xl {
|
.rounded-l {
|
||||||
border-radius: 0.75rem;
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-4 {
|
|
||||||
border-width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-2 {
|
.border-2 {
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.border-4 {
|
||||||
|
border-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.border-l-4 {
|
.border-l-4 {
|
||||||
border-left-width: 4px;
|
border-left-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,7 +425,12 @@
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"isClosed": "This note is resolved",
|
"isClosed": "This note is resolved",
|
||||||
"closeNote":
|
"addCommentPlaceholder": "Add a comment...",
|
||||||
"Close this note"
|
"addComment": "Add comment",
|
||||||
|
"addCommentAndClose": "Add comment and close",
|
||||||
|
"closeNote": "Close note",
|
||||||
|
"reopenNote": "Reopen note",
|
||||||
|
"reopenNoteAndComment": "Reopen note and comment",
|
||||||
|
"anonymous": "Anonymous user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -947,11 +947,56 @@
|
||||||
"notes": {
|
"notes": {
|
||||||
"layers": {
|
"layers": {
|
||||||
"0": {
|
"0": {
|
||||||
|
"filter": {
|
||||||
|
"0": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Should mention {search} in the first comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Should <b>not</b> mention {search} in the first comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Opened by {search}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "<b>Not</b> opened by {search}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"options": {
|
||||||
|
"0": {
|
||||||
|
"question": "Opened by anonymous user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"name": "OpenStreetMap notes",
|
"name": "OpenStreetMap notes",
|
||||||
"tagRenderings": {
|
"tagRenderings": {
|
||||||
"date_created": {
|
"Spam": {
|
||||||
"render": "Opened on {date_created}"
|
"render": "<a href='https://www.openstreetmap.org/reports/new?reportable_id={_first_user_id}&reportable_type=User' target='_blank' class='subtle'>Report {_first_user} as spam</a>"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"mappings": {
|
||||||
|
"0": {
|
||||||
|
"then": "Closed note"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"render": "Note"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue