forked from MapComplete/MapComplete
		
	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 {BBox} from "../../BBox"; | ||||
| import {ElementStorage} from "../../ElementStorage"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| 
 | ||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | ||||
|     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 ?? []) { | ||||
|                 const neededTags = filter.filter.options[filter.selected].osmTags | ||||
|                 if (!neededTags.matchesProperties(f.feature.properties)) { | ||||
|                 const neededTags : TagsFilter = filter?.currentFilter | ||||
|                 if (neededTags !== undefined && !neededTags.matchesProperties(f.feature.properties)) { | ||||
|                     // Hidden by the filter on the layer itself - we want to hide it no matter wat
 | ||||
|                     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) => { | ||||
|             this.auth.xhr({ | ||||
|                 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) { | ||||
|                 console.log("Closing note gave:", err, response) | ||||
|                 if (err !== null) { | ||||
|                     error(err) | ||||
|                 } else { | ||||
|  |  | |||
|  | @ -7,11 +7,10 @@ import Attribution from "../../UI/BigComponents/Attribution"; | |||
| import Minimap, {MinimapObj} from "../../UI/Base/Minimap"; | ||||
| import {Tiles} from "../../Models/TileRange"; | ||||
| import BaseUIElement from "../../UI/BaseUIElement"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||
| import {QueryParameters} from "../Web/QueryParameters"; | ||||
| import * as personal from "../../assets/themes/personal/personal.json"; | ||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | ||||
| import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer"; | ||||
| import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | ||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||
|  | @ -339,7 +338,6 @@ export default class MapState extends UserRelatedState { | |||
|     private InitializeFilteredLayers() { | ||||
| 
 | ||||
|         const layoutToUse = this.layoutToUse; | ||||
|         const empty = [] | ||||
|         const flayers: FilteredLayer[] = []; | ||||
|         for (const layer of layoutToUse.layers) { | ||||
|             let isDisplayed: UIEventSource<boolean> | ||||
|  | @ -355,26 +353,18 @@ export default class MapState extends UserRelatedState { | |||
|                     "Wether or not layer " + layer.id + " is shown" | ||||
|                 ) | ||||
|             } | ||||
|             const flayer = { | ||||
|             const flayer : FilteredLayer = { | ||||
|                 isDisplayed: isDisplayed, | ||||
|                 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) { | ||||
|                 const filtersPerName = new Map<string, FilterConfig>() | ||||
|                 layer.filters.forEach(f => filtersPerName.set(f.id, f)) | ||||
|                 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) | ||||
|             } | ||||
|                 stateSrc    .addCallbackAndRun(state => flayer.appliedFilters.data.set(filterConfig.id, state)) | ||||
|                 flayer.appliedFilters.map(dict => dict.get(filterConfig.id)) | ||||
|                     .addCallback(state => stateSrc.setData(state)) | ||||
|             }) | ||||
| 
 | ||||
|             flayers.push(flayer); | ||||
|         } | ||||
|  |  | |||
|  | @ -1,9 +1,13 @@ | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| 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 { | ||||
|     readonly isDisplayed: UIEventSource<boolean>; | ||||
|     readonly appliedFilters: UIEventSource<{ filter: FilterConfig, selected: number }[]>; | ||||
|     readonly appliedFilters: UIEventSource<Map<string, FilterState>>; | ||||
|     readonly layerDef: LayerConfig; | ||||
| } | ||||
|  | @ -4,15 +4,17 @@ import FilterConfigJson from "./Json/FilterConfigJson"; | |||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| import ValidatedTextField from "../../UI/Input/ValidatedTextField"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; | ||||
| 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 { | ||||
|     public readonly id: string | ||||
|     public readonly options: { | ||||
|         question: Translation; | ||||
|         osmTags: TagsFilter; | ||||
|         osmTags: TagsFilter | undefined; | ||||
|         originalTagsSpec: string | AndOrTagConfigJson | ||||
|         fields: { name: string, type: string }[] | ||||
|     }[]; | ||||
|  | @ -39,11 +41,14 @@ export default class FilterConfig { | |||
|                 option.question, | ||||
|                 `${ctx}.question` | ||||
|             ); | ||||
|             let osmTags = TagUtils.Tag( | ||||
|                     option.osmTags ?? {and: []}, | ||||
|             let osmTags = undefined; | ||||
|             if (option.osmTags !== undefined) { | ||||
|                 osmTags = TagUtils.Tag( | ||||
|                     option.osmTags, | ||||
|                     `${ctx}.osmTags` | ||||
|                 ); | ||||
| 
 | ||||
|             } | ||||
|             if (question === undefined) { | ||||
|                 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
 | ||||
|                 osmTags = TagUtils.Tag({and:[]}) | ||||
|                 osmTags = undefined | ||||
|             } | ||||
| 
 | ||||
|             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.` | ||||
|         } | ||||
| 
 | ||||
|         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" | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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 { | ||||
|         if (fixed.errors?.length > 0) { | ||||
|         if (fixed?.errors?.length > 0) { | ||||
|             throw fixed.errors.join("\n"); | ||||
|         } | ||||
|         fixed.warnings?.forEach(w => console.warn(w)) | ||||
|  |  | |||
|  | @ -10,13 +10,14 @@ import Svg from "../../Svg"; | |||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import State from "../../State"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import FilteredLayer, {FilterState} from "../../Models/FilteredLayer"; | ||||
| import BackgroundSelector from "./BackgroundSelector"; | ||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | ||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||
| import {SubstitutedTranslation} from "../SubstitutedTranslation"; | ||||
| import ValidatedTextField from "../Input/ValidatedTextField"; | ||||
| import {QueryParameters} from "../../Logic/Web/QueryParameters"; | ||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||
| 
 | ||||
| export default class FilterView extends VariableUiElement { | ||||
|     constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) { | ||||
|  | @ -143,63 +144,33 @@ export default class FilterView extends VariableUiElement { | |||
|             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( | ||||
|             filter => FilterView.createFilter(filter) | ||||
|         ); | ||||
|         const toShow : BaseUIElement [] = [] | ||||
| 
 | ||||
|         listFilterElements.forEach((inputElement, i) => | ||||
|             inputElement[1].addCallback((changed) => { | ||||
|                 const oldValue = flayer.appliedFilters.data | ||||
|         for (const filter of layer.filters) { | ||||
|              | ||||
|                 if (changed === undefined) { | ||||
|                     // 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) | ||||
|                 ); | ||||
|             const [ui, actualTags] = FilterView.createFilter(filter) | ||||
|              | ||||
|                 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(listFilterElements.map(input => input[0].SetClass("mt-3"))) | ||||
|         return new Combine(toShow) | ||||
|             .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
 | ||||
|     private static createFilterWithFields(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { | ||||
| 
 | ||||
|         const filter = filterConfig.options[0] | ||||
|         const mappings = new Map<string, BaseUIElement>() | ||||
|         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]) | ||||
|         } | ||||
|         const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings) | ||||
|             const neutral = { | ||||
|                 filter: new FilterConfig({ | ||||
|                     id: filterConfig.id, | ||||
|                     options: [ | ||||
|                         { | ||||
|                             question: "--", | ||||
|                         } | ||||
|                     ] | ||||
|                 }, "While dynamically constructing a filterconfig"), | ||||
|                 selected: 0 | ||||
|             } | ||||
|             const trigger = allValid.map(isValid => { | ||||
|         const trigger : UIEventSource<FilterState>= allValid.map(isValid => { | ||||
|             if (!isValid) { | ||||
|                     return neutral | ||||
|                 return undefined | ||||
|             } | ||||
| 
 | ||||
|             const props = properties.data | ||||
|             // Replace all the field occurences in the tags...
 | ||||
|                 const osmTags = Utils.WalkJson(filter.originalTagsSpec, | ||||
|            const tagsSpec = Utils.WalkJson(filter.originalTagsSpec, | ||||
|                 v => { | ||||
|                     if (typeof v !== "string") { | ||||
|                         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 { | ||||
|                     filter: new FilterConfig({ | ||||
|                         id: filterConfig.id, | ||||
|                         options: [ | ||||
|                             { | ||||
|                                 question: "--", | ||||
|                                 osmTags | ||||
|                             } | ||||
|                         ] | ||||
|                     }, "While dynamically constructing a filterconfig"), | ||||
|                     selected: 0 | ||||
|                 currentFilter: tagsFilter, | ||||
|                 state: JSON.stringify(props) | ||||
|             } | ||||
|         }, [properties]) | ||||
|          | ||||
|         return [tr, trigger]; | ||||
|     } | ||||
|      | ||||
| 
 | ||||
|         if (filterConfig.options.length === 1) { | ||||
|     private static createCheckboxFilter(filterConfig: FilterConfig):  [BaseUIElement, UIEventSource<FilterState>] { | ||||
|         let option = filterConfig.options[0]; | ||||
| 
 | ||||
|         const icon = Svg.checkbox_filled_svg().SetClass("block mr-2 w-6"); | ||||
|  | @ -274,19 +231,16 @@ export default class FilterView extends VariableUiElement { | |||
|             .ToggleOnClick() | ||||
|             .SetClass("block m-1") | ||||
| 
 | ||||
|             const selected = { | ||||
|                 filter: filterConfig, | ||||
|                 selected: 0 | ||||
|             } | ||||
|             return [toggle, toggle.isEnabled.map(enabled => enabled ? selected : undefined, [], | ||||
|                 f => f?.filter === filterConfig && f?.selected === 0) | ||||
|         return [toggle, toggle.isEnabled.map(enabled => enabled ? {currentFilter: option.osmTags, state: "true"} : undefined, [], | ||||
|             f => f !== undefined) | ||||
|         ] | ||||
|     } | ||||
|     private static createMultiFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<FilterState>] { | ||||
| 
 | ||||
|         let options = filterConfig.options; | ||||
| 
 | ||||
|         const values = options.map((f, i) => ({ | ||||
|             filter: filterConfig, selected: i | ||||
|         const values : FilterState[] = options.map((f, i) => ({ | ||||
|             currentFilter: f.osmTags, state: i | ||||
|         })) | ||||
|         const radio = new RadioButton( | ||||
|             options.map( | ||||
|  | @ -302,8 +256,25 @@ export default class FilterView extends VariableUiElement { | |||
|                 i => values[i], | ||||
|                 [], | ||||
|                 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, | ||||
|                         state.LastClickLocation.data, | ||||
|                         confirm, | ||||
|                         cancel) | ||||
|                         cancel, | ||||
|                         () => { | ||||
|                             isShown.setData(false) | ||||
|                         }) | ||||
|                 } | ||||
|             )) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {QueryParameters} from "../Logic/Web/QueryParameters"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import Hash from "../Logic/Web/Hash"; | ||||
| 
 | ||||
| export class DefaultGuiState { | ||||
|  | @ -46,18 +45,19 @@ export class DefaultGuiState { | |||
|             "false", | ||||
|             "Whether or not the current view box is shown" | ||||
|         ) | ||||
|         if (Hash.hash.data === "download") { | ||||
|             this.downloadControlIsOpened.setData(true) | ||||
|         const states = { | ||||
|             download: this.downloadControlIsOpened, | ||||
|             filters: this.filterViewIsOpened, | ||||
|             copyright: this.copyrightViewIsOpened, | ||||
|             currentview: this.currentViewControlIsOpened, | ||||
|             welcome: this.welcomeMessageIsOpened | ||||
|         } | ||||
|         if (Hash.hash.data === "filters") { | ||||
|             this.filterViewIsOpened.setData(true) | ||||
|         } | ||||
|         if (Hash.hash.data === "copyright") { | ||||
|             this.copyrightViewIsOpened.setData(true) | ||||
|         }if (Hash.hash.data === "currentview") { | ||||
|             this.currentViewControlIsOpened.setData(true) | ||||
|         } | ||||
|         if (Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome") { | ||||
|         Hash.hash.addCallbackAndRunD(hash => { | ||||
|             hash = hash.toLowerCase() | ||||
|             states[hash]?.setData(true) | ||||
|         }) | ||||
|         | ||||
|         if (Hash.hash.data === "" || Hash.hash.data === undefined) { | ||||
|             this.welcomeMessageIsOpened.setData(true) | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -29,6 +29,7 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|         loc: { lon: number, lat: number }, | ||||
|         confirm: (tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) => void, | ||||
|         cancel: () => void, | ||||
|         closePopup: () => void | ||||
|     ) { | ||||
| 
 | ||||
|         let preciseInput: LocationInput = undefined | ||||
|  | @ -137,33 +138,26 @@ export default class ConfirmLocationOfPoint extends Combine { | |||
|                 ] | ||||
|             ).SetClass("flex flex-col") | ||||
|         ).onClick(() => { | ||||
|             preset.layerToAddTo.appliedFilters.setData([]) | ||||
|              | ||||
|             const appliedFilters = preset.layerToAddTo.appliedFilters; | ||||
|             appliedFilters.data.forEach((_, k) => appliedFilters.data.set(k, undefined)) | ||||
|             appliedFilters.ping() | ||||
|             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( | ||||
|             openLayerOrConfirm, | ||||
|             disableFilter,  | ||||
|             preset.layerToAddTo.appliedFilters.map(filters => { | ||||
|                 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 | ||||
|             hasActiveFilter) | ||||
|          | ||||
|             }) | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
|         const tagInfo = SimpleAddUI.CreateTagInfoFor(preset, state.osmConnection); | ||||
|  |  | |||
|  | @ -520,7 +520,8 @@ export class ImportPointButton extends AbstractImportButton { | |||
|         guiState: DefaultGuiState, | ||||
|         originalFeatureTags: UIEventSource<any>, | ||||
|         feature: any, | ||||
|         onCancel: () => void): BaseUIElement { | ||||
|         onCancel: () => void, | ||||
|         close: () => void): BaseUIElement { | ||||
| 
 | ||||
|         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), { | ||||
|             lon, | ||||
|             lat | ||||
|         }, confirm, onCancel) | ||||
|         }, confirm, onCancel, close) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  | @ -567,7 +568,7 @@ export class ImportPointButton extends AbstractImportButton { | |||
|                      originalFeatureTags, | ||||
|                      guiState, | ||||
|                      feature, | ||||
|                      onCancel): BaseUIElement { | ||||
|                      onCancel: () => void): BaseUIElement { | ||||
| 
 | ||||
| 
 | ||||
|         const geometry = feature.geometry | ||||
|  | @ -579,7 +580,11 @@ export class ImportPointButton extends AbstractImportButton { | |||
|                 guiState, | ||||
|                 originalFeatureTags, | ||||
|                 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 {OpenIdEditor} from "./BigComponents/CopyrightPanel"; | ||||
| import Toggle from "./Input/Toggle"; | ||||
| import Img from "./Base/Img"; | ||||
| import ValidatedTextField from "./Input/ValidatedTextField"; | ||||
| import Link from "./Base/Link"; | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     funcName: string, | ||||
|  | @ -53,7 +56,40 @@ export default class SpecialVisualizations { | |||
| 
 | ||||
|     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[] = | ||||
|             [ | ||||
|                 { | ||||
|  | @ -590,7 +626,7 @@ export default class SpecialVisualizations { | |||
|                     funcName: "open_in_iD", | ||||
|                     docs: "Opens the current view in the iD-editor", | ||||
|                     args: [], | ||||
|                     constr: (state, feature ) => { | ||||
|                     constr: (state, feature) => { | ||||
|                         return new OpenIdEditor(state, undefined, feature.data.id) | ||||
|                     } | ||||
|                 }, | ||||
|  | @ -611,29 +647,51 @@ export default class SpecialVisualizations { | |||
|                 }, | ||||
|                 { | ||||
|                     funcName: "close_note", | ||||
|                     docs: "Button to close a note", | ||||
|                     args:[ | ||||
|                     docs: "Button to close a note - eventually with a prefixed text", | ||||
|                     args: [ | ||||
|                         { | ||||
|                             name:"text", | ||||
|                             name: "text", | ||||
|                             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", | ||||
|                             defaultValue: "id" | ||||
|                         }, | ||||
|                         { | ||||
|                             name: "comment", | ||||
|                             doc: "Text to add onto the note when closing", | ||||
|                         } | ||||
|                     ], | ||||
|                     constr: (state, tags, args, guiState) => { | ||||
|                         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(() => { | ||||
|                             const id = tags.data[args[1] ?? "id"] | ||||
|                             if(state.featureSwitchIsTesting.data){ | ||||
|                             if (state.featureSwitchIsTesting.data) { | ||||
|                                 console.log("Not actually closing note...") | ||||
|                                 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( | ||||
|                             t.isClosed.SetClass("thanks"), | ||||
|  | @ -641,6 +699,151 @@ export default class SpecialVisualizations { | |||
|                             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; | ||||
|     } | ||||
| 
 | ||||
|   | ||||
|     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": [], | ||||
|     "sources": [] | ||||
|   }, | ||||
|   { | ||||
|     "path": "note.svg", | ||||
|     "license": "CC0", | ||||
|     "authors": [ | ||||
|       "Pieter Vander Vennet" | ||||
|     ], | ||||
|     "sources": [] | ||||
|   }, | ||||
|   { | ||||
|     "path": "osm-logo-us.svg", | ||||
|     "license": "Logo", | ||||
|  | @ -965,6 +973,14 @@ | |||
|     ], | ||||
|     "sources": [] | ||||
|   }, | ||||
|   { | ||||
|     "path": "resolved.svg", | ||||
|     "license": "CC0", | ||||
|     "authors": [ | ||||
|       "Pieter Vander Vennet" | ||||
|     ], | ||||
|     "sources": [] | ||||
|   }, | ||||
|   { | ||||
|     "path": "ring.svg", | ||||
|     "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, | ||||
|   "title": "Notes on OpenStreetMap", | ||||
|   "version": "0.1", | ||||
|   "description": "Notes from OpenStreetMap", | ||||
|   "icon": "./assets/themes/notes/resolved.svg", | ||||
|   "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/svg/resolved.svg", | ||||
|   "clustering": false, | ||||
|   "enableDownload": true, | ||||
|   "layers": [ | ||||
|  | @ -19,7 +19,7 @@ | |||
|       "name": { | ||||
|         "en": "OpenStreetMap notes" | ||||
|       }, | ||||
|       "description": "Notes on OpenStreetMap.org", | ||||
|       "description": "This layer shows notes on OpenStreetMap.", | ||||
|       "source": { | ||||
|         "osmTags": "id~*", | ||||
|         "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": [ | ||||
|         "_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": [ | ||||
|         { | ||||
|  | @ -52,18 +55,18 @@ | |||
|       "tagRenderings": [ | ||||
|         { | ||||
|           "id": "conversation", | ||||
|           "render": "{_conversation}" | ||||
|           "render": "{visualize_note_comments()}" | ||||
|         }, | ||||
|         { | ||||
|           "id": "date_created", | ||||
|           "id": "comment", | ||||
|           "render": "{add_note_comment()}" | ||||
|         }, | ||||
|         { | ||||
|           "id": "Spam", | ||||
|           "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>" | ||||
|           }, | ||||
|         { | ||||
|           "id": "close", | ||||
|           "render": "{close_note()}", | ||||
|           "condition": "closed_at=" | ||||
|           "condition": "_opened_by_anonymous_user=false" | ||||
|         } | ||||
|       ], | ||||
|       "mapRendering": [ | ||||
|  | @ -73,11 +76,11 @@ | |||
|             "centroid" | ||||
|           ], | ||||
|           "icon": { | ||||
|             "render": "./assets/themes/notes/note.svg", | ||||
|             "render": "./assets/svg/note.svg", | ||||
|             "mappings": [ | ||||
|               { | ||||
|                 "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; | ||||
| } | ||||
| 
 | ||||
| .mt-1 { | ||||
|   margin-top: 0.25rem; | ||||
| .mr-2 { | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .mr-4 { | ||||
|   margin-right: 1rem; | ||||
| } | ||||
| 
 | ||||
| .mt-1 { | ||||
|   margin-top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .mt-2 { | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
|  | @ -888,10 +892,6 @@ video { | |||
|   margin-left: 2rem; | ||||
| } | ||||
| 
 | ||||
| .mr-2 { | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .mb-10 { | ||||
|   margin-bottom: 2.5rem; | ||||
| } | ||||
|  | @ -1068,6 +1068,10 @@ video { | |||
|   width: 2rem; | ||||
| } | ||||
| 
 | ||||
| .w-6 { | ||||
|   width: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .w-0 { | ||||
|   width: 0px; | ||||
| } | ||||
|  | @ -1080,10 +1084,6 @@ video { | |||
|   width: 2.75rem; | ||||
| } | ||||
| 
 | ||||
| .w-6 { | ||||
|   width: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .w-16 { | ||||
|   width: 4rem; | ||||
| } | ||||
|  | @ -1283,26 +1283,31 @@ video { | |||
|   border-radius: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .rounded-xl { | ||||
|   border-radius: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .rounded-lg { | ||||
|   border-radius: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .rounded-xl { | ||||
|   border-radius: 0.75rem; | ||||
| .rounded-l { | ||||
|   border-top-left-radius: 0.25rem; | ||||
|   border-bottom-left-radius: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .border { | ||||
|   border-width: 1px; | ||||
| } | ||||
| 
 | ||||
| .border-4 { | ||||
|   border-width: 4px; | ||||
| } | ||||
| 
 | ||||
| .border-2 { | ||||
|   border-width: 2px; | ||||
| } | ||||
| 
 | ||||
| .border-4 { | ||||
|   border-width: 4px; | ||||
| } | ||||
| 
 | ||||
| .border-l-4 { | ||||
|   border-left-width: 4px; | ||||
| } | ||||
|  |  | |||
|  | @ -425,7 +425,12 @@ | |||
|   }, | ||||
|   "notes": { | ||||
|     "isClosed": "This note is resolved", | ||||
|     "closeNote":  | ||||
|       "Close this note" | ||||
|     "addCommentPlaceholder": "Add a comment...", | ||||
|     "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": { | ||||
|         "layers": { | ||||
|             "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", | ||||
|                 "tagRenderings": { | ||||
|                     "date_created": { | ||||
|                         "render": "Opened on {date_created}" | ||||
|                     "Spam": { | ||||
|                         "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…
	
	Add table
		Add a link
		
	
		Reference in a new issue