chore: automated housekeeping...

This commit is contained in:
Pieter Vander Vennet 2024-11-28 12:00:23 +01:00
parent 8ef7af613f
commit 00151afdea
114 changed files with 2857 additions and 2135 deletions

View file

@ -61,7 +61,7 @@ export default class FilteringFeatureSource implements FeatureSource {
selectedElement?.addCallback(() => this.update())
zoomlevel?.mapD(z => Math.floor(z)).addCallback(() => this.update())
zoomlevel?.mapD((z) => Math.floor(z)).addCallback(() => this.update())
this.update()
}
@ -76,7 +76,7 @@ export default class FilteringFeatureSource implements FeatureSource {
const newFeatures = (features ?? []).filter((f) => {
this.registerCallback(f.properties.id)
if(selectedElement === f.properties.id){
if (selectedElement === f.properties.id) {
return true
}

View file

@ -17,7 +17,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
private static defaultPanoramax = new AuthorizedPanoramax(
Constants.panoramax.url,
Constants.panoramax.token,
3000,
3000
)
public defaultKeyPrefixes: string[] = ["panoramax"]
@ -30,7 +30,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
location?: {
lon: number
lat: number
},
}
): BaseUIElement {
const p = new Panoramax(img.host)
return new Link(
@ -39,7 +39,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
imageId: img?.id,
location,
}),
true,
true
)
}
@ -141,7 +141,7 @@ export default class PanoramaxImageProvider extends ImageProvider {
img?.status !== undefined &&
img?.status !== "ready" &&
img?.status !== "broken" &&
img?.status !== "hidden",
img?.status !== "hidden"
)
}
@ -200,7 +200,7 @@ export class PanoramaxUploader implements ImageUploader {
author: string,
noblur: boolean = false,
sequenceId?: string,
datetime?: string,
datetime?: string
): Promise<{
key: string
value: string
@ -214,25 +214,34 @@ export class PanoramaxUploader implements ImageUploader {
const tags = await ExifReader.load(blob)
const [[latD], [latM], [latS, latSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLatitude?.value
>tags?.GPSLatitude?.value
const [[lonD], [lonM], [lonS, lonSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLongitude?.value
>tags?.GPSLongitude?.value
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
if (typeof exifLat === "number" && !isNaN(exifLat) && typeof exifLon === "number" && !isNaN(exifLon)
&& !(exifLat === 0 && exifLon === 0)) {
if (
typeof exifLat === "number" &&
!isNaN(exifLat) &&
typeof exifLon === "number" &&
!isNaN(exifLon) &&
!(exifLat === 0 && exifLon === 0)
) {
lat = exifLat
lon = exifLon
}
const [date, time] = tags.DateTime.value[0].split(" ")
const exifDatetime = new Date(date.replaceAll(":", "-") + "T" + time)
if(exifDatetime.getFullYear() === 1970){
if (exifDatetime.getFullYear() === 1970) {
// The data probably got reset to the epoch
// we don't use the value
console.log("Datetime from picture is probably invalid:", exifDatetime, "using 'now' instead")
}else{
console.log(
"Datetime from picture is probably invalid:",
exifDatetime,
"using 'now' instead"
)
} else {
datetime = exifDatetime.toISOString()
}
console.log("Tags are", tags)

View file

@ -563,13 +563,14 @@ export default class UserRelatedState {
return amendedPrefs
}
/**
* The disabled questions for this theme and layer
*/
public getThemeDisabled(themeId: string, layerId: string): UIEventSource<string[]> {
const flatSource = this.osmConnection.getPreference("disabled-questions-" + themeId + "-" + layerId, "[]")
const flatSource = this.osmConnection.getPreference(
"disabled-questions-" + themeId + "-" + layerId,
"[]"
)
return UIEventSource.asObject<string[]>(flatSource, [])
}
}

View file

@ -1,14 +1,42 @@
import { Utils } from "../../Utils"
/** This code is autogenerated - do not edit. Edit ./assets/layers/usersettings/usersettings.json instead */
export class ThemeMetaTagging {
public static readonly themeName = "usersettings"
public static readonly themeName = "usersettings"
public metaTaggging_for_usersettings(feat: {properties: Record<string, string>}) {
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_md', () => feat.properties._description.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)?.at(1) )
Utils.AddLazyProperty(feat.properties, '_d', () => feat.properties._description?.replace(/&lt;/g,'<')?.replace(/&gt;/g,'>') ?? '' )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate_a', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.href.match(/mastodon|en.osm.town/) !== null)[0]?.href }) (feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_link', () => (feat => {const e = document.createElement('div');e.innerHTML = feat.properties._d;return Array.from(e.getElementsByTagName("a")).filter(a => a.getAttribute("rel")?.indexOf('me') >= 0)[0]?.href})(feat) )
Utils.AddLazyProperty(feat.properties, '_mastodon_candidate', () => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a )
feat.properties['__current_backgroun'] = 'initial_value'
}
}
public metaTaggging_for_usersettings(feat: { properties: Record<string, string> }) {
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_md", () =>
feat.properties._description
.match(/\[[^\]]*\]\((.*(mastodon|en.osm.town).*)\).*/)
?.at(1)
)
Utils.AddLazyProperty(
feat.properties,
"_d",
() => feat.properties._description?.replace(/&lt;/g, "<")?.replace(/&gt;/g, ">") ?? ""
)
Utils.AddLazyProperty(feat.properties, "_mastodon_candidate_a", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.href.match(/mastodon|en.osm.town/) !== null
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(feat.properties, "_mastodon_link", () =>
((feat) => {
const e = document.createElement("div")
e.innerHTML = feat.properties._d
return Array.from(e.getElementsByTagName("a")).filter(
(a) => a.getAttribute("rel")?.indexOf("me") >= 0
)[0]?.href
})(feat)
)
Utils.AddLazyProperty(
feat.properties,
"_mastodon_candidate",
() => feat.properties._mastodon_candidate_md ?? feat.properties._mastodon_candidate_a
)
feat.properties["__current_backgroun"] = "initial_value"
}
}

View file

@ -504,14 +504,8 @@ export class TagUtils {
* regex.matchesProperties({maxspeed: "50 mph"}) // => true
*/
public static Tag(
json: string,
context?: string | ConversionContext
): FlatTag;
public static Tag(
json: TagConfigJson,
context?: string | ConversionContext
): TagsFilterClosed;
public static Tag(json: string, context?: string | ConversionContext): FlatTag
public static Tag(json: TagConfigJson, context?: string | ConversionContext): TagsFilterClosed
public static Tag(
json: TagConfigJson,
context: string | ConversionContext = ""

View file

@ -210,7 +210,11 @@ export default class FilteredLayer {
* - the specified 'global filters'
* - the 'isShown'-filter set by the layer
*/
public isShown(properties: Record<string, string>, globalFilters?: GlobalFilter[], zoomlevel?: number): boolean {
public isShown(
properties: Record<string, string>,
globalFilters?: GlobalFilter[],
zoomlevel?: number
): boolean {
if (properties._deleted === "yes") {
return false
}
@ -219,7 +223,7 @@ export default class FilteredLayer {
if (neededTags !== undefined) {
const doesMatch = neededTags.matchesProperties(properties)
if (globalFilter.forceShowOnMatch) {
if(doesMatch){
if (doesMatch) {
return true
}
} else if (!doesMatch) {
@ -241,7 +245,10 @@ export default class FilteredLayer {
}
}
if(zoomlevel !== undefined && (this.layerDef.minzoom > zoomlevel || this.layerDef.minzoomVisible < zoomlevel)){
if (
zoomlevel !== undefined &&
(this.layerDef.minzoom > zoomlevel || this.layerDef.minzoomVisible < zoomlevel)
) {
return false
}

View file

@ -20,10 +20,14 @@ export interface MapProperties {
readonly allowRotating: UIEventSource<true | boolean>
readonly rotation: UIEventSource<number>
readonly pitch: UIEventSource<number>
readonly lastClickLocation: Store<{ lon: number; lat: number ; /**
readonly lastClickLocation: Store<{
lon: number
lat: number
/**
* The nearest feature from a MapComplete layer
*/
nearestFeature?: Feature}>
nearestFeature?: Feature
}>
readonly allowZooming: UIEventSource<true | boolean>
readonly useTerrain: Store<boolean>
readonly showScale: UIEventSource<boolean>

View file

@ -41,7 +41,7 @@ export default class PointRenderingConfig extends WithContextLoader {
"end",
"projected_centerpoint",
"polygon_centroid",
"waypoints"
"waypoints",
])
public readonly location: Set<
| "point"

View file

@ -88,7 +88,7 @@ export default class TagRenderingConfig {
| string
| TagRenderingConfigJson
| (QuestionableTagRenderingConfigJson & { questionHintIsMd?: boolean }),
context?: string,
context?: string
) {
let json = <string | QuestionableTagRenderingConfigJson>config
if (json === undefined) {
@ -145,22 +145,22 @@ export default class TagRenderingConfig {
this.questionhint = Translations.T(json.questionHint, translationKey + ".questionHint")
this.questionHintIsMd = json["questionHintIsMd"] ?? false
this.description = Translations.T(json.description, translationKey + ".description")
if(json.onSoftDelete && !Array.isArray(json.onSoftDelete)){
throw context+".onSoftDelete Not an array: "+typeof json.onSoftDelete
if (json.onSoftDelete && !Array.isArray(json.onSoftDelete)) {
throw context + ".onSoftDelete Not an array: " + typeof json.onSoftDelete
}
this.onSoftDelete = json.onSoftDelete?.map(t => {
this.onSoftDelete = json.onSoftDelete?.map((t) => {
const tag = TagUtils.Tag(t, context)
if (tag instanceof RegexTag) {
throw context+".onSoftDelete Invalid onSoftDelete: cannot upload tag " + t
throw context + ".onSoftDelete Invalid onSoftDelete: cannot upload tag " + t
}
if (tag instanceof ComparingTag) {
throw context+".onSoftDelete Invalid onSoftDelete: cannot upload tag " + t
throw context + ".onSoftDelete Invalid onSoftDelete: cannot upload tag " + t
}
return tag
})
this.editButtonAriaLabel = Translations.T(
json.editButtonAriaLabel,
translationKey + ".editButtonAriaLabel",
translationKey + ".editButtonAriaLabel"
)
this.condition = TagUtils.Tag(json.condition ?? { and: [] }, `${context}.condition`)
@ -176,7 +176,7 @@ export default class TagRenderingConfig {
}
this.metacondition = TagUtils.Tag(
json.metacondition ?? { and: [] },
`${context}.metacondition`,
`${context}.metacondition`
)
if (json.freeform) {
if (
@ -194,7 +194,7 @@ export default class TagRenderingConfig {
}, perhaps you meant ${Utils.sortedByLevenshteinDistance(
json.freeform.key,
<any>Validators.availableTypes,
(s) => <any>s,
(s) => <any>s
)}`
}
const type: ValidatorType = <any>json.freeform.type ?? "string"
@ -216,7 +216,7 @@ export default class TagRenderingConfig {
placeholder,
addExtraTags:
json.freeform.addExtraTags?.map((tg, i) =>
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`),
TagUtils.ParseUploadableTag(tg, `${context}.extratag[${i}]`)
) ?? [],
inline: json.freeform.inline ?? false,
default: json.freeform.default,
@ -282,8 +282,8 @@ export default class TagRenderingConfig {
context,
this.multiAnswer,
this.question !== undefined,
commonIconSize,
),
commonIconSize
)
)
} else {
this.mappings = []
@ -309,7 +309,7 @@ export default class TagRenderingConfig {
for (const expectedKey of keys) {
if (usedKeys.indexOf(expectedKey) < 0) {
const msg = `${context}.mappings[${i}]: This mapping only defines values for ${usedKeys.join(
", ",
", "
)}, but it should also give a value for ${expectedKey}`
this.configuration_warnings.push(msg)
}
@ -356,7 +356,7 @@ export default class TagRenderingConfig {
context: string,
multiAnswer?: boolean,
isQuestionable?: boolean,
commonSize: string = "small",
commonSize: string = "small"
): Mapping {
const ctx = `${translationKey}.mappings.${i}`
if (mapping.if === undefined) {
@ -365,7 +365,7 @@ export default class TagRenderingConfig {
if (mapping.then === undefined) {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: no 'then'-clause found. You might have typed 'render' instead of 'then', change it in ${JSON.stringify(
mapping,
mapping
)}`
}
throw `${ctx}: Invalid mapping: no 'then'-clause found in ${JSON.stringify(mapping)}`
@ -376,7 +376,7 @@ export default class TagRenderingConfig {
if (mapping["render"] !== undefined) {
throw `${ctx}: Invalid mapping: a 'render'-key is present, this is probably a bug: ${JSON.stringify(
mapping,
mapping
)}`
}
if (typeof mapping.if !== "string" && mapping.if["length"] !== undefined) {
@ -387,8 +387,14 @@ export default class TagRenderingConfig {
throw `${ctx}.addExtraTags: expected a list, but got a ${typeof mapping.addExtraTags}`
}
if (mapping.addExtraTags !== undefined && multiAnswer) {
const usedKeys = mapping.addExtraTags?.flatMap((et) => TagUtils.Tag(et, context).usedKeys())
if (usedKeys.some((key) => TagUtils.Tag(mapping.if, context).usedKeys().indexOf(key) > 0)) {
const usedKeys = mapping.addExtraTags?.flatMap((et) =>
TagUtils.Tag(et, context).usedKeys()
)
if (
usedKeys.some(
(key) => TagUtils.Tag(mapping.if, context).usedKeys().indexOf(key) > 0
)
) {
throw `${ctx}: Invalid mapping: got a multi-Answer with addExtraTags which also modifies one of the keys; this is not allowed`
}
}
@ -399,11 +405,11 @@ export default class TagRenderingConfig {
} else if (mapping.hideInAnswer !== undefined) {
hideInAnswer = TagUtils.Tag(
mapping.hideInAnswer,
`${context}.mapping[${i}].hideInAnswer`,
`${context}.mapping[${i}].hideInAnswer`
)
}
const addExtraTags = (mapping.addExtraTags ?? []).map((str, j) =>
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`),
TagUtils.SimpleTag(str, `${ctx}.addExtraTags[${j}]`)
)
if (hideInAnswer === true && addExtraTags.length > 0) {
throw `${ctx}: Invalid mapping: 'hideInAnswer' is set to 'true', but 'addExtraTags' is enabled as well. This means that extra tags will be applied if this mapping is chosen as answer, but it cannot be chosen as answer. This either indicates a thought error or obsolete code that must be removed.`
@ -499,7 +505,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValues(
tags: Record<string, string>,
tags: Record<string, string>
): { then: Translation; icon?: string; iconClass?: string }[] {
if (!this.multiAnswer) {
return [this.GetRenderValueWithImage(tags)]
@ -522,7 +528,7 @@ export default class TagRenderingConfig {
return mapping
}
return undefined
}),
})
)
if (freeformKeyDefined && tags[this.freeform.key] !== undefined) {
@ -530,7 +536,7 @@ export default class TagRenderingConfig {
applicableMappings
?.flatMap((m) => m.if?.usedTags() ?? [])
?.filter((kv) => kv.key === this.freeform.key)
?.map((kv) => kv.value),
?.map((kv) => kv.value)
)
const freeformValues = tags[this.freeform.key].split(";")
@ -539,7 +545,7 @@ export default class TagRenderingConfig {
applicableMappings.push({
then: new TypedTranslation<object>(
this.render.replace("{" + this.freeform.key + "}", leftover).translations,
this.render.context,
this.render.context
),
})
}
@ -557,7 +563,7 @@ export default class TagRenderingConfig {
* @constructor
*/
public GetRenderValueWithImage(
tags: Record<string, string>,
tags: Record<string, string>
): { then: TypedTranslation<any>; icon?: string; iconClass?: string } | undefined {
if (this.condition !== undefined) {
if (!this.condition.matchesProperties(tags)) {
@ -626,7 +632,7 @@ export default class TagRenderingConfig {
const answerMappings = this.mappings?.filter((m) => m.hideInAnswer !== true)
if (key === undefined) {
const values: { k: string; v: string }[][] = Utils.NoNull(
answerMappings?.map((m) => m.if.asChange({})) ?? [],
answerMappings?.map((m) => m.if.asChange({})) ?? []
)
if (values.length === 0) {
return
@ -644,15 +650,15 @@ export default class TagRenderingConfig {
return {
key: commonKey,
values: Utils.NoNull(
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v),
values.map((arr) => arr.filter((item) => item.k === commonKey)[0]?.v)
),
}
}
let values = Utils.NoNull(
answerMappings?.map(
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v,
) ?? [],
(m) => m.if.asChange({}).filter((item) => item.k === key)[0]?.v
) ?? []
)
if (values.length === undefined) {
values = undefined
@ -716,7 +722,7 @@ export default class TagRenderingConfig {
freeformValue: string | undefined,
singleSelectedMapping: number,
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>,
currentProperties: Record<string, string>
): UploadableTag {
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
@ -791,7 +797,7 @@ export default class TagRenderingConfig {
new And([
new Tag(this.freeform.key, freeformValue),
...(this.freeform.addExtraTags ?? []),
]),
])
)
}
const and = TagUtils.FlattenMultiAnswer([...selectedMappings, ...unselectedMappings])
@ -861,11 +867,11 @@ export default class TagRenderingConfig {
}
const msgs: string[] = [
icon +
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
" " +
"*" +
m.then.textFor(lang) +
"* is shown if with " +
m.if.asHumanString(true, false, {}),
]
if (m.hideInAnswer === true) {
@ -874,11 +880,11 @@ export default class TagRenderingConfig {
if (m.ifnot !== undefined) {
msgs.push(
"Unselecting this answer will add " +
m.ifnot.asHumanString(true, false, {}),
m.ifnot.asHumanString(true, false, {})
)
}
return msgs.join(". ")
}),
})
)
}
@ -887,7 +893,7 @@ export default class TagRenderingConfig {
const conditionAsLink = (<TagsFilter>this.condition.optimize()).asHumanString(
true,
false,
{},
{}
)
condition =
"This tagrendering is only visible in the popup if the following condition is met: " +
@ -921,7 +927,7 @@ export default class TagRenderingConfig {
this.metacondition,
this.condition,
this.freeform?.key ? new RegexTag(this.freeform?.key, /.*/) : undefined,
this.invalidValues,
this.invalidValues
)
for (const m of this.mappings ?? []) {
tags.push(m.if)
@ -943,7 +949,7 @@ export default class TagRenderingConfig {
*/
public removeToSetUnknown(
partOfLayer: LayerConfig,
currentTags: Record<string, string>,
currentTags: Record<string, string>
): string[] | undefined {
if (!partOfLayer?.source || !currentTags) {
return
@ -991,7 +997,7 @@ export class TagRenderingConfigUtils {
public static withNameSuggestionIndex(
config: TagRenderingConfig,
tags: UIEventSource<Record<string, string>>,
feature?: Feature,
feature?: Feature
): Store<TagRenderingConfig> {
const isNSI = NameSuggestionIndex.supportedTypes().indexOf(config.freeform?.key) >= 0
if (!isNSI) {
@ -1009,8 +1015,8 @@ export class TagRenderingConfigUtils {
tags,
country.split(";"),
center,
{ sortByFrequency: true },
),
{ sortByFrequency: true }
)
)
})
return extraMappings.map((extraMappings) => {

View file

@ -178,7 +178,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.map = new UIEventSource<MlMap>(undefined)
const geolocationState = new GeoLocationState()
const initial = new InitialMapPositioning(layout, geolocationState)
this.mapProperties = new MapLibreAdaptor(this.map, initial, {correctClick: 20})
this.mapProperties = new MapLibreAdaptor(this.map, initial, { correctClick: 20 })
this.featureSwitchIsTesting = this.featureSwitches.featureSwitchIsTesting
this.featureSwitchUserbadge = this.featureSwitches.featureSwitchEnableLogin
@ -557,9 +557,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
})
}
private setSelectedElement(feature: Feature){
private setSelectedElement(feature: Feature) {
const current = this.selectedElement.data
if(current?.properties?.id !== undefined && current.properties.id === feature.properties.id ){
if (
current?.properties?.id !== undefined &&
current.properties.id === feature.properties.id
) {
console.log("Not setting selected, same id", current, feature)
return // already set
}
@ -1002,13 +1005,12 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.userRelatedState.recentlyVisitedSearch.add(r)
})
this.mapProperties.lastClickLocation.addCallbackD(lastClick => {
if(lastClick.mode !== "left" || !lastClick.nearestFeature){
this.mapProperties.lastClickLocation.addCallbackD((lastClick) => {
if (lastClick.mode !== "left" || !lastClick.nearestFeature) {
return
}
const f = lastClick.nearestFeature
this.setSelectedElement(f)
})
this.userRelatedState.showScale.addCallbackAndRun((showScale) => {

View file

@ -50,7 +50,7 @@
}
:global(.dots-menu > path) {
fill: var(--button-background-hover);
fill: var(--button-background-hover);
transition: fill 350ms linear;
cursor: pointer;
}
@ -73,9 +73,9 @@
background-color: white;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background {
transition: background-color 150ms linear;
}
.transition-background.collapsed {
background-color: #00000000;

View file

@ -65,7 +65,10 @@
{#if $value.length > 0}
<Backspace
on:click={(e) =>{ value.set("") ; e.preventDefault()}}
on:click={(e) => {
value.set("")
e.preventDefault()
}}
color="var(--button-background)"
class="mr-3 h-6 w-6 cursor-pointer"
/>

View file

@ -28,7 +28,7 @@
(s) =>
(s === "yes" &&
state?.userRelatedState?.osmConnection?.userDetails?.data?.csCount >=
Constants.userJourney.tagsVisibleAt) ||
Constants.userJourney.tagsVisibleAt) ||
s === "always" ||
s === "full"
)
@ -64,9 +64,9 @@
<Tr t={filteredLayer.layerDef.name} />
{#if $zoomlevel < layer.minzoom}
<span class="alert">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span>
<span class="alert">
<Tr t={Translations.t.general.layerSelection.zoomInToSeeThisLayer} />
</span>
{/if}
</Checkbox>
{/if}

View file

@ -65,22 +65,24 @@
export let onlyLink: boolean
const t = Translations.t.general.menu
let shown = new UIEventSource(state.guistate.pageStates.menu.data || !onlyLink)
state.guistate.pageStates.menu.addCallback(isShown => {
if(!onlyLink){
state.guistate.pageStates.menu.addCallback((isShown) => {
if (!onlyLink) {
return true
}
if(isShown){
if (isShown) {
shown.setData(true)
}else{
} else {
Utils.waitFor(250).then(() => {
shown.setData(state.guistate.pageStates.menu.data)
})
}
})
</script>
<div class="low-interaction flex h-screen flex-col gap-y-2 overflow-y-auto p-2 sm:gap-y-3 sm:p-3" class:hidden={!$shown}>
<div
class="low-interaction flex h-screen flex-col gap-y-2 overflow-y-auto p-2 sm:gap-y-3 sm:p-3"
class:hidden={!$shown}
>
<div class="flex justify-between">
<h2>
<Tr t={t.title} />

View file

@ -56,7 +56,7 @@
/**
* Reuse a point if the clicked location is within this amount of meter
*/
export let snapTolerance: number = 5
export let snapTolerance: number = 5
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let adaptor = new MapLibreAdaptor(map, mapProperties)
@ -101,7 +101,8 @@
})
let id = 0
adaptor.lastClickLocation.addCallbackD(({ lon, lat }) => {
let projected: Feature<Point, {index:number, id?: number, reuse?: string}> = GeoOperations.nearestPoint(wayGeojson, [lon, lat])
let projected: Feature<Point, { index: number; id?: number; reuse?: string }> =
GeoOperations.nearestPoint(wayGeojson, [lon, lat])
console.log("Added splitpoint", projected, id)
@ -110,36 +111,36 @@
const i = projected.properties.index
const p = projected.geometry.coordinates
const way = wayGeojson.geometry.coordinates
const nextPoint = <[number,number]> way[i + 1]
const nextPoint = <[number, number]>way[i + 1]
const nextDistance = GeoOperations.distanceBetween(nextPoint, p)
const previousPoint = <[number,number]> way[i]
const previousPoint = <[number, number]>way[i]
const previousDistance = GeoOperations.distanceBetween(previousPoint, p)
console.log("ND", nextDistance, "PD", previousDistance)
if(nextDistance <= snapTolerance && previousDistance >= nextDistance){
if (nextDistance <= snapTolerance && previousDistance >= nextDistance) {
projected = {
type:"Feature",
type: "Feature",
geometry: {
type:"Point",
coordinates: nextPoint
type: "Point",
coordinates: nextPoint,
},
properties: {
index: i+1,
reuse: "yes"
}
index: i + 1,
reuse: "yes",
},
}
}
if (previousDistance <= snapTolerance && previousDistance < nextDistance){
if (previousDistance <= snapTolerance && previousDistance < nextDistance) {
projected = {
type:"Feature",
type: "Feature",
geometry: {
type:"Point",
coordinates: previousPoint
type: "Point",
coordinates: previousPoint,
},
properties: {
index: i,
reuse: "yes"
}
reuse: "yes",
},
}
}

View file

@ -38,7 +38,7 @@
let errors = new UIEventSource<Translation[]>([])
async function handleFiles(files: FileList, ignoreGps: boolean= false) {
async function handleFiles(files: FileList, ignoreGps: boolean = false) {
const errs = []
for (let i = 0; i < files.length; i++) {
const file = files.item(i)
@ -102,7 +102,11 @@
capture="environment"
cls="button border-2 flex flex-col"
multiple={true}
on:submit={(e) =>{ handleFiles(e.detail) ; e.preventDefault(); e.stopPropagation()}}
on:submit={(e) => {
handleFiles(e.detail)
e.preventDefault()
e.stopPropagation()
}}
>
<div class="flex w-full items-center justify-center text-2xl">
{#if image !== undefined}
@ -114,7 +118,7 @@
{labelText}
{:else}
<div class="flex flex-col">
<Tr t={t.addPicture}/>
<Tr t={t.addPicture} />
{#if noBlur}
<span class="subtle text-sm">
<Tr t={t.upload.noBlur} />
@ -128,9 +132,13 @@
accept=".jpg, .jpeg"
cls="flex justify-center md:hidden button"
multiple={true}
on:submit={(e) =>{ return handleFiles(e.detail, true) ; e.preventDefault(); e.stopPropagation()}}
on:submit={(e) => {
return handleFiles(e.detail, true)
e.preventDefault()
e.stopPropagation()
}}
>
<Tr t={t.selectFile}/>
<Tr t={t.selectFile} />
</FileSelector>
<div class="subtle text-xs italic">
<Tr t={Translations.t.general.attribution.panoramaxLicenseCCBYSA} />

View file

@ -1,5 +1,10 @@
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import maplibregl, { Map as MLMap, Map as MlMap, ScaleControl, SourceSpecification } from "maplibre-gl"
import maplibregl, {
Map as MLMap,
Map as MlMap,
ScaleControl,
SourceSpecification,
} from "maplibre-gl"
import { RasterLayerPolygon } from "../../Models/RasterLayers"
import { Utils } from "../../Utils"
import { BBox } from "../../Logic/BBox"
@ -43,10 +48,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
readonly allowRotating: UIEventSource<true | boolean | undefined>
readonly allowZooming: UIEventSource<true | boolean | undefined>
readonly lastClickLocation: Store<
undefined | { lon: number; lat: number; mode: "left" | "right" | "middle" , /**
* The nearest feature from a MapComplete layer
*/
nearestFeature?: Feature }
| undefined
| {
lon: number
lat: number
mode: "left" | "right" | "middle"
/**
* The nearest feature from a MapComplete layer
*/
nearestFeature?: Feature
}
>
readonly minzoom: UIEventSource<number>
readonly maxzoom: UIEventSource<number>
@ -64,9 +75,13 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
private readonly _maplibreMap: Store<MLMap>
constructor(maplibreMap: Store<MLMap>, state?: Partial<MapProperties>, options?:{
correctClick?: number
}) {
constructor(
maplibreMap: Store<MLMap>,
state?: Partial<MapProperties>,
options?: {
correctClick?: number
}
) {
if (!MapLibreAdaptor.pmtilesInited) {
maplibregl.addProtocol("pmtiles", new Protocol().tile)
MapLibreAdaptor.pmtilesInited = true
@ -106,7 +121,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const lastClickLocation = new UIEventSource<{
lat: number
lon: number
mode: "left" | "right" | "middle",
mode: "left" | "right" | "middle"
nearestFeature?: Feature
}>(undefined)
this.lastClickLocation = lastClickLocation
@ -126,30 +141,32 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
const mouseEvent: MouseEvent = e.originalEvent
mode = mode ?? clickmodes[mouseEvent.button]
let nearestFeature: Feature = undefined
if(options?.correctClick && maplibreMap.data){
if (options?.correctClick && maplibreMap.data) {
const map = maplibreMap.data
const point = e.point
const buffer = options?.correctClick
const features = map.queryRenderedFeatures([
[point.x - buffer, point.y - buffer],
[point.x + buffer, point.y + buffer]
]).filter(f => f.source.startsWith("mapcomplete_"))
if(features.length === 1){
const features = map
.queryRenderedFeatures([
[point.x - buffer, point.y - buffer],
[point.x + buffer, point.y + buffer],
])
.filter((f) => f.source.startsWith("mapcomplete_"))
if (features.length === 1) {
nearestFeature = features[0]
}else{
} else {
let nearestD: number = undefined
for (const feature of features) {
let d: number // in meter
if(feature.geometry.type === "LineString"){
const way = <Feature<LineString>> feature
const lngLat:[number,number] = [e.lngLat.lng, e.lngLat.lat]
if (feature.geometry.type === "LineString") {
const way = <Feature<LineString>>feature
const lngLat: [number, number] = [e.lngLat.lng, e.lngLat.lat]
const p = GeoOperations.nearestPoint(way, lngLat)
console.log(">>>",p, way, lngLat)
if(!p){
console.log(">>>", p, way, lngLat)
if (!p) {
continue
}
d = p.properties.dist * 1000
if(nearestFeature === undefined || d < nearestD){
if (nearestFeature === undefined || d < nearestD) {
nearestFeature = way
nearestD = d
}
@ -158,7 +175,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
}
}
lastClickLocation.setData({ lon, lat, mode, nearestFeature })
}
maplibreMap.addCallbackAndRunD((map) => {

View file

@ -39,7 +39,7 @@ class PointRenderingLayer {
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void,
selectedElement?: Store<{ properties: { id?: string } }>,
selectedElement?: Store<{ properties: { id?: string } }>
) {
this._visibility = visibility
this._config = config
@ -98,7 +98,7 @@ class PointRenderingLayer {
" while rendering",
location,
"of",
this._config,
this._config
)
}
const id = feature.properties.id + "-" + location
@ -110,7 +110,10 @@ class PointRenderingLayer {
this.addPoint(feature, <[number, number]>loc)
}
}
if (feature.geometry.type === "MultiLineString" || feature.geometry.type === "Polygon") {
if (
feature.geometry.type === "MultiLineString" ||
feature.geometry.type === "Polygon"
) {
for (const coors of feature.geometry.coordinates) {
for (const loc of coors) {
this.addPoint(feature, <[number, number]>loc)
@ -122,7 +125,7 @@ class PointRenderingLayer {
const loc = GeoOperations.featureToCoordinateWithRenderingType(
<any>feature,
location,
location
)
if (loc === undefined) {
continue
@ -251,7 +254,7 @@ class LineRenderingLayer {
config: LineRenderingConfig,
visibility?: Store<boolean>,
fetchStore?: (id: string) => Store<Record<string, string>>,
onClick?: (feature: Feature) => void,
onClick?: (feature: Feature) => void
) {
this._layername = layername
this._map = map
@ -271,7 +274,7 @@ class LineRenderingLayer {
private async addSymbolLayer(
sourceId: string,
imageAlongWay: { if?: TagsFilter; then: string }[],
imageAlongWay: { if?: TagsFilter; then: string }[]
) {
const map = this._map
await Promise.allSettled(
@ -301,7 +304,7 @@ class LineRenderingLayer {
spec.filter = filter
}
map.addLayer(spec)
}),
})
)
}
@ -311,7 +314,7 @@ class LineRenderingLayer {
* @private
*/
private calculatePropsFor(
properties: Record<string, string>,
properties: Record<string, string>
): Partial<Record<(typeof LineRenderingLayer.lineConfigKeys)[number], string>> {
const config = this._config
@ -393,7 +396,7 @@ class LineRenderingLayer {
} catch (e) {
console.error(
`Invalid dasharray in layer ${this._layername}:`,
this._config.dashArray,
this._config.dashArray
)
}
}
@ -410,11 +413,11 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id: feature.properties.id },
this.calculatePropsFor(feature.properties),
this.calculatePropsFor(feature.properties)
)
}
if(this._onClick){
if (this._onClick) {
map.on("click", linelayer, (e) => {
// line-layer-listener
e.originalEvent["consumed"] = true
@ -455,7 +458,7 @@ class LineRenderingLayer {
"Error while setting visibility of layers ",
linelayer,
polylayer,
e,
e
)
}
})
@ -476,7 +479,7 @@ class LineRenderingLayer {
console.trace(
"Got a feature without ID; this causes rendering bugs:",
feature,
"from",
"from"
)
LineRenderingLayer.missingIdTriggered = true
}
@ -488,7 +491,7 @@ class LineRenderingLayer {
if (this._fetchStore === undefined) {
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(feature.properties),
this.calculatePropsFor(feature.properties)
)
} else {
const tags = this._fetchStore(id)
@ -505,7 +508,7 @@ class LineRenderingLayer {
}
map.setFeatureState(
{ source: this._layername, id },
this.calculatePropsFor(properties),
this.calculatePropsFor(properties)
)
})
}
@ -529,7 +532,7 @@ export default class ShowDataLayer {
layer: LayerConfig
drawMarkers?: true | boolean
drawLines?: true | boolean
},
}
) {
this._options = options
this.onDestroy.push(map.addCallbackAndRunD((map) => this.initDrawFeatures(map)))
@ -539,7 +542,7 @@ export default class ShowDataLayer {
mlmap: UIEventSource<MlMap>,
features: FeatureSource,
layers: LayerConfig[],
options?: Partial<ShowDataLayerOptions>,
options?: Partial<ShowDataLayerOptions>
) {
const perLayer: PerLayerFeatureSourceSplitter<FeatureSourceForLayer> =
new PerLayerFeatureSourceSplitter(
@ -547,7 +550,7 @@ export default class ShowDataLayer {
features,
{
constructStore: (features, layer) => new SimpleFeatureSource(layer, features),
},
}
)
if (options?.zoomToFeatures) {
options.zoomToFeatures = false
@ -571,7 +574,7 @@ export default class ShowDataLayer {
public static showRange(
map: Store<MlMap>,
features: FeatureSource,
doShowLayer?: Store<boolean>,
doShowLayer?: Store<boolean>
): ShowDataLayer {
return new ShowDataLayer(map, {
layer: ShowDataLayer.rangeLayer,
@ -580,8 +583,7 @@ export default class ShowDataLayer {
})
}
public destruct() {
}
public destruct() {}
private static zoomToCurrentFeatures(map: MlMap, features: Feature[]) {
if (!features || !map || features.length == 0) {
@ -605,8 +607,8 @@ export default class ShowDataLayer {
this._options.layer.title === undefined
? undefined
: (feature: Feature) => {
selectedElement?.setData(feature)
}
selectedElement?.setData(feature)
}
}
if (this._options.drawLines !== false) {
for (let i = 0; i < this._options.layer.lineRendering.length; i++) {
@ -618,7 +620,7 @@ export default class ShowDataLayer {
lineRenderingConfig,
doShowLayer,
fetchStore,
onClick,
onClick
)
this.onDestroy.push(l.destruct)
}
@ -634,13 +636,13 @@ export default class ShowDataLayer {
doShowLayer,
fetchStore,
onClick,
selectedElement,
selectedElement
)
}
}
if (this._options.zoomToFeatures) {
features.features.addCallbackAndRunD((features) =>
ShowDataLayer.zoomToCurrentFeatures(map, features),
ShowDataLayer.zoomToCurrentFeatures(map, features)
)
}
}

View file

@ -61,11 +61,11 @@
const changedProperties = TagUtils.changeAsProperties(selectedTags.asChange(tags?.data ?? {}))
const deleteReason = changedProperties[DeleteConfig.deleteReasonKey]
if (deleteReason) {
let softDeletionTags: UploadableTag
if(hasSoftDeletion){
softDeletionTags = new And([deleteConfig.softDeletionTags,
...layer.tagRenderings.flatMap(tr => tr.onSoftDelete ?? []),
if (hasSoftDeletion) {
softDeletionTags = new And([
deleteConfig.softDeletionTags,
...layer.tagRenderings.flatMap((tr) => tr.onSoftDelete ?? []),
])
}

View file

@ -8,9 +8,11 @@
* Shows _all_ disabled questions
*/
export let state
let layers = state.layout.layers.filter(l => l.isNormal())
let layers = state.layout.layers.filter((l) => l.isNormal())
let allDisabled = Stores.concat<string>(layers.map(l => state.userRelatedState.getThemeDisabled(state.layout.id, l.id))).map(l => [].concat(...l))
let allDisabled = Stores.concat<string>(
layers.map((l) => state.userRelatedState.getThemeDisabled(state.layout.id, l.id))
).map((l) => [].concat(...l))
const t = Translations.t.general.questions
</script>

View file

@ -1,33 +1,33 @@
<script lang="ts">/**
* Gives an overview of questions which are disabled for the given theme
*/
import UserRelatedState from "../../Logic/State/UserRelatedState"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ThemeViewState from "../../Models/ThemeViewState"
import Tr from "../Base/Tr.svelte"
import { Translation } from "../i18n/Translation"
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
import ToSvelte from "../Base/ToSvelte.svelte"
<script lang="ts">
/**
* Gives an overview of questions which are disabled for the given theme
*/
import UserRelatedState from "../../Logic/State/UserRelatedState"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import ThemeViewState from "../../Models/ThemeViewState"
import Tr from "../Base/Tr.svelte"
import { Translation } from "../i18n/Translation"
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
import ToSvelte from "../Base/ToSvelte.svelte"
export let layer: LayerConfig
export let state: ThemeViewState
export let layer: LayerConfig
export let state: ThemeViewState
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id)
let disabledQuestions = state.userRelatedState.getThemeDisabled(state.layout.id, layer.id)
function getQuestion(id: string): Translation {
return layer.tagRenderings.find(q => q.id === id).question.Subs({})
}
function getQuestion(id: string): Translation {
return layer.tagRenderings.find((q) => q.id === id).question.Subs({})
}
function enable(idToEnable: string) {
const newList = disabledQuestions.data.filter(id => id !== idToEnable)
disabledQuestions.set(newList)
}
function enable(idToEnable: string) {
const newList = disabledQuestions.data.filter((id) => id !== idToEnable)
disabledQuestions.set(newList)
}
</script>
{#if $disabledQuestions.length > 0}
<div class="low-interaction p-2">
<h4 class="flex my-2">
<h4 class="my-2 flex">
<div class="no-image-background block h-6 w-6">
<ToSvelte construct={() => layer.defaultIcon()} />
</div>
@ -37,7 +37,7 @@ function enable(idToEnable: string) {
{#each $disabledQuestions as id}
<button class="badge button-unstyled" on:click={() => enable(id)}>
<Tr cls="ml-2" t={getQuestion(id)} />
<XMarkIcon class="w-4 h-4 mr-2" />
<XMarkIcon class="mr-2 h-4 w-4" />
</button>
{/each}
</div>

View file

@ -88,7 +88,13 @@
{:else if step === "splitting"}
<div class="interactive border-interactive flex flex-col p-2">
<div class="h-80 w-full">
<WaySplitMap {state} {splitPoints} {osmWay} {snapTolerance} mapProperties={{rasterLayer: state.mapProperties.rasterLayer}}/>
<WaySplitMap
{state}
{splitPoints}
{osmWay}
{snapTolerance}
mapProperties={{ rasterLayer: state.mapProperties.rasterLayer }}
/>
</div>
<div class="flex w-full flex-wrap-reverse md:flex-nowrap">
<BackButton

View file

@ -45,16 +45,15 @@
}
const baseQuestions = (layer?.tagRenderings ?? [])?.filter(
(tr) => allowed(tr.labels) && tr.question !== undefined,
(tr) => allowed(tr.labels) && tr.question !== undefined
)
/**
* Ids of skipped questions
*/
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>())
let layerDisabledForTheme = state.userRelatedState.getThemeDisabled(state.theme.id, layer.id)
layerDisabledForTheme.addCallbackAndRunD(disabled => {
layerDisabledForTheme.addCallbackAndRunD((disabled) => {
skippedQuestions.set(new Set(disabled.concat(Array.from(skippedQuestions.data))))
})
let questionboxElem: HTMLDivElement
@ -78,10 +77,10 @@
}
return questionsToAsk
},
[skippedQuestions],
[skippedQuestions]
)
let firstQuestion: UIEventSource<TagRenderingConfig> = new UIEventSource<TagRenderingConfig>(
undefined,
undefined
)
let allQuestionsToAsk: UIEventSource<TagRenderingConfig[]> = new UIEventSource<
TagRenderingConfig[]
@ -106,7 +105,6 @@
let loginEnabled = state.featureSwitches.featureSwitchEnableLogin
let debug = state.featureSwitches.featureSwitchIsDebugging
function skip(question: { id: string }, didAnswer: boolean = false) {
skippedQuestions.data.add(question.id) // Must use ID, the config object might be a copy of the original
skippedQuestions.ping()
@ -131,13 +129,7 @@
{#if $showAllQuestionsAtOnce}
<div class="flex flex-col gap-y-1">
{#each $allQuestionsToAsk as question (question.id)}
<TagRenderingQuestionDynamic
config={question}
{tags}
{selectedElement}
{state}
{layer}
/>
<TagRenderingQuestionDynamic config={question} {tags} {selectedElement} {state} {layer} />
{/each}
</div>
{:else if $firstQuestion !== undefined}
@ -148,14 +140,14 @@
{state}
{tags}
on:saved={() => {
skip($firstQuestion, true)
}}
skip($firstQuestion, true)
}}
>
<button
class="secondary"
on:click={() => {
skip($firstQuestion)
}}
skip($firstQuestion)
}}
slot="cancel"
>
<Tr t={Translations.t.general.skip} />
@ -170,7 +162,6 @@
{/if}
<div class="mt-4 mb-8">
{#if skipped + answered > 0}
<div class="flex justify-center">
{#if answered === 0}
@ -198,9 +189,9 @@
{:else}
<Tr
t={Translations.t.general.questionBox.answeredMultipleSkippedMultiple.Subs({
answered,
skipped,
})}
answered,
skipped,
})}
/>
{/if}
</div>
@ -215,7 +206,6 @@
>
<Tr t={Translations.t.general.questionBox.reactivate} />
</button>
{/if}
{/if}
@ -223,13 +213,12 @@
<button
class="w-full"
on:click={() => {
skippedQuestions.setData(new Set())
skipped = 0
}}
skippedQuestions.setData(new Set())
skipped = 0
}}
>
Show the disabled questions for this object
</button>
{/if}
{#if $debug}
Skipped questions are {Array.from($skippedQuestions).join(", ")}

View file

@ -93,7 +93,7 @@
return !m.hideInAnswer.matchesProperties(tgs)
})
selectedMapping = mappings?.findIndex(
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs),
(mapping) => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs)
)
if (selectedMapping < 0) {
selectedMapping = undefined
@ -201,7 +201,7 @@
if (freeformValue?.length > 0) {
selectedMapping = config.mappings.length
}
}),
})
)
$: {
@ -219,7 +219,7 @@
$freeformInput,
selectedMapping,
checkedMappings,
tags.data,
tags.data
)
if (featureSwitchIsDebugging?.data) {
console.log(
@ -231,7 +231,7 @@
currentTags: tags.data,
},
" --> ",
selectedTags,
selectedTags
)
}
} catch (e) {
@ -253,7 +253,7 @@
selectedTags = new And([...selectedTags.and, ...extraTagsArray])
} else {
console.error(
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags),
"selectedTags is not of type Tag or And, it is a " + JSON.stringify(selectedTags)
)
}
}
@ -322,7 +322,7 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
}),
})
)
}
@ -351,7 +351,7 @@
}
function enableQuestion() {
const newList = disabledInTheme.data?.filter(id => id !== config.id)
const newList = disabledInTheme.data?.filter((id) => id !== config.id)
disabledInTheme.set(newList)
menuIsOpened.set(false)
}
@ -359,7 +359,6 @@
{#if question !== undefined}
<div class={clss}>
{#if layer.isNormal()}
<LoginToggle {state}>
<DotMenu hideBackground={true} open={menuIsOpened}>

View file

@ -2116,7 +2116,6 @@ export default class SpecialVisualizations {
constr(state) {
return new SvelteUIElement(DisabledQuestions, { state })
},
},
]

View file

@ -18,7 +18,6 @@
import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"
import { GeoOperations } from "../../Logic/GeoOperations"
export let paths: string[]
let downloaded = 0
@ -26,40 +25,42 @@
const filteredLayer = new FilteredLayer(layer)
let allData = <UIEventSource<(ChangeSetData & OsmFeature)[]>>UIEventSource.FromPromise(
Promise.all(paths.map(async p => {
const r = await Utils.downloadJson<FeatureCollection>(p)
downloaded++
return r
}))
).mapD(list => [].concat(...list.map(f => f.features)))
let overview = allData.mapD(data =>
ChangesetsOverview.fromDirtyData(data)
.filter((cs) => filteredLayer.isShown(<any>cs.properties)), [filteredLayer.currentFilter])
const trs = layer.tagRenderings.filter(
(tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined
).filter(tr => tr.question !== undefined)
let diffInDays = overview.mapD(overview => {
const dateStrings = Utils.NoNull(
overview._meta.map((cs) => cs.properties.date)
Promise.all(
paths.map(async (p) => {
const r = await Utils.downloadJson<FeatureCollection>(p)
downloaded++
return r
})
)
).mapD((list) => [].concat(...list.map((f) => f.features)))
let overview = allData.mapD(
(data) =>
ChangesetsOverview.fromDirtyData(data).filter((cs) =>
filteredLayer.isShown(<any>cs.properties)
),
[filteredLayer.currentFilter]
)
const trs = layer.tagRenderings
.filter((tr) => tr.mappings?.length > 0 || tr.freeform?.key !== undefined)
.filter((tr) => tr.question !== undefined)
let diffInDays = overview.mapD((overview) => {
const dateStrings = Utils.NoNull(overview._meta.map((cs) => cs.properties.date))
const dates: number[] = dateStrings.map((d) => new Date(d).getTime())
const mindate = Math.min(...dates)
const maxdate = Math.max(...dates)
return (maxdate - mindate) / (1000 * 60 * 60 * 24)
})
function offerAsDownload(){
const data = GeoOperations.toCSV($overview._meta, {
ignoreTags:
/^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/,
})
Utils.offerContentsAsDownloadableFile(data, "statistics.csv", {
mimetype: "text/csv",
})
function offerAsDownload() {
const data = GeoOperations.toCSV($overview._meta, {
ignoreTags: /^((deletion:node)|(import:node)|(move:node)|(soft-delete:))/,
})
Utils.offerContentsAsDownloadableFile(data, "statistics.csv", {
mimetype: "text/csv",
})
}
</script>
@ -73,15 +74,15 @@
<Accordion>
{#each trs as tr}
<AccordionItem paddingDefault="p-0" inactiveClass="text-black">
<span slot="header" class={"w-full p-2 text-base"}>
{tr.question ?? tr.id}
</span>
<span slot="header" class={"w-full p-2 text-base"}>
{tr.question ?? tr.id}
</span>
<SingleStat {tr} overview={$overview} diffInDays={$diffInDays} />
</AccordionItem>
{/each}
</Accordion>
<button on:click={() => offerAsDownload()}>
<DownloadIcon class="w-6 h-6" />
<DownloadIcon class="h-6 w-6" />
Download as CSV
</button>
{/if}

View file

@ -27,9 +27,9 @@ export interface ChangeSetData extends Feature<Polygon> {
delete: number
area: number
is_suspect: boolean
// harmful: any
// harmful: any
checked: boolean
// check_date: any
// check_date: any
host: string
theme: string
imagery: string
@ -61,7 +61,7 @@ export class ChangesetsOverview {
"testing mapcomplete 0.0.0": "buurtnatuur",
entrances: "indoor",
"https://raw.githubusercontent.com/osmbe/play/master/mapcomplete/geveltuinen/geveltuinen.json":
"geveltuintjes"
"geveltuintjes",
}
public static readonly valuesToSum: ReadonlyArray<string> = [
@ -88,7 +88,7 @@ export class ChangesetsOverview {
return new ChangesetsOverview(meta?.map((cs) => ChangesetsOverview.cleanChangesetData(cs)))
}
private static cleanChangesetData(cs: ChangeSetData & OsmFeature): (ChangeSetData & OsmFeature) {
private static cleanChangesetData(cs: ChangeSetData & OsmFeature): ChangeSetData & OsmFeature {
if (cs === undefined) {
return undefined
}

View file

@ -1,5 +1,4 @@
<script lang="ts">
/**
* Shows the statistics for a single item
*/
@ -14,9 +13,7 @@
let total: number = undefined
if (tr.freeform?.key !== undefined) {
total = new Set(
overview._meta.map((f) => f.properties[tr.freeform.key])
).size
total = new Set(overview._meta.map((f) => f.properties[tr.freeform.key])).size
}
</script>
@ -26,31 +23,28 @@
<h3>By number of changesets</h3>
<div class="flex">
<ToSvelte construct={ new TagRenderingChart(overview._meta, tr, {
groupToOtherCutoff:
total > 50 ? 25 : total > 10 ? 3 : 0,
chartstyle: "width: 24rem; height: 24rem",
chartType: "doughnut",
sort: true,
})} />
<ToSvelte
construct={new TagRenderingChart(overview._meta, tr, {
groupToOtherCutoff: total > 50 ? 25 : total > 10 ? 3 : 0,
chartstyle: "width: 24rem; height: 24rem",
chartType: "doughnut",
sort: true,
})}
/>
</div>
<ToSvelte construct={new StackedRenderingChart(tr, overview._meta, {
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff:
total > 50 ? 25 : total > 10 ? 3 : 0,
})} />
<ToSvelte
construct={new StackedRenderingChart(tr, overview._meta, {
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff: total > 50 ? 25 : total > 10 ? 3 : 0,
})}
/>
<h3>By number of modifications</h3>
<ToSvelte construct={ new StackedRenderingChart( tr, overview._meta,
{
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff: total > 50 ? 10 : 0,
sumFields: ChangesetsOverview. valuesToSum,
}
)} />
<ToSvelte
construct={new StackedRenderingChart(tr, overview._meta, {
period: diffInDays <= 367 ? "day" : "month",
groupToOtherCutoff: total > 50 ? 10 : 0,
sumFields: ChangesetsOverview.valuesToSum,
})}
/>

View file

@ -8,23 +8,19 @@
"https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/changeset-metadata/"
let stats_files = "file-overview.json"
let indexFile = UIEventSource.FromPromise(
Utils.downloadJson<string[]>(homeUrl + stats_files)
)
let indexFile = UIEventSource.FromPromise(Utils.downloadJson<string[]>(homeUrl + stats_files))
</script>
<div class="m-4">
<div class="flex justify-between">
<h2>Statistics of changes made with MapComplete</h2>
<a href="/" class="button">Back to index</a>
</div>
{#if $indexFile === undefined}
<Loading>Loading index file...</Loading>
{:else}
<AllStats paths={$indexFile.filter(p => p.startsWith("stats")).map(p => homeUrl+"/"+p)} />
<AllStats
paths={$indexFile.filter((p) => p.startsWith("stats")).map((p) => homeUrl + "/" + p)}
/>
{/if}
</div>

View file

@ -28,18 +28,23 @@
const [[latD], [latM], [latS, latSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLatitude?.value
>tags?.GPSLatitude?.value
const [[lonD], [lonM], [lonS, lonSDenom]] = <
[[number, number], [number, number], [number, number]]
>tags?.GPSLongitude?.value
>tags?.GPSLongitude?.value
const exifLat = latD + latM / 60 + latS / (3600 * latSDenom)
const exifLon = lonD + lonM / 60 + lonS / (3600 * lonSDenom)
if (typeof exifLat === "number" && !isNaN(exifLat) && typeof exifLon === "number" && !isNaN(exifLon)
&& !(exifLat === 0 && exifLon === 0)) {
if (
typeof exifLat === "number" &&
!isNaN(exifLat) &&
typeof exifLon === "number" &&
!isNaN(exifLon) &&
!(exifLat === 0 && exifLon === 0)
) {
lat = exifLat
lon = exifLon
l("Using EXIFLAT + EXIFLON")
}else{
} else {
l("NOT using exifLat and exifLon: invalid value detected")
}
l("Lat and lon are", lat, lon)
@ -54,11 +59,7 @@
}
try {
const p = new AuthorizedPanoramax(
Constants.panoramax.url,
Constants.panoramax.token,
)
const p = new AuthorizedPanoramax(Constants.panoramax.url, Constants.panoramax.token)
const sequenceId = "7f34cf53-27ff-46c9-ac22-78511fa8457a" // test-sequence
l("Fetching sequence number...")
const sequence: { id: string; "stats:items": { count: number } } = (
@ -71,7 +72,7 @@
datetime,
isBlurred: false,
exifOverride: {
"Artist": "TEST ACCOUNT",
Artist: "TEST ACCOUNT",
},
})
l("Upload completed. Adding meta")
@ -87,7 +88,6 @@
<div class="border border-black p-1">Select file</div>
</FileSelector>
<div class="flex flex-col">
{#each $log as logl}
<div>{logl}</div>
{/each}

View file

@ -479,11 +479,7 @@
state.selectedElement.setData(undefined)
}}
>
<SelectedElementView
{state}
layer={$selectedLayer}
selectedElement={$selectedElement}
/>
<SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} />
</FloatOver>
{/if}
{/if}

View file

@ -1,7 +1,7 @@
{
"contributors": [
{
"commits": 8594,
"commits": 8650,
"contributor": "Pieter Vander Vennet"
},
{
@ -13,7 +13,7 @@
"contributor": "Tobias"
},
{
"commits": 41,
"commits": 42,
"contributor": "dependabot[bot]"
},
{
@ -180,6 +180,10 @@
"commits": 3,
"contributor": "Léo Villeveygoux"
},
{
"commits": 2,
"contributor": "Jens Köcke"
},
{
"commits": 2,
"contributor": "Jim Kats"

View file

@ -1,5 +1,6 @@
{
"ca": "català",
"cs": "čeština",
"da": "dansk",
"de": "Deutsch",
"en": "English",
@ -23,7 +24,6 @@
"sl": "slovenščina",
"sv": "svenska",
"uk": "українська мова",
"zgh": "ⵜⴰⵎⴰⵣⵉⵖⵜ ⵜⴰⵏⴰⵡⴰⵢⵜ ⵜⴰⵎⵖⵔⵉⴱⵉⵜ",
"zh_Hans": "简体中文",
"zh_Hant": "繁體中文"
}

File diff suppressed because it is too large Load diff

View file

@ -1709,6 +1709,10 @@
{
"if": "value=polygon_centroid",
"then": "Show an icon at a polygon centroid (but not if it is a way)"
},
{
"if": "value=waypoints",
"then": "Show an icon on every intermediate point of a way"
}
],
"multianswer": "true"
@ -11191,6 +11195,10 @@
"if": "value=split_point",
"then": "split_point - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
},
{
"if": "value=split_road",
"then": "split_road - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
},
{
"if": "value=sport_pitch",
"then": "sport_pitch - A sport pitch"
@ -11465,7 +11473,14 @@
]
},
"labels": {
"description": "What labels should be applied on this tagRendering?\n\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\n\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search",
"description": "question: What labels should be applied on this tagRendering?\n\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\n\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search",
"type": "array",
"items": {
"type": "string"
}
},
"onSoftDelete": {
"description": "question: What tags should be applied when the object is soft-deleted?",
"type": "array",
"items": {
"type": "string"
@ -12603,9 +12618,23 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [
@ -13939,9 +13968,24 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"override",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [
@ -15309,9 +15353,24 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"renderings",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [
@ -16696,9 +16755,25 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"renderings",
"override",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [
@ -18058,9 +18133,24 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"renderings",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [
@ -19445,9 +19535,25 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"tagRenderings",
"renderings",
"override",
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [

View file

@ -1143,6 +1143,10 @@
"if": "value=split_point",
"then": "<b>split_point</b> (builtin) - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
},
{
"if": "value=split_road",
"then": "<b>split_road</b> (builtin) - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
},
{
"if": "value=sport_pitch",
"then": "<b>sport_pitch</b> (builtin) - A sport pitch"
@ -13847,6 +13851,10 @@
"if": "value=split_point",
"then": "split_point - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
},
{
"if": "value=split_road",
"then": "split_road - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
},
{
"if": "value=sport_pitch",
"then": "sport_pitch - A sport pitch"
@ -35587,6 +35595,10 @@
"if": "value=split_point",
"then": "split_point - Layer rendering the little scissors for the minimap in the 'splitRoadWizard'"
},
{
"if": "value=split_road",
"then": "split_road - Layer rendering the way to split in the 'splitRoadWizard'. This one is used instead of the variable rendering by the themes themselves, as they might not always be very visible"
},
{
"if": "value=sport_pitch",
"then": "sport_pitch - A sport pitch"

View file

@ -921,9 +921,22 @@
"labels"
],
"required": false,
"hints": {},
"hints": {
"question": "What labels should be applied on this tagRendering?"
},
"type": "array",
"description": "What labels should be applied on this tagRendering?\nA list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
"description": "A list of labels. These are strings that are used for various purposes, e.g. to only include a subset of the tagRenderings when reusing a layer\nSpecial values:\n- \"hidden\": do not show this tagRendering. Useful in it is used by e.g. an accordion\n- \"description\": this label is a description used in the search"
},
{
"path": [
"onSoftDelete"
],
"required": false,
"hints": {
"question": "What tags should be applied when the object is soft-deleted?"
},
"type": "array",
"description": ""
},
{
"path": [

View file

@ -17,7 +17,7 @@
"contributor": "Anonymous"
},
{
"commits": 105,
"commits": 106,
"contributor": "mcliquid"
},
{
@ -30,11 +30,11 @@
},
{
"commits": 70,
"contributor": "danieldegroot2"
"contributor": "mike140"
},
{
"commits": 67,
"contributor": "mike140"
"commits": 70,
"contributor": "danieldegroot2"
},
{
"commits": 53,
@ -57,7 +57,7 @@
"contributor": "Supaplex"
},
{
"commits": 37,
"commits": 38,
"contributor": "Lucas"
},
{
@ -148,6 +148,10 @@
"commits": 11,
"contributor": "Túllio Franca"
},
{
"commits": 10,
"contributor": "small"
},
{
"commits": 10,
"contributor": "Jeff Huang"
@ -272,6 +276,10 @@
"commits": 6,
"contributor": "lvgx"
},
{
"commits": 5,
"contributor": "foxandpotatoes"
},
{
"commits": 5,
"contributor": "Ignacio"
@ -320,6 +328,10 @@
"commits": 5,
"contributor": "Alexey Shabanov"
},
{
"commits": 4,
"contributor": "Weblate Admin"
},
{
"commits": 4,
"contributor": "André Marcelo Alvarenga"
@ -344,6 +356,10 @@
"commits": 4,
"contributor": "Jan Zabel"
},
{
"commits": 3,
"contributor": "Eric Armijo"
},
{
"commits": 3,
"contributor": "Andrii Holovin"
@ -388,10 +404,6 @@
"commits": 3,
"contributor": "liimee"
},
{
"commits": 3,
"contributor": "foxandpotatoes"
},
{
"commits": 3,
"contributor": "Sasha"
@ -428,10 +440,6 @@
"commits": 3,
"contributor": "SiegbjornSitumeang"
},
{
"commits": 2,
"contributor": "Weblate Admin"
},
{
"commits": 2,
"contributor": "SmallSoap"
@ -528,10 +536,6 @@
"commits": 2,
"contributor": "Localizer"
},
{
"commits": 2,
"contributor": "Eric Armijo"
},
{
"commits": 2,
"contributor": "MeblIkea"
@ -568,6 +572,18 @@
"commits": 2,
"contributor": "Leo Alcaraz"
},
{
"commits": 1,
"contributor": "Héctor Ochoa Ortiz"
},
{
"commits": 1,
"contributor": "Gábor"
},
{
"commits": 1,
"contributor": "Roger"
},
{
"commits": 1,
"contributor": "M1chaelWang"