Merge feature/svelte into develop

This commit is contained in:
Pieter Vander Vennet 2023-03-08 19:02:41 +01:00
commit 868d476891
120 changed files with 5168 additions and 10657 deletions

View file

@ -3,7 +3,7 @@ on:
push:
branches:
- develop
- feature/vite
- feature/svelte
jobs:
build:
runs-on: ubuntu-latest

View file

@ -1,4 +1,6 @@
{
"semi": false,
"printWidth": 100
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

@ -2,6 +2,8 @@
"recommendations": [
"esbenp.prettier-vscode",
"eamodio.gitlens",
"GitHub.vscode-pull-request-github"
"GitHub.vscode-pull-request-github",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss"
]
}

38
.vscode/settings.json vendored
View file

@ -1,21 +1,21 @@
{
"json.schemas": [
{
"fileMatch": [
"/assets/layers/*/*.json",
"!/assets/layers/*/license_info.json"
],
"url": "./Docs/Schemas/LayerConfigJson.schema.json"
},
{
"fileMatch": [
"/assets/themes/*/*.json",
"!/assets/themes/*/license_info.json"
],
"url": "./Docs/Schemas/LayoutConfigJson.schema.json"
}
],
"editor.tabSize": 2,
"files.autoSave": "onFocusChange",
"search.useIgnoreFiles": true
"json.schemas": [
{
"fileMatch": ["/assets/layers/*/*.json", "!/assets/layers/*/license_info.json"],
"url": "./Docs/Schemas/LayerConfigJson.schema.json"
},
{
"fileMatch": ["/assets/themes/*/*.json", "!/assets/themes/*/license_info.json"],
"url": "./Docs/Schemas/LayoutConfigJson.schema.json"
}
],
"editor.tabSize": 2,
"files.autoSave": "onFocusChange",
"search.useIgnoreFiles": true,
"css.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[svelte]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

View file

@ -1,5 +1,4 @@
import questions from "../assets/tagRenderings/questions.json"
import icons from "../assets/tagRenderings/icons.json"
import { Utils } from "../Utils"
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
@ -14,11 +13,9 @@ export default class SharedTagRenderings {
SharedTagRenderings.generatedSharedFields()
public static SharedTagRenderingJson: Map<string, TagRenderingConfigJson> =
SharedTagRenderings.generatedSharedFieldsJsons()
public static SharedIcons: Map<string, TagRenderingConfig> =
SharedTagRenderings.generatedSharedFields(true)
private static generatedSharedFields(iconsOnly = false): Map<string, TagRenderingConfig> {
const configJsons = SharedTagRenderings.generatedSharedFieldsJsons(iconsOnly)
private static generatedSharedFields(): Map<string, TagRenderingConfig> {
const configJsons = SharedTagRenderings.generatedSharedFieldsJsons()
const d = new Map<string, TagRenderingConfig>()
for (const key of Array.from(configJsons.keys())) {
try {
@ -31,7 +28,7 @@ export default class SharedTagRenderings {
console.error(
"BUG: could not parse",
key,
" from questions.json or icons.json - this error happened during the build step of the SharedTagRenderings",
" from questions.json - this error happened during the build step of the SharedTagRenderings",
e
)
}
@ -40,24 +37,14 @@ export default class SharedTagRenderings {
return d
}
private static generatedSharedFieldsJsons(
iconsOnly = false
): Map<string, TagRenderingConfigJson> {
private static generatedSharedFieldsJsons(): Map<string, TagRenderingConfigJson> {
const dict = new Map<string, TagRenderingConfigJson>()
if (!iconsOnly) {
for (const key in questions) {
if (key === "id") {
continue
}
dict.set(key, <TagRenderingConfigJson>questions[key])
}
}
for (const key in icons) {
for (const key in questions) {
if (key === "id") {
continue
}
dict.set(key, <TagRenderingConfigJson>icons[key])
dict.set(key, <TagRenderingConfigJson>questions[key])
}
dict.forEach((value, key) => {

View file

@ -2,8 +2,10 @@ import { QueryParameters } from "../Web/QueryParameters"
import { BBox } from "../BBox"
import Constants from "../../Models/Constants"
import { GeoLocationPointProperties, GeoLocationState } from "../State/GeoLocationState"
import State from "../../State"
import { UIEventSource } from "../UIEventSource"
import Loc from "../../Models/Loc"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"
/**
* The geolocation-handler takes a map-location and a geolocation state.
@ -12,12 +14,24 @@ import { UIEventSource } from "../UIEventSource"
*/
export default class GeoLocationHandler {
public readonly geolocationState: GeoLocationState
private readonly _state: State
private readonly _state: {
currentUserLocation: SimpleFeatureSource
layoutToUse: LayoutConfig
locationControl: UIEventSource<Loc>
selectedElement: UIEventSource<any>
leafletMap?: UIEventSource<any>
}
public readonly mapHasMoved: UIEventSource<boolean> = new UIEventSource<boolean>(false)
constructor(
geolocationState: GeoLocationState,
state: State // { locationControl: UIEventSource<Loc>, selectedElement: UIEventSource<any>, leafletMap?: UIEventSource<any> })
state: {
locationControl: UIEventSource<Loc>
currentUserLocation: SimpleFeatureSource
layoutToUse: LayoutConfig
selectedElement: UIEventSource<any>
leafletMap?: UIEventSource<any>
}
) {
this.geolocationState = geolocationState
this._state = state

View file

@ -194,8 +194,7 @@ export default class DetermineLayout {
let { errors } = new ValidateThemeAndLayers(
new DoesImageExist(new Set<string>(), (_) => true),
"",
false,
SharedTagRenderings.SharedTagRendering
false
).convert(json, "validation")
if (errors.length > 0) {
throw "Detected errors: " + errors.join("\n")

View file

@ -185,16 +185,20 @@ export class GeoOperations {
* GeoOperations.inside([1.42822265625, 48.61838518688487], multiPolygon) // => false
* GeoOperations.inside([4.02099609375, 47.81315451752768], multiPolygon) // => false
*/
public static inside(pointCoordinate, feature): boolean {
public static inside(
pointCoordinate: [number, number] | Feature<Point>,
feature: Feature
): boolean {
// ray-casting algorithm based on
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
if (feature.geometry.type === "Point") {
// The feature that should 'contain' pointCoordinate is a point itself, so it cannot contain anything
return false
}
if (pointCoordinate.geometry !== undefined) {
pointCoordinate = pointCoordinate.geometry.coordinates
if (pointCoordinate["geometry"] !== undefined) {
pointCoordinate = pointCoordinate["geometry"].coordinates
}
const x: number = pointCoordinate[0]
@ -203,6 +207,7 @@ export class GeoOperations {
if (feature.geometry.type === "MultiPolygon") {
const coordinatess = feature.geometry.coordinates
for (const coordinates of coordinatess) {
// @ts-ignore
const inThisPolygon = GeoOperations.pointInPolygonCoordinates(x, y, coordinates)
if (inThisPolygon) {
return true
@ -212,6 +217,7 @@ export class GeoOperations {
}
if (feature.geometry.type === "Polygon") {
// @ts-ignore
return GeoOperations.pointInPolygonCoordinates(x, y, feature.geometry.coordinates)
}

View file

@ -1,8 +1,6 @@
import { UIEventSource } from "../UIEventSource"
import UserDetails, { OsmConnection } from "./OsmConnection"
import { Utils } from "../../Utils"
import { DomEvent } from "leaflet"
import preventDefault = DomEvent.preventDefault
export class OsmPreferences {
public preferences = new UIEventSource<Record<string, string>>({}, "all-osm-preferences")

View file

@ -58,6 +58,49 @@ export class SimpleMetaTagger {
}
}
export class ReferencingWaysMetaTagger extends SimpleMetaTagger {
/**
* Disable this metatagger, e.g. for caching or tests
* This is a bit a work-around
*/
public static enabled = true
constructor() {
super(
{
keys: ["_referencing_ways"],
isLazy: true,
doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ",
},
(feature, _, __, state) => {
if (!ReferencingWaysMetaTagger.enabled) {
return false
}
//this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points
const id = feature.properties.id
if (!id.startsWith("node/")) {
return false
}
console.trace("Downloading referencing ways for", feature.properties.id)
OsmObject.DownloadReferencingWays(id).then((referencingWays) => {
const currentTagsSource = state.allElements?.getEventSourceById(id) ?? []
const wayIds = referencingWays.map((w) => "way/" + w.id)
wayIds.sort()
const wayIdsStr = wayIds.join(";")
if (
wayIdsStr !== "" &&
currentTagsSource.data["_referencing_ways"] !== wayIdsStr
) {
currentTagsSource.data["_referencing_ways"] = wayIdsStr
currentTagsSource.ping()
}
})
return true
}
)
}
}
export class CountryTagger extends SimpleMetaTagger {
private static readonly coder = new CountryCoder(
Constants.countryCoderEndpoint,
@ -492,33 +535,7 @@ export default class SimpleMetaTaggers {
}
)
public static referencingWays = new SimpleMetaTagger(
{
keys: ["_referencing_ways"],
isLazy: true,
includesDates: true,
doc: "_referencing_ways contains - for a node - which ways use this this node as point in their geometry. ",
},
(feature, _, __, state) => {
//this function has some extra code to make it work in SimpleAddUI.ts to also work for newly added points
const id = feature.properties.id
if (!id.startsWith("node/")) {
return false
}
OsmObject.DownloadReferencingWays(id).then((referencingWays) => {
const currentTagsSource = state.allElements?.getEventSourceById(id) ?? []
const wayIds = referencingWays.map((w) => "way/" + w.id)
wayIds.sort()
const wayIdsStr = wayIds.join(";")
if (wayIdsStr !== "" && currentTagsSource.data["_referencing_ways"] !== wayIdsStr) {
currentTagsSource.data["_referencing_ways"] = wayIdsStr
currentTagsSource.ping()
}
})
return true
}
)
public static referencingWays = new ReferencingWaysMetaTagger()
public static metatags: SimpleMetaTagger[] = [
SimpleMetaTaggers.latlon,

View file

@ -18,6 +18,7 @@ export default class FeatureSwitchState {
public readonly featureSwitchBackgroundSelection: UIEventSource<boolean>
public readonly featureSwitchAddNew: UIEventSource<boolean>
public readonly featureSwitchWelcomeMessage: UIEventSource<boolean>
public readonly featureSwitchCommunityIndex: UIEventSource<boolean>
public readonly featureSwitchExtraLinkEnabled: UIEventSource<boolean>
public readonly featureSwitchMoreQuests: UIEventSource<boolean>
public readonly featureSwitchShareScreen: UIEventSource<boolean>
@ -91,6 +92,11 @@ export default class FeatureSwitchState {
() => true,
"Disables/enables the help menu or welcome message"
)
this.featureSwitchCommunityIndex = featSw(
"fs-community-index",
() => true,
"Disables/enables the button to get in touch with the community"
)
this.featureSwitchExtraLinkEnabled = featSw(
"fs-iframe-popout",
(_) => true,

View file

@ -1,4 +1,5 @@
import { Utils } from "../Utils"
import { Readable, Subscriber, Unsubscriber } from "svelte/store"
/**
* Various static utils
@ -88,7 +89,7 @@ export class Stores {
}
}
export abstract class Store<T> {
export abstract class Store<T> implements Readable<T> {
abstract readonly data: T
/**
@ -113,6 +114,18 @@ export abstract class Store<T> {
abstract map<J>(f: (t: T) => J): Store<J>
abstract map<J>(f: (t: T) => J, extraStoresToWatch: Store<any>[]): Store<J>
public mapD<J>(f: (t: T) => J, extraStoresToWatch?: Store<any>[]): Store<J> {
return this.map((t) => {
if (t === undefined) {
return undefined
}
if (t === null) {
return null
}
return f(t)
}, extraStoresToWatch)
}
/**
* Add a callback function which will run on future data changes
*/
@ -258,6 +271,17 @@ export abstract class Store<T> {
}
})
}
/**
* Same as 'addCallbackAndRun', added to be compatible with Svelte
* @param run
* @param invalidate
*/
public subscribe(run: Subscriber<T> & ((value: T) => void), invalidate?): Unsubscriber {
// We don't need to do anything with 'invalidate', see
// https://github.com/sveltejs/svelte/issues/3859
return this.addCallbackAndRun(run)
}
}
export class ImmutableStore<T> extends Store<T> {

View file

@ -1,9 +1,7 @@
import { TileLayer } from "leaflet"
export default interface BaseLayer {
id: string
name: string
layer: () => TileLayer
layer: () => any /*leaflet.TileLayer - not importing as it breaks scripts*/
max_zoom: number
min_zoom: number
feature: any

View file

@ -5,18 +5,25 @@ import metapaths from "../../../assets/layoutconfigmeta.json"
import tagrenderingmetapaths from "../../../assets/questionabletagrenderingconfigmeta.json"
import Translations from "../../../UI/i18n/Translations"
export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
export class ExtractImages extends Conversion<
LayoutConfigJson,
{ path: string; context: string }[]
> {
private _isOfficial: boolean
private _sharedTagRenderings: Map<string, any>
private _sharedTagRenderings: Set<string>
private static readonly layoutMetaPaths = metapaths.filter(
(mp) =>
ExtractImages.mightBeTagRendering(<any>mp) ||
(mp.typeHint !== undefined && (mp.typeHint === "image" || mp.typeHint === "icon"))
(mp.typeHint !== undefined &&
(mp.typeHint === "image" ||
mp.typeHint === "icon" ||
mp.typeHint === "image[]" ||
mp.typeHint === "icon[]"))
)
private static readonly tagRenderingMetaPaths = tagrenderingmetapaths
constructor(isOfficial: boolean, sharedTagRenderings: Map<string, any>) {
constructor(isOfficial: boolean, sharedTagRenderings: Set<string>) {
super("Extract all images from a layoutConfig using the meta paths.", [], "ExctractImages")
this._isOfficial = isOfficial
this._sharedTagRenderings = sharedTagRenderings
@ -64,23 +71,23 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
* ]
* }
* ]
* }, "test").result;
* }, "test").result.map(i => i.path);
* images.length // => 2
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") // => 0
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") // => 1
* images.findIndex(img => img == "./assets/layers/bike_parking/staple.svg") >= 0 // => true
* images.findIndex(img => img == "./assets/layers/bike_parking/bollard.svg") >= 0 // => true
*
* // should not pickup rotation, should drop color
* const images = new ExtractImages(true, new Map<string, any>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
* const images = new ExtractImages(true, new Set<string>()).convert(<any>{"layers": [{mapRendering: [{"location": ["point", "centroid"],"icon": "pin:black",rotation: 180,iconSize: "40,40,center"}]}]
* }, "test").result
* images.length // => 1
* images[0] // => "pin"
* images[0].path // => "pin"
*
*/
convert(
json: LayoutConfigJson,
context: string
): { result: string[]; errors: string[]; warnings: string[] } {
const allFoundImages: string[] = []
): { result: { path: string; context: string }[]; errors: string[]; warnings: string[] } {
const allFoundImages: { path: string; context: string }[] = []
const errors = []
const warnings = []
for (const metapath of ExtractImages.layoutMetaPaths) {
@ -108,7 +115,7 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
continue
}
allFoundImages.push(foundImage)
allFoundImages.push({ path: foundImage, context: context + "." + path })
} else {
// This is a tagRendering.
// Either every rendered value might be an icon
@ -137,7 +144,10 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
JSON.stringify(img.leaf)
)
} else {
allFoundImages.push(img.leaf)
allFoundImages.push({
path: img.leaf,
context: context + "." + path,
})
}
}
if (!allRenderedValuesAreImages && isImage) {
@ -146,7 +156,12 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
...Translations.T(
img.leaf,
"extract_images from " + img.path.join(".")
).ExtractImages(false)
)
.ExtractImages(false)
.map((path) => ({
path,
context: context + "." + path,
}))
)
}
}
@ -161,20 +176,30 @@ export class ExtractImages extends Conversion<LayoutConfigJson, string[]> {
)
continue
}
allFoundImages.push(foundElement.leaf)
if (typeof foundElement.leaf !== "string") {
continue
}
allFoundImages.push({
context: context + "." + foundElement.path.join("."),
path: foundElement.leaf,
})
}
}
}
const splitParts = []
.concat(
...Utils.NoNull(allFoundImages)
.map((img) => img["path"] ?? img)
.map((img) => img.split(";"))
const cleanedImages: { path: string; context: string }[] = []
for (const foundImage of allFoundImages) {
// Split "circle:white;./assets/layers/.../something.svg" into ["circle", "./assets/layers/.../something.svg"]
const allPaths = Utils.NoNull(
Utils.NoEmpty(foundImage.path?.split(";")?.map((part) => part.split(":")[0]))
)
.map((img) => img.split(":")[0])
.filter((img) => img !== "")
return { result: Utils.Dedup(splitParts), errors, warnings }
for (const path of allPaths) {
cleanedImages.push({ path, context: foundImage.context })
}
}
return { result: cleanedImages, errors, warnings }
}
}

View file

@ -7,29 +7,24 @@ import {
FirstOf,
Fuse,
On,
SetDefault,
} from "./Conversion"
import { LayerConfigJson } from "../Json/LayerConfigJson"
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson"
import { Utils } from "../../../Utils"
import RewritableConfigJson from "../Json/RewritableConfigJson"
import SpecialVisualizations from "../../../UI/SpecialVisualizations"
import Translations from "../../../UI/i18n/Translations"
import { Translation } from "../../../UI/i18n/Translation"
import tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json"
import { AddContextToTranslations } from "./AddContextToTranslations"
import FilterConfigJson from "../Json/FilterConfigJson"
import predifined_filters from "../../../assets/layers/filters/filters.json"
SetDefault
} from "./Conversion";
import { LayerConfigJson } from "../Json/LayerConfigJson";
import { TagRenderingConfigJson } from "../Json/TagRenderingConfigJson";
import { Utils } from "../../../Utils";
import RewritableConfigJson from "../Json/RewritableConfigJson";
import SpecialVisualizations from "../../../UI/SpecialVisualizations";
import Translations from "../../../UI/i18n/Translations";
import { Translation } from "../../../UI/i18n/Translation";
import tagrenderingconfigmeta from "../../../assets/tagrenderingconfigmeta.json";
import { AddContextToTranslations } from "./AddContextToTranslations";
import FilterConfigJson from "../Json/FilterConfigJson";
import predifined_filters from "../../../assets/layers/filters/filters.json";
import { TagConfigJson } from "../Json/TagConfigJson";
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson";
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson";
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
private static load_filters(): Map<string, FilterConfigJson> {
let filters = new Map<string, FilterConfigJson>()
for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
filters.set(filter.id, filter)
}
return filters
}
private static readonly predefinedFilters = ExpandFilter.load_filters()
constructor() {
@ -40,6 +35,14 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
)
}
private static load_filters(): Map<string, FilterConfigJson> {
let filters = new Map<string, FilterConfigJson>()
for (const filter of <FilterConfigJson[]>predifined_filters.filter) {
filters.set(filter.id, filter)
}
return filters
}
convert(
json: LayerConfigJson,
context: string
@ -128,6 +131,37 @@ class ExpandTagRendering extends Conversion<
}
private lookup(name: string): TagRenderingConfigJson[] {
const direct = this.directLookup(name)
if (direct === undefined) {
return undefined
}
const result: TagRenderingConfigJson[] = []
for (const tagRenderingConfigJson of direct) {
if (tagRenderingConfigJson["builtin"] !== undefined) {
let nm: string | string[] = tagRenderingConfigJson["builtin"]
let indirect: TagRenderingConfigJson[]
if (typeof nm === "string") {
indirect = this.lookup(nm)
} else {
indirect = [].concat(...nm.map((n) => this.lookup(n)))
}
for (let foundTr of indirect) {
foundTr = Utils.Clone<any>(foundTr)
Utils.Merge(tagRenderingConfigJson["override"] ?? {}, foundTr)
foundTr.id = tagRenderingConfigJson.id ?? foundTr.id
result.push(foundTr)
}
} else {
result.push(tagRenderingConfigJson)
}
}
return result
}
/**
* Looks up a tagRendering based on the name.
*/
private directLookup(name: string): TagRenderingConfigJson[] {
const state = this._state
if (state.tagRenderings.has(name)) {
return [state.tagRenderings.get(name)]
@ -747,6 +781,79 @@ export class RewriteSpecial extends DesugaringStep<TagRenderingConfigJson> {
}
}
class ExpandIconBadges extends DesugaringStep<PointRenderingConfigJson | LineRenderingConfigJson> {
private _state: DesugaringContext
private _layer: LayerConfigJson
private _expand: ExpandTagRendering
constructor(state: DesugaringContext, layer: LayerConfigJson) {
super("Expands shorthand properties on iconBadges", ["iconBadges"], "ExpandIconBadges")
this._state = state
this._layer = layer
this._expand = new ExpandTagRendering(state, layer)
}
convert(
json: PointRenderingConfigJson | LineRenderingConfigJson,
context: string
): {
result: PointRenderingConfigJson | LineRenderingConfigJson
errors?: string[]
warnings?: string[]
information?: string[]
} {
if (!json["iconBadges"]) {
return { result: json }
}
const badgesJson = (<PointRenderingConfigJson>json).iconBadges
const iconBadges: { if: TagConfigJson; then: string | TagRenderingConfigJson }[] = []
const errs: string[] = []
const warns: string[] = []
for (let i = 0; i < badgesJson.length; i++) {
const iconBadge: { if: TagConfigJson; then: string | TagRenderingConfigJson } =
badgesJson[i]
const { errors, result, warnings } = this._expand.convert(
iconBadge.then,
context + ".iconBadges[" + i + "]"
)
errs.push(...errors)
warns.push(...warnings)
if (result === undefined) {
iconBadges.push(iconBadge)
continue
}
iconBadges.push(
...result.map((resolved) => ({
if: iconBadge.if,
then: resolved,
}))
)
}
return {
result: { ...json, iconBadges },
errors: errs,
warnings: warns,
}
}
}
class PreparePointRendering extends Fuse<PointRenderingConfigJson | LineRenderingConfigJson> {
constructor(state: DesugaringContext, layer: LayerConfigJson) {
super(
"Prepares point renderings by expanding 'icon' and 'iconBadges'",
new On(
"icon",
new FirstOf(new ExpandTagRendering(state, layer, { applyCondition: false }))
),
new ExpandIconBadges(state, layer)
)
}
}
export class PrepareLayer extends Fuse<LayerConfigJson> {
constructor(state: DesugaringContext) {
super(
@ -755,19 +862,11 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new On("tagRenderings", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On("tagRenderings", (layer) => new Concat(new ExpandTagRendering(state, layer))),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On(
new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>(
"mapRendering",
(layer) =>
new Each(
new On(
"icon",
new FirstOf(
new ExpandTagRendering(state, layer, { applyCondition: false })
)
)
)
(layer) => new Each(new PreparePointRendering(state, layer))
),
new SetDefault("titleIcons", ["defaults"]),
new SetDefault("titleIcons", ["icons.defaults"]),
new On("titleIcons", (layer) => new Concat(new ExpandTagRendering(state, layer))),
new ExpandFilter()
)

View file

@ -60,13 +60,16 @@ class ValidateLanguageCompleteness extends DesugaringStep<any> {
export class DoesImageExist extends DesugaringStep<string> {
private readonly _knownImagePaths: Set<string>
private readonly _ignore?: Set<string>
private readonly doesPathExist: (path: string) => boolean = undefined
constructor(
knownImagePaths: Set<string>,
checkExistsSync: (path: string) => boolean = undefined
checkExistsSync: (path: string) => boolean = undefined,
ignore?: Set<string>
) {
super("Checks if an image exists", [], "DoesImageExist")
this._ignore = ignore
this._knownImagePaths = knownImagePaths
this.doesPathExist = checkExistsSync
}
@ -75,6 +78,10 @@ export class DoesImageExist extends DesugaringStep<string> {
image: string,
context: string
): { result: string; errors?: string[]; warnings?: string[]; information?: string[] } {
if (this._ignore?.has(image)) {
return { result: image }
}
const errors = []
const warnings = []
const information = []
@ -124,20 +131,23 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
*/
private readonly _path?: string
private readonly _isBuiltin: boolean
private _sharedTagRenderings: Map<string, any>
//private readonly _sharedTagRenderings: Map<string, any>
private readonly _validateImage: DesugaringStep<string>
private readonly _extractImages: ExtractImages = undefined
constructor(
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings: Map<string, any>
sharedTagRenderings?: Set<string>
) {
super("Doesn't change anything, but emits warnings and errors", [], "ValidateTheme")
this._validateImage = doesImageExist
this._path = path
this._isBuiltin = isBuiltin
this._sharedTagRenderings = sharedTagRenderings
if (sharedTagRenderings) {
this._extractImages = new ExtractImages(this._isBuiltin, sharedTagRenderings)
}
}
convert(
@ -169,13 +179,10 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
}
}
if (this._isBuiltin) {
if (this._isBuiltin && this._extractImages !== undefined) {
// Check images: are they local, are the licenses there, is the theme icon square, ...
const images = new ExtractImages(
this._isBuiltin,
this._sharedTagRenderings
).convertStrict(json, "validation")
const remoteImages = images.filter((img) => img.indexOf("http") == 0)
const images = this._extractImages.convertStrict(json, "validation")
const remoteImages = images.filter((img) => img.path.indexOf("http") == 0)
for (const remoteImage of remoteImages) {
errors.push(
"Found a remote image: " +
@ -187,8 +194,8 @@ class ValidateTheme extends DesugaringStep<LayoutConfigJson> {
}
for (const image of images) {
this._validateImage.convertJoin(
image,
context === undefined ? "" : ` in a layer defined in the theme ${context}`,
image.path,
context === undefined ? "" : ` in the theme ${context} at ${image.context}`,
errors,
warnings,
information
@ -268,7 +275,7 @@ export class ValidateThemeAndLayers extends Fuse<LayoutConfigJson> {
doesImageExist: DoesImageExist,
path: string,
isBuiltin: boolean,
sharedTagRenderings: Map<string, any>
sharedTagRenderings?: Set<string>
) {
super(
"Validates a theme and the contained layers",
@ -901,53 +908,6 @@ export class DetectDuplicateFilters extends DesugaringStep<{
)
}
/**
* Add all filter options into 'perOsmTag'
*/
private addLayerFilters(
layer: LayerConfigJson,
perOsmTag: Map<
string,
{
layer: LayerConfigJson
layout: LayoutConfigJson | undefined
filter: FilterConfigJson
}[]
>,
layout?: LayoutConfigJson | undefined
): void {
if (layer.filter === undefined || layer.filter === null) {
return
}
if (layer.filter["sameAs"] !== undefined) {
return
}
for (const filter of <(string | FilterConfigJson)[]>layer.filter) {
if (typeof filter === "string") {
continue
}
if (filter["#"]?.indexOf("ignore-possible-duplicate") >= 0) {
continue
}
for (const option of filter.options) {
if (option.osmTags === undefined) {
continue
}
const key = JSON.stringify(option.osmTags)
if (!perOsmTag.has(key)) {
perOsmTag.set(key, [])
}
perOsmTag.get(key).push({
layer,
filter,
layout,
})
}
}
}
convert(
json: { layers: LayerConfigJson[]; themes: LayoutConfigJson[] },
context: string
@ -1014,4 +974,51 @@ export class DetectDuplicateFilters extends DesugaringStep<{
information,
}
}
/**
* Add all filter options into 'perOsmTag'
*/
private addLayerFilters(
layer: LayerConfigJson,
perOsmTag: Map<
string,
{
layer: LayerConfigJson
layout: LayoutConfigJson | undefined
filter: FilterConfigJson
}[]
>,
layout?: LayoutConfigJson | undefined
): void {
if (layer.filter === undefined || layer.filter === null) {
return
}
if (layer.filter["sameAs"] !== undefined) {
return
}
for (const filter of <(string | FilterConfigJson)[]>layer.filter) {
if (typeof filter === "string") {
continue
}
if (filter["#"]?.indexOf("ignore-possible-duplicate") >= 0) {
continue
}
for (const option of filter.options) {
if (option.osmTags === undefined) {
continue
}
const key = JSON.stringify(option.osmTags)
if (!perOsmTag.has(key)) {
perOsmTag.set(key, [])
}
perOsmTag.get(key).push({
layer,
filter,
layout,
})
}
}
}
}

View file

@ -8,7 +8,22 @@ import { ExtractImages } from "./Conversion/FixImages"
import ExtraLinkConfig from "./ExtraLinkConfig"
import { Utils } from "../../Utils"
import used_languages from "../../assets/generated/used_languages.json"
export default class LayoutConfig {
/**
* Minimal information about a theme
**/
export class LayoutInformation {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview?: boolean
keywords?: any[]
}
export default class LayoutConfig implements LayoutInformation {
public static readonly defaultSocialImage = "assets/SocialImage.png"
public readonly id: string
public readonly credits?: string
@ -82,10 +97,12 @@ export default class LayoutConfig {
this.credits = json.credits
this.language = json.mustHaveLanguage ?? Object.keys(json.title)
this.usedImages = Array.from(
new ExtractImages(official, undefined).convertStrict(
json,
"while extracting the images of " + json.id + " " + context ?? ""
)
new ExtractImages(official, undefined)
.convertStrict(
json,
"while extracting the images of " + json.id + " " + context ?? ""
)
.map((i) => i.path)
).sort()
{
if (typeof json.title === "string") {

View file

@ -1,7 +1,6 @@
import PointRenderingConfigJson from "./Json/PointRenderingConfigJson"
import TagRenderingConfig from "./TagRenderingConfig"
import { TagsFilter } from "../../Logic/Tags/TagsFilter"
import SharedTagRenderings from "../../Customizations/SharedTagRenderings"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { Utils } from "../../Utils"
import Svg from "../../Svg"
@ -12,7 +11,6 @@ import { FixedUiElement } from "../../UI/Base/FixedUiElement"
import Img from "../../UI/Base/Img"
import Combine from "../../UI/Base/Combine"
import { VariableUiElement } from "../../UI/Base/VariableUIElement"
import { TagRenderingConfigJson } from "./Json/TagRenderingConfigJson"
export default class PointRenderingConfig extends WithContextLoader {
private static readonly allowed_location_codes = new Set<string>([
@ -37,6 +35,10 @@ export default class PointRenderingConfig extends WithContextLoader {
constructor(json: PointRenderingConfigJson, context: string) {
super(json, context)
if (json === undefined || json === null) {
throw "Invalid PointRenderingConfig: undefined or null"
}
if (typeof json.location === "string") {
json.location = [json.location]
}
@ -69,18 +71,9 @@ export default class PointRenderingConfig extends WithContextLoader {
}
this.cssClasses = this.tr("cssClasses", undefined)
this.iconBadges = (json.iconBadges ?? []).map((overlay, i) => {
let tr: TagRenderingConfig
if (
typeof overlay.then === "string" &&
SharedTagRenderings.SharedIcons.get(overlay.then) !== undefined
) {
tr = SharedTagRenderings.SharedIcons.get(overlay.then)
} else {
tr = new TagRenderingConfig(overlay.then, `iconBadges.${i}`)
}
return {
if: TagUtils.Tag(overlay.if),
then: tr,
then: new TagRenderingConfig(overlay.then, `iconBadges.${i}`),
}
})

54
UI/AllTagsPanel.svelte Normal file
View file

@ -0,0 +1,54 @@
<script lang="ts">
import ToSvelte from "./Base/ToSvelte.svelte"
import Table from "./Base/Table"
import { UIEventSource } from "../Logic/UIEventSource"
//Svelte props
export let tags: UIEventSource<any>
export let state: any
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ?? [])
)
const allTags = tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return parts
})
const tagsTable = new Table(["Key", "Value"], $allTags).SetClass("zebra-table")
</script>
<section>
<ToSvelte construct={tagsTable} />
</section>
<style lang="scss">
section {
@apply border border-solid border-black rounded-2xl p-4 block;
}
</style>

View file

@ -1,47 +0,0 @@
import { VariableUiElement } from "./Base/VariableUIElement"
import { UIEventSource } from "../Logic/UIEventSource"
import Table from "./Base/Table"
export class AllTagsPanel extends VariableUiElement {
constructor(tags: UIEventSource<any>, state?) {
const calculatedTags = [].concat(
// SimpleMetaTagger.lazyTags,
...(state?.layoutToUse?.layers?.map((l) => l.calculatedTags?.map((c) => c[0]) ?? []) ??
[])
)
super(
tags.map((tags) => {
const parts = []
for (const key in tags) {
if (!tags.hasOwnProperty(key)) {
continue
}
let v = tags[key]
if (v === "") {
v = "<b>empty string</b>"
}
parts.push([key, v ?? "<b>undefined</b>"])
}
for (const key of calculatedTags) {
const value = tags[key]
if (value === undefined) {
continue
}
let type = ""
if (typeof value !== "string") {
type = " <i>" + typeof value + "</i>"
}
parts.push(["<i>" + key + "</i>", value])
}
return new Table(["key", "value"], parts)
.SetStyle(
"border: 1px solid black; border-radius: 1em;padding:1em;display:block;"
)
.SetClass("zebra-table")
})
)
}
}

View file

@ -3,7 +3,6 @@ import Loc from "../../Models/Loc"
import BaseLayer from "../../Models/BaseLayer"
import { UIEventSource } from "../../Logic/UIEventSource"
import { BBox } from "../../Logic/BBox"
import { deprecate } from "util"
export interface MinimapOptions {
background?: UIEventSource<BaseLayer>

View file

@ -23,7 +23,7 @@ import StrayClickHandler from "../../Logic/Actors/StrayClickHandler"
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
* Shows the given uiToShow-element in the messagebox
*/
export class StrayClickHandlerImplementation {
class StrayClickHandlerImplementation {
private _lastMarker
constructor(
@ -91,6 +91,7 @@ export class StrayClickHandlerImplementation {
})
}
}
export default class MinimapImplementation extends BaseUIElement implements MinimapObj {
private static _nextId = 0
public readonly leafletMap: UIEventSource<Map>

View file

@ -0,0 +1,82 @@
<script lang="ts">
import { onMount } from "svelte";
import { Store } from "../../Logic/UIEventSource";
import BaseUIElement from "../BaseUIElement";
import Img from "./Img";
import Translations from "../i18n/Translations";
import { ImmutableStore } from "../../Logic/UIEventSource.js";
export let imageUrl: string | BaseUIElement = undefined;
export let message: string | BaseUIElement = undefined;
export let options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
extraClasses?: string
} = {};
// Website to open when clicked
let href : Store<string> = undefined
if(options?.url){
href = typeof options?.url == "string" ? new ImmutableStore(options.url) : options.url;
}
let imgElem: HTMLElement;
let msgElem: HTMLElement;
let imgClasses = "block justify-center shrink-0 mr-4 " + (options?.imgSize ?? "h-11 w-11");
onMount(() => {
// Image
if (imgElem && imageUrl) {
let img: BaseUIElement;
if ((imageUrl ?? "") === "") {
img = undefined;
} else if (typeof imageUrl !== "string") {
img = imageUrl?.SetClass(imgClasses);
}
if (img) imgElem.replaceWith(img.ConstructElement());
}
// Message
if (msgElem && message) {
let msg = Translations.W(message)?.SetClass("block text-ellipsis no-images flex-shrink");
msgElem.replaceWith(msg.ConstructElement());
}
});
</script>
<svelte:element
class={(options.extraClasses??"") + 'flex hover:shadow-xl transition-[color,background-color,box-shadow] hover:bg-unsubtle'}
href={$href}
target={options?.newTab ? "_blank" : ""}
this={href === undefined ? "span" : "a"}
>
<slot name="image">
{#if imageUrl !== undefined}
{#if typeof imageUrl === "string"}
<Img src={imageUrl} class={imgClasses+ " bg-red border border-black"}></Img>
{:else }
<template bind:this={imgElem} />
{/if}
{/if}
</slot>
<slot name="message">
<template bind:this={msgElem} />
</slot>
</svelte:element>
<style lang="scss">
span,
a {
@apply flex p-3 my-2 py-4 rounded-lg shrink-0;
@apply items-center w-full no-underline;
@apply bg-subtle text-black;
:global(span) {
@apply block text-ellipsis;
}
}
</style>

View file

@ -1,13 +1,11 @@
import Translations from "../i18n/Translations"
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import Link from "./Link"
import Img from "./Img"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { UIElement } from "../UIElement"
import { VariableUiElement } from "./VariableUIElement"
import Lazy from "./Lazy"
import Loading from "./Loading"
import SubtleButtonSvelte from "./SubtleButton.svelte"
import SvelteUIElement from "./SvelteUIElement"
export class SubtleButton extends UIElement {
private readonly imageUrl: string | BaseUIElement
@ -15,7 +13,7 @@ export class SubtleButton extends UIElement {
private readonly options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: string
imgSize?: string,
extraClasses?: string
}
@ -25,9 +23,9 @@ export class SubtleButton extends UIElement {
options: {
url?: string | Store<string>
newTab?: boolean
imgSize?: "h-11 w-11" | string
imgSize?: "h-11 w-11" | string,
extraClasses?: string
} = undefined
} = {}
) {
super()
this.imageUrl = imageUrl
@ -36,30 +34,11 @@ export class SubtleButton extends UIElement {
}
protected InnerRender(): string | BaseUIElement {
const classes =
"block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline " +
(this?.options?.extraClasses ?? "")
const message = Translations.W(this.message)?.SetClass(
"block text-ellipsis no-images flex-shrink"
)
let img
const imgClasses =
"block justify-center flex-none mr-4 " + (this.options?.imgSize ?? "h-11 w-11")
if ((this.imageUrl ?? "") === "") {
img = undefined
} else if (typeof this.imageUrl === "string") {
img = new Img(this.imageUrl)?.SetClass(imgClasses)
} else {
img = this.imageUrl?.SetClass(imgClasses)
}
const button = new Combine([img, message]).SetClass("flex items-center group w-full")
if (this.options?.url == undefined) {
this.SetClass(classes)
return button
}
return new Link(button, this.options.url, this.options.newTab ?? false).SetClass(classes)
return new SvelteUIElement(SubtleButtonSvelte, {
imageUrl: this?.imageUrl ?? undefined,
message: this?.message ?? "",
options: this?.options ?? {},
})
}
public OnClickWithLoading(

View file

@ -0,0 +1,37 @@
import BaseUIElement from "../BaseUIElement"
import { SvelteComponentTyped } from "svelte"
/**
* The SvelteUIComponent serves as a translating class which which wraps a SvelteElement into the BaseUIElement framework.
*/
export default class SvelteUIElement<
Props extends Record<string, any> = any,
Events extends Record<string, any> = any,
Slots extends Record<string, any> = any
> extends BaseUIElement {
private readonly _svelteComponent: {
new (args: {
target: HTMLElement
props: Props
events?: Events
slots?: Slots
}): SvelteComponentTyped<Props, Events, Slots>
}
private readonly _props: Props
constructor(svelteElement, props: Props) {
super()
this._svelteComponent = svelteElement
this._props = props
}
protected InnerConstructElement(): HTMLElement {
const el = document.createElement("div")
new this._svelteComponent({
target: el,
props: this._props,
})
return el
}
}

18
UI/Base/ToSvelte.svelte Normal file
View file

@ -0,0 +1,18 @@
<script lang="ts">
import BaseUIElement from "../BaseUIElement.js"
import { onMount } from "svelte"
export let construct: BaseUIElement | (() => BaseUIElement)
let elem: HTMLElement
onMount(() => {
let html =
typeof construct === "function"
? construct().ConstructElement()
: construct.ConstructElement()
elem.replaceWith(html)
})
</script>
<span bind:this={elem} />

View file

@ -108,7 +108,10 @@ class SingleLayerSelectionButton extends Toggle {
// Is the previous layer still valid? If so, we don't bother to switch
if (
previousLayer.data.feature === null ||
GeoOperations.inside(locationControl.data, previousLayer.data.feature)
GeoOperations.inside(
[locationControl.data.lon, locationControl.data.lat],
previousLayer.data.feature
)
) {
return
}

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { Tiles } from "../../Models/TileRange"
import { Utils } from "../../Utils"
import global_community from "../../assets/community_index_global_resources.json"
import ContactLink from "./ContactLink.svelte"
import { GeoOperations } from "../../Logic/GeoOperations"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import type { Feature, Geometry, GeometryCollection } from "@turf/turf"
export let locationControl: Store<{ lat: number; lon: number }>
const tileToFetch: Store<string> = locationControl.mapD((l) => {
const t = Tiles.embedded_tile(l.lat, l.lon, 6)
return `https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/tile_${t.z}_${t.x}_${t.y}.geojson`
})
const t = Translations.t.communityIndex
const resources = new UIEventSource<
Feature<Geometry | GeometryCollection, { resources; nameEn: string }>[]
>([])
tileToFetch.addCallbackAndRun(async (url) => {
const data = await Utils.downloadJsonCached(url, 24 * 60 * 60)
if (data === undefined) {
return
}
resources.setData(data.features)
})
const filteredResources = resources.map(
(features) =>
features.filter((f) => {
return GeoOperations.inside([locationControl.data.lon, locationControl.data.lat], f)
}),
[locationControl]
)
</script>
<div>
<ToSvelte construct={t.intro} />
{#each $filteredResources as feature}
<ContactLink country={feature.properties} />
{/each}
<ContactLink country={{ resources: global_community, nameEn: "Global resources" }} />
</div>

View file

@ -0,0 +1,50 @@
<script lang="ts">
// A contact link indicates how a mapper can contact their local community
// The _properties_ of a community feature
import Locale from "../i18n/Locale.js"
import Translations from "../i18n/Translations"
import ToSvelte from "../Base/ToSvelte.svelte"
import * as native from "../../assets/language_native.json"
import { TypedTranslation } from "../i18n/Translation"
const availableTranslationTyped: TypedTranslation<{ native: string }> =
Translations.t.communityIndex.available
const availableTranslation = availableTranslationTyped.OnEveryLanguage((s, ln) =>
s.replace("{native}", native[ln] ?? ln)
)
export let country: { resources; nameEn: string }
let resources: {
id: string
resolved: Record<string, string>
languageCodes: string[]
type: string
}[] = []
$: resources = Array.from(Object.values(country?.resources ?? {}))
const language = Locale.language
</script>
<div>
{#if country?.nameEn}
<h3>{country?.nameEn}</h3>
{/if}
{#each resources as resource}
<div class="flex link-underline items-center my-4">
<img
class="w-8 h-8 m-2"
src={`https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/${resource.type}.svg`}
/>
<div class="flex flex-col">
<a href={resource.resolved.url} target="_blank" rel="noreferrer nofollow" class="font-bold">
{resource.resolved.name ?? resource.resolved.url}
</a>
{resource.resolved?.description}
{#if resource.languageCodes?.indexOf($language) >= 0}
<span class="border-2 rounded-full border-lime-500 text-sm w-fit px-2">
<ToSvelte construct={() => availableTranslation.Clone()} />
</span>
{/if}
</div>
</div>
{/each}
</div>

View file

@ -123,7 +123,9 @@ export default class CopyrightPanel extends Combine {
const t = Translations.t.general.attribution
const layoutToUse = state.layoutToUse
const iconAttributions = layoutToUse.usedImages.map(CopyrightPanel.IconAttribution)
const iconAttributions: BaseUIElement[] = layoutToUse.usedImages.map(
CopyrightPanel.IconAttribution
)
let maintainer: BaseUIElement = undefined
if (layoutToUse.credits !== undefined && layoutToUse.credits !== "") {

View file

@ -0,0 +1,38 @@
<script lang="ts">
import UserDetails from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let userDetails: UIEventSource<UserDetails>
const t = Translations.t.general.morescreen
console.log($userDetails.csCount < 50)
</script>
<div>
{#if $userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock}
<SubtleButton
options={{
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
}}
>
<span slot="message">{t.requestATheme.toString()}</span>
</SubtleButton>
{:else}
<SubtleButton
options={{
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
}}
>
<span slot="image" class="h-11 w-11 mx-4 bg-red" >
<ToSvelte construct={Svg.pencil_ui()}/>
</span>
<span slot="message">{t.createYourOwnTheme.toString()}</span>
</SubtleButton>
{/if}
</div>

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import * as themeOverview from "../../assets/generated/theme_overview.json"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
export let search: UIEventSource<string>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let onMainScreen: boolean = true
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes: LayoutInformation[] = themeOverview["default"].filter(
(layout) => layout.hideFromOverview
)
const userPreferences = state.osmConnection.preferencesHandler.preferences
const t = Translations.t.general.morescreen
$: knownThemesId = Utils.NoNull(
Object.keys($userPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) => key.substring(prefix.length, key.length - "-enabled".length))
)
$: knownThemes = hiddenThemes.filter((theme) => knownThemesId.includes(theme.id))
</script>
<ThemesList {search} {state} {onMainScreen} themes={knownThemes} isCustom={true} hideThemes={false}>
<svelte:fragment slot="title">
<h3>{t.previouslyHiddenTitle.toString()}</h3>
<p>
{t.hiddenExplanation.Subs({
hidden_discovered: knownThemes.length.toString(),
total_hidden: hiddenThemes.length.toString(),
})}
</p>
</svelte:fragment>
</ThemesList>

View file

@ -1,35 +1,25 @@
import { VariableUiElement } from "../Base/VariableUIElement"
import Svg from "../../Svg"
import Combine from "../Base/Combine"
import { SubtleButton } from "../Base/SubtleButton"
import Translations from "../i18n/Translations"
import Constants from "../../Models/Constants"
import BaseUIElement from "../BaseUIElement"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import Loc from "../../Models/Loc"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import Toggle from "../Input/Toggle"
import { Utils } from "../../Utils"
import Title from "../Base/Title"
import themeOverview from "../../assets/generated/theme_overview.json"
import { Translation } from "../i18n/Translation"
import { TextField } from "../Input/TextField"
import FilteredCombine from "../Base/FilteredCombine"
import Locale from "../i18n/Locale"
import SvelteUIElement from "../Base/SvelteUIElement"
import ThemesList from "./ThemesList.svelte"
import HiddenThemeList from "./HiddenThemeList.svelte"
import UnofficialThemeList from "./UnofficialThemeList.svelte"
export default class MoreScreen extends Combine {
private static readonly officialThemes: {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywors?: any[]
}[] = themeOverview
private static readonly officialThemes: LayoutInformation[] = themeOverview
constructor(
state: UserRelatedState & {
@ -39,13 +29,6 @@ export default class MoreScreen extends Combine {
onMainScreen: boolean = false
) {
const tr = Translations.t.general.morescreen
let themeButtonStyle = ""
let themeListStyle = ""
if (onMainScreen) {
themeButtonStyle = "h-32 min-h-32 max-h-32 text-ellipsis overflow-hidden"
themeListStyle =
"md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4"
}
const search = new TextField({
placeholder: tr.searchForATheme,
@ -106,38 +89,26 @@ export default class MoreScreen extends Combine {
super([
new Combine([searchBar]).SetClass("flex justify-center"),
MoreScreen.createOfficialThemesList(
new SvelteUIElement(ThemesList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createPreviouslyVistedHiddenList(
onMainScreen,
search: search.GetValue(),
themes: MoreScreen.officialThemes,
}),
new SvelteUIElement(HiddenThemeList, {
state,
themeButtonStyle,
themeListStyle,
search.GetValue()
),
MoreScreen.createUnofficialThemeList(
themeButtonStyle,
onMainScreen,
search: search.GetValue(),
}),
new SvelteUIElement(UnofficialThemeList, {
state,
themeListStyle,
search.GetValue()
),
onMainScreen,
search: search.GetValue(),
}),
tr.streetcomplete.Clone().SetClass("block text-base mx-10 my-3 mb-10"),
])
}
private static NothingFound(search: UIEventSource<string>): BaseUIElement {
const t = Translations.t.general.morescreen
return new Combine([
new Title(t.noMatchingThemes, 5).SetClass("w-max font-bold"),
new SubtleButton(Svg.search_disable_ui(), t.noSearch, { imgSize: "h-6" })
.SetClass("h-12 w-max")
.onClick(() => search.setData("")),
]).SetClass("flex flex-col items-center w-full")
}
private static createUrlFor(
layout: { id: string; definition?: string },
isCustom: boolean,
@ -238,102 +209,6 @@ export default class MoreScreen extends Combine {
new SubtleButton(undefined, t.button, { url: "./professional.html" }),
]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg")
}
private static createUnofficialThemeList(
buttonClass: string,
state: UserRelatedState,
themeListClasses: string,
search: UIEventSource<string>
): BaseUIElement {
var currentIds: Store<string[]> = state.installedUserThemes
var stableIds = Stores.ListStabilized<string>(currentIds)
return new VariableUiElement(
stableIds.map((ids) => {
const allThemes: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
[]
for (const id of ids) {
const themeInfo = state.GetUnofficialTheme(id)
if (themeInfo === undefined) {
continue
}
const link = MoreScreen.createLinkButton(state, themeInfo, true)
if (link !== undefined) {
allThemes.push({
element: link.SetClass(buttonClass),
predicate: (s) => id.toLowerCase().indexOf(s) >= 0,
})
}
}
if (allThemes.length <= 0) {
return undefined
}
return new Combine([
Translations.t.general.customThemeIntro,
new FilteredCombine(allThemes, search, {
innerClasses: themeListClasses,
onEmpty: MoreScreen.NothingFound(search),
}),
])
})
)
}
private static createPreviouslyVistedHiddenList(
state: UserRelatedState,
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
const t = Translations.t.general.morescreen
const prefix = "mapcomplete-hidden-theme-"
const hiddenThemes = themeOverview.filter((layout) => layout.hideFromOverview)
const hiddenTotal = hiddenThemes.length
return new Toggle(
new VariableUiElement(
state.osmConnection.preferencesHandler.preferences.map((allPreferences) => {
const knownThemes: Set<string> = new Set(
Utils.NoNull(
Object.keys(allPreferences)
.filter((key) => key.startsWith(prefix))
.map((key) =>
key.substring(prefix.length, key.length - "-enabled".length)
)
)
)
if (knownThemes.size === 0) {
return undefined
}
const knownThemeDescriptions = hiddenThemes
.filter((theme) => knownThemes.has(theme.id))
.map((theme) => ({
element: MoreScreen.createLinkButton(state, theme)?.SetClass(
buttonClass
),
predicate: MoreScreen.MatchesLayoutFunc(theme),
}))
const knownLayouts = new FilteredCombine(knownThemeDescriptions, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
return new Combine([
new Title(t.previouslyHiddenTitle),
t.hiddenExplanation.Subs({
hidden_discovered: "" + knownThemes.size,
total_hidden: "" + hiddenTotal,
}),
knownLayouts,
])
})
).SetClass("flex flex-col"),
undefined,
state.osmConnection.isLoggedIn
)
}
private static MatchesLayoutFunc(layout: {
id: string
@ -364,70 +239,4 @@ export default class MoreScreen extends Combine {
return false
}
}
private static createOfficialThemesList(
state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> },
buttonClass: string,
themeListStyle: string,
search: UIEventSource<string>
): BaseUIElement {
let buttons: { element: BaseUIElement; predicate?: (s: string) => boolean }[] =
MoreScreen.officialThemes.map((layout) => {
if (layout === undefined) {
console.trace("Layout is undefined")
return undefined
}
if (layout.hideFromOverview) {
return undefined
}
const button = MoreScreen.createLinkButton(state, layout)?.SetClass(buttonClass)
if (layout.id === "personal") {
const element = new VariableUiElement(
state.osmConnection.userDetails
.map((userdetails) => userdetails.csCount)
.map((csCount) => {
if (csCount < Constants.userJourney.personalLayoutUnlock) {
return undefined
} else {
return button
}
})
)
return { element }
}
return { element: button, predicate: MoreScreen.MatchesLayoutFunc(layout) }
})
const professional = MoreScreen.CreateProffessionalSerivesButton()
const customGeneratorLink = MoreScreen.createCustomGeneratorButton(state)
buttons.splice(0, 0, { element: customGeneratorLink }, { element: professional })
return new FilteredCombine(buttons, search, {
innerClasses: themeListStyle,
onEmpty: MoreScreen.NothingFound(search),
})
}
/*
* Returns either a link to the issue tracker or a link to the custom generator, depending on the achieved number of changesets
* */
private static createCustomGeneratorButton(state: {
osmConnection: OsmConnection
}): VariableUiElement {
const tr = Translations.t.general.morescreen
return new VariableUiElement(
state.osmConnection.userDetails.map((userDetails) => {
if (userDetails.csCount < Constants.userJourney.themeGeneratorReadOnlyUnlock) {
return new SubtleButton(null, tr.requestATheme.Clone(), {
url: "https://github.com/pietervdvn/MapComplete/issues",
newTab: true,
})
}
return new SubtleButton(Svg.pencil_ui(), tr.createYourOwnTheme.Clone(), {
url: "https://pietervdvn.github.io/mc/legacy/070/customGenerator.html",
newTab: false,
})
})
)
}
}

View file

@ -0,0 +1,65 @@
<script lang="ts" context="module">
export interface Theme {
id: string
icon: string
title: any
shortDescription: any
definition?: any
mustHaveLanguage?: boolean
hideFromOverview: boolean
keywords?: any[]
}
</script>
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import Svg from "../../Svg"
import SubtleButton from "../Base/SubtleButton.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
export let search: UIEventSource<string>
const t = Translations.t.general.morescreen
</script>
<span>
<h5>{t.noMatchingThemes.toString()}</h5>
<button
on:click={() => {
search.setData("")
}}
>
<span>
<SubtleButton>
<span slot="image">
<ToSvelte construct={Svg.search_disable_ui()} />
</span>
<span slot="message">{t.noSearch.toString()}</span>
</SubtleButton>
</span>
</button>
</span>
<style lang="scss">
span {
@apply flex flex-col items-center w-full;
h5 {
@apply w-max font-bold;
}
// SubtleButton
button {
@apply h-12;
span {
@apply w-max;
:global(img) {
@apply h-6;
}
}
}
}
</style>

View file

@ -0,0 +1,24 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import Title from "../Base/Title"
import ToSvelte from "../Base/ToSvelte.svelte"
import Translations from "../i18n/Translations"
const t = Translations.t.professional.indexPage
</script>
<div>
<ToSvelte construct={new Title(t.hook, 4)} />
<span>
{t.hookMore.toString()}
</span>
<SubtleButton options={{ url: "./professional.html" }}>
<span slot="message">{t.button.toString()}</span>
</SubtleButton>
</div>
<style lang="scss">
div {
@apply flex flex-col border border-gray-300 p-2 rounded-lg;
}
</style>

View file

@ -0,0 +1,110 @@
<script lang="ts">
import SubtleButton from "../Base/SubtleButton.svelte"
import { Translation } from "../i18n/Translation"
import * as personal from "../../assets/themes/personal/personal.json"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection"
import Constants from "../../Models/Constants"
import type Loc from "../../Models/Loc"
import type { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
export let theme: LayoutInformation
export let isCustom: boolean = false
export let userDetails: UIEventSource<UserDetails>
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
$: title = new Translation(
theme.title,
!isCustom && !theme.mustHaveLanguage ? "themes:" + theme.id + ".title" : undefined
).toString()
$: description = new Translation(theme.shortDescription).toString()
// TODO: Improve this function
function createUrl(
layout: { id: string; definition?: string },
isCustom: boolean,
state?: { locationControl?: UIEventSource<{ lat; lon; zoom }>; layoutToUse?: { id } }
): Store<string> {
if (layout === undefined) {
return undefined
}
if (layout.id === undefined) {
console.error("ID is undefined for layout", layout)
return undefined
}
if (layout.id === state?.layoutToUse?.id) {
return undefined
}
const currentLocation = state?.locationControl
let path = window.location.pathname
// Path starts with a '/' and contains everything, e.g. '/dir/dir/page.html'
path = path.substr(0, path.lastIndexOf("/"))
// Path will now contain '/dir/dir', or empty string in case of nothing
if (path === "") {
path = "."
}
let linkPrefix = `${path}/${layout.id.toLowerCase()}.html?`
if (location.hostname === "localhost" || location.hostname === "127.0.0.1") {
linkPrefix = `${path}/theme.html?layout=${layout.id}&`
}
if (isCustom) {
linkPrefix = `${path}/theme.html?userlayout=${layout.id}&`
}
let hash = ""
if (layout.definition !== undefined) {
hash = "#" + btoa(JSON.stringify(layout.definition))
}
return (
currentLocation?.map((currentLocation) => {
const params = [
["z", currentLocation?.zoom],
["lat", currentLocation?.lat],
["lon", currentLocation?.lon],
]
.filter((part) => part[1] !== undefined)
.map((part) => part[0] + "=" + part[1])
.join("&")
return `${linkPrefix}${params}${hash}`
}) ?? new ImmutableStore<string>(`${linkPrefix}`)
)
}
</script>
{#if theme.id !== personal.id || $userDetails.csCount > Constants.userJourney.personalLayoutUnlock}
<div>
<SubtleButton options={{ url: createUrl(theme, isCustom, state) }}>
<img slot="image" src={theme.icon} class="block h-11 w-11 bg-red mx-4" alt="" />
<span slot="message" class="message">
<span>
<span>{title}</span>
<span>{description}</span>
</span>
</span>
</SubtleButton>
</div>
{/if}
<style lang="scss">
div {
@apply h-32 min-h-[8rem] max-h-32 text-ellipsis overflow-hidden;
span.message {
@apply flex flex-col justify-center h-24;
& > span {
@apply flex flex-col overflow-hidden;
span:nth-child(2) {
@apply text-[#999];
}
}
}
}
</style>

View file

@ -0,0 +1,83 @@
<script lang="ts">
import NoThemeResultButton from "./NoThemeResultButton.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import Locale from "../i18n/Locale"
import CustomGeneratorButton from "./CustomGeneratorButton.svelte"
import ProfessionalServicesButton from "./ProfessionalServicesButton.svelte"
import ThemeButton from "./ThemeButton.svelte"
import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig";
export let search: UIEventSource<string>
export let themes: LayoutInformation[]
export let state: { osmConnection: OsmConnection; locationControl?: UIEventSource<Loc> }
export let isCustom: boolean = false
export let onMainScreen: boolean = true
export let hideThemes: boolean = true
// Filter theme based on search value
$: filteredThemes = themes.filter((theme) => {
if ($search === undefined || $search === "") return true
const srch = $search.toLocaleLowerCase()
if (theme.id.toLowerCase().indexOf(srch) >= 0) {
return true
}
const entitiesToSearch = [theme.shortDescription, theme.title, ...(theme.keywords ?? [])]
for (const entity of entitiesToSearch) {
if (entity === undefined) {
continue
}
const term = entity["*"] ?? entity[Locale.language.data]
if (term?.toLowerCase()?.indexOf(search) >= 0) {
return true
}
}
return false
})
</script>
<section>
<slot name="title" />
{#if onMainScreen}
<div class="md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 gap-4">
{#if ($search === undefined || $search === "") && !isCustom}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{:else }
<div>
{#if ($search === undefined || $search === "") && !isCustom}
<CustomGeneratorButton userDetails={state.osmConnection.userDetails} />
<ProfessionalServicesButton />
{/if}
{#each filteredThemes as theme}
{#if theme !== undefined && !(hideThemes && theme?.hideFromOverview)}
<ThemeButton {theme} {isCustom} userDetails={state.osmConnection.userDetails} {state} />
{/if}
{/each}
</div>
{/if}
{#if filteredThemes.length == 0}
<NoThemeResultButton {search} />
{/if}
</section>
<style lang="scss">
section {
@apply flex flex-col;
}
</style>

View file

@ -0,0 +1,35 @@
<script lang="ts">
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
import type Loc from "../../Models/Loc"
import { Utils } from "../../Utils"
import ThemesList from "./ThemesList.svelte"
import Translations from "../i18n/Translations"
import UserRelatedState from "../../Logic/State/UserRelatedState"
export let search: UIEventSource<string>
export let state: UserRelatedState & {
osmConnection: OsmConnection
locationControl?: UIEventSource<Loc>
}
export let onMainScreen: boolean = true
const t = Translations.t.general
const currentIds: Store<string[]> = state.installedUserThemes
const stableIds = Stores.ListStabilized<string>(currentIds)
$: customThemes = Utils.NoNull($stableIds.map((id) => state.GetUnofficialTheme(id)))
</script>
<ThemesList
{search}
{state}
{onMainScreen}
themes={customThemes}
isCustom={true}
hideThemes={false}
>
<svelte:fragment slot="title">
<!-- TODO: Change string to exclude html -->
{@html t.customThemeIntro.toString()}
</svelte:fragment>
</ThemesList>

View file

@ -34,6 +34,8 @@ import { GeoLocationState } from "../Logic/State/GeoLocationState"
import Hotkeys from "./Base/Hotkeys"
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers"
import CopyrightPanel from "./BigComponents/CopyrightPanel"
import SvelteUIElement from "./Base/SvelteUIElement"
import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte"
/**
* The default MapComplete GUI initializer
@ -237,6 +239,20 @@ export default class DefaultGUI {
const welcomeMessageMapControl = Toggle.If(state.featureSwitchWelcomeMessage, () =>
self.InitWelcomeMessage()
)
const communityIndex = Toggle.If(state.featureSwitchCommunityIndex, () => {
const communityIndexControl = new MapControlButton(Svg.community_svg())
const communityIndex = new ScrollableFullScreen(
() => Translations.t.communityIndex.title,
() => new SvelteUIElement(CommunityIndexView, { ...state }),
"community_index"
)
communityIndexControl.onClick(() => {
communityIndex.Activate()
})
return communityIndexControl
})
const testingBadge = Toggle.If(state.featureSwitchIsTesting, () =>
new FixedUiElement("TESTING").SetClass("alert m-2 border-2 border-black")
)
@ -253,6 +269,7 @@ export default class DefaultGUI {
welcomeMessageMapControl,
userInfoMapControl,
copyright,
communityIndex,
extraLink,
testingBadge,
])

View file

@ -23,17 +23,22 @@ import { FlowStep } from "./FlowStep"
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
import Title from "../Base/Title"
import CheckBoxes from "../Input/Checkboxes"
import { AllTagsPanel } from "../AllTagsPanel"
import AllTagsPanel from "../AllTagsPanel.svelte"
import BackgroundMapSwitch from "../BigComponents/BackgroundMapSwitch"
import { Feature, Point } from "geojson"
import DivContainer from "../Base/DivContainer"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import SvelteUIElement from "../Base/SvelteUIElement"
class PreviewPanel extends ScrollableFullScreen {
constructor(tags: UIEventSource<any>) {
super(
(_) => new FixedUiElement("Element to import"),
(_) => new Combine(["The tags are:", new AllTagsPanel(tags)]).SetClass("flex flex-col"),
(_) =>
new Combine([
"The tags are:",
new SvelteUIElement(AllTagsPanel, { tags }),
]).SetClass("flex flex-col"),
"element"
)
}

View file

@ -20,7 +20,7 @@ import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { NearbyImageVis } from "./Popup/NearbyImageVis"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Stores, UIEventSource } from "../Logic/UIEventSource"
import { AllTagsPanel } from "./AllTagsPanel"
import AllTagsPanel from "./AllTagsPanel.svelte"
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
import { ImageCarousel } from "./Image/ImageCarousel"
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
@ -53,6 +53,7 @@ import AutoApplyButton from "./Popup/AutoApplyButton"
import { LanguageElement } from "./Popup/LanguageElement"
import FeatureReviews from "../Logic/Web/MangroveReviews"
import Maproulette from "../Logic/Maproulette"
import SvelteUIElement from "./Base/SvelteUIElement"
export default class SpecialVisualizations {
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
@ -160,7 +161,8 @@ export default class SpecialVisualizations {
funcName: "all_tags",
docs: "Prints all key-value pairs of the object - used for debugging",
args: [],
constr: (state, tags: UIEventSource<any>) => new AllTagsPanel(tags, state),
constr: (state, tags: UIEventSource<any>) =>
new SvelteUIElement(AllTagsPanel, { tags, state }),
},
{
funcName: "image_carousel",

View file

@ -2,6 +2,7 @@ import Locale from "./Locale"
import { Utils } from "../../Utils"
import BaseUIElement from "../BaseUIElement"
import LinkToWeblate from "../Base/LinkToWeblate"
import { SvelteComponent } from "svelte"
export class Translation extends BaseUIElement {
public static forcedLanguage = undefined

View file

@ -268,8 +268,19 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return hist
}
/**
* Removes all empty strings from this list
* If undefined or null is given, an empty list is returned
*
* Utils.NoEmpty(undefined) // => []
* Utils.NoEmpty(["abc","","def", null]) // => ["abc","def", null]
*
*/
public static NoEmpty(array: string[]): string[] {
const ls: string[] = []
if (!array) {
return ls
}
for (const t of array) {
if (t === "") {
continue

View file

@ -0,0 +1,355 @@
{
"OSM-Discord": {
"id": "OSM-Discord",
"type": "discord",
"account": "openstreetmap",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"de",
"en",
"es",
"fr",
"it",
"pt-BR",
"ro",
"tr"
],
"order": 6,
"strings": {
"name": "OpenStreetMap World Discord"
},
"contacts": [
{
"name": "Austin Harrison",
"email": "jaustinharrison@gmail.com"
}
],
"resolved": {
"name": "OpenStreetMap World Discord",
"url": "https://discord.gg/openstreetmap",
"description": "Get in touch with other mappers on Discord",
"nameHTML": "<a target=\"_blank\" href=\"https://discord.gg/openstreetmap\">OpenStreetMap World Discord</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://discord.gg/openstreetmap\">https://discord.gg/openstreetmap</a>",
"descriptionHTML": "Get in touch with other mappers on Discord"
}
},
"OSM-Discourse": {
"id": "OSM-Discourse",
"type": "discourse",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"de",
"en",
"es",
"nl",
"pl",
"pt-BR"
],
"order": 7,
"strings": {
"name": "OpenStreetMap Discourse",
"description": "A shared place for conversations about OpenStreetMap",
"url": "https://community.openstreetmap.org/"
},
"contacts": [
{
"name": "Grant Slater",
"email": "osmfuture@firefishy.com"
},
{
"name": "Rubén Martín",
"email": "nukeador@protonmail.com"
}
],
"resolved": {
"name": "OpenStreetMap Discourse",
"url": "https://community.openstreetmap.org/",
"description": "A shared place for conversations about OpenStreetMap",
"nameHTML": "<a target=\"_blank\" href=\"https://community.openstreetmap.org/\">OpenStreetMap Discourse</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://community.openstreetmap.org/\">https://community.openstreetmap.org/</a>",
"descriptionHTML": "A shared place for conversations about OpenStreetMap"
}
},
"OSM-Facebook": {
"id": "OSM-Facebook",
"type": "facebook",
"account": "OpenStreetMap",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": 3,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap",
"description": "Like us on Facebook for news and updates about OpenStreetMap."
},
"contacts": [
{
"name": "Harry Wood",
"email": "mail@harrywood.co.uk"
}
],
"resolved": {
"name": "OpenStreetMap on Facebook",
"url": "https://www.facebook.com/OpenStreetMap",
"description": "Like us on Facebook for news and updates about OpenStreetMap.",
"nameHTML": "<a target=\"_blank\" href=\"https://www.facebook.com/OpenStreetMap\">OpenStreetMap on Facebook</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://www.facebook.com/OpenStreetMap\">https://www.facebook.com/OpenStreetMap</a>",
"descriptionHTML": "Like us on Facebook for news and updates about OpenStreetMap."
}
},
"OSM-help": {
"id": "OSM-help",
"type": "forum",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": -2,
"strings": {
"name": "OpenStreetMap Help",
"description": "Ask a question and get answers on OSM's community-driven question and answer site.",
"extendedDescription": "{url} is for everyone who needs help with OpenStreetMap. Whether you are a beginner mapper or have a technical question, we're here to help!",
"url": "https://help.openstreetmap.org/"
},
"contacts": [
{
"name": "OSMF Operations",
"email": "operations@osmfoundation.org"
}
],
"resolved": {
"name": "OpenStreetMap Help",
"url": "https://help.openstreetmap.org/",
"description": "Ask a question and get answers on OSM's community-driven question and answer site.",
"extendedDescription": "https://help.openstreetmap.org/ is for everyone who needs help with OpenStreetMap. Whether you are a beginner mapper or have a technical question, we're here to help!",
"nameHTML": "<a target=\"_blank\" href=\"https://help.openstreetmap.org/\">OpenStreetMap Help</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://help.openstreetmap.org/\">https://help.openstreetmap.org/</a>",
"descriptionHTML": "Ask a question and get answers on OSM's community-driven question and answer site.",
"extendedDescriptionHTML": "<a target=\"_blank\" href=\"https://help.openstreetmap.org/\">https://help.openstreetmap.org/</a> is for everyone who needs help with OpenStreetMap. Whether you are a beginner mapper or have a technical question, we're here to help!"
}
},
"OSM-IRC": {
"id": "OSM-IRC",
"type": "irc",
"account": "osm",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": -4,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap"
},
"contacts": [
{
"name": "Harry Wood",
"email": "mail@harrywood.co.uk"
}
],
"resolved": {
"name": "OpenStreetMap on IRC",
"url": "https://webchat.oftc.net/?channels=osm",
"description": "Join #osm on irc.oftc.net (port 6667)",
"nameHTML": "<a target=\"_blank\" href=\"https://webchat.oftc.net/?channels=osm\">OpenStreetMap on IRC</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://webchat.oftc.net/?channels=osm\">https://webchat.oftc.net/?channels=osm</a>",
"descriptionHTML": "Join #osm on irc.oftc.net (port 6667)"
}
},
"OSM-Mastodon": {
"id": "OSM-Mastodon",
"type": "mastodon",
"account": "openstreetmap",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": 3,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap",
"url": "https://en.osm.town/@openstreetmap"
},
"contacts": [
{
"name": "Harry Wood",
"email": "mail@harrywood.co.uk"
}
],
"resolved": {
"name": "OpenStreetMap Mastodon Account",
"url": "https://en.osm.town/@openstreetmap",
"description": "The official Mastodon account for OpenStreetMap",
"nameHTML": "<a target=\"_blank\" href=\"https://en.osm.town/@openstreetmap\">OpenStreetMap Mastodon Account</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://en.osm.town/@openstreetmap\">https://en.osm.town/@openstreetmap</a>",
"descriptionHTML": "The official Mastodon account for OpenStreetMap"
}
},
"OSM-Reddit": {
"id": "OSM-Reddit",
"type": "reddit",
"account": "openstreetmap",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": 2,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap",
"description": "/r/{account} is a great place to learn more about OpenStreetMap. Ask us anything!"
},
"contacts": [
{
"name": "Serge Wroclawski",
"email": "emacsen@gmail.com"
}
],
"resolved": {
"name": "OpenStreetMap on Reddit",
"url": "https://www.reddit.com/r/openstreetmap",
"description": "/r/openstreetmap is a great place to learn more about OpenStreetMap. Ask us anything!",
"nameHTML": "<a target=\"_blank\" href=\"https://www.reddit.com/r/openstreetmap\">OpenStreetMap on Reddit</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://www.reddit.com/r/openstreetmap\">https://www.reddit.com/r/openstreetmap</a>",
"descriptionHTML": "<a target=\"_blank\" href=\"https://www.reddit.com/r/openstreetmap\">/r/openstreetmap</a> is a great place to learn more about OpenStreetMap. Ask us anything!"
}
},
"OSM-Telegram": {
"id": "OSM-Telegram",
"type": "telegram",
"account": "OpenStreetMapOrg",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": 5,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap",
"description": "Join the OpenStreetMap Telegram global supergroup at {url}"
},
"contacts": [
{
"name": "Max N",
"email": "abonnements@revolwear.com"
}
],
"resolved": {
"name": "OpenStreetMap Telegram",
"url": "https://t.me/OpenStreetMapOrg",
"description": "Join the OpenStreetMap Telegram global supergroup at https://t.me/OpenStreetMapOrg",
"nameHTML": "<a target=\"_blank\" href=\"https://t.me/OpenStreetMapOrg\">OpenStreetMap Telegram</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://t.me/OpenStreetMapOrg\">https://t.me/OpenStreetMapOrg</a>",
"descriptionHTML": "Join the OpenStreetMap Telegram global supergroup at <a target=\"_blank\" href=\"https://t.me/OpenStreetMapOrg\">https://t.me/OpenStreetMapOrg</a>"
}
},
"OSM-Twitter": {
"id": "OSM-Twitter",
"type": "twitter",
"account": "openstreetmap",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en"
],
"order": 4,
"strings": {
"community": "OpenStreetMap",
"communityID": "openstreetmap"
},
"contacts": [
{
"name": "Harry Wood",
"email": "mail@harrywood.co.uk"
}
],
"resolved": {
"name": "OpenStreetMap on Twitter",
"url": "https://twitter.com/openstreetmap",
"description": "Follow us on Twitter",
"nameHTML": "<a target=\"_blank\" href=\"https://twitter.com/openstreetmap\">OpenStreetMap on Twitter</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://twitter.com/openstreetmap\">https://twitter.com/openstreetmap</a>",
"descriptionHTML": "Follow us on Twitter"
}
},
"OSMF": {
"id": "OSMF",
"type": "osm-lc",
"locationSet": {
"include": [
"001"
]
},
"languageCodes": [
"en",
"fr",
"it",
"ja",
"nl",
"ru"
],
"order": 10,
"strings": {
"name": "OpenStreetMap Foundation",
"description": "OSMF is a UK-based not-for-profit that supports the OpenStreetMap Project",
"extendedDescription": "OSMF supports the OpenStreetMap project by fundraising, maintaining the servers which power OSM, organizing the annual State of the Map conference, and coordinating the volunteers who keep OSM running. You can show your support and have a voice in the direction of OpenStreetMap by joining as an OSMF member here: {signupUrl}",
"signupUrl": "https://join.osmfoundation.org/",
"url": "https://wiki.osmfoundation.org/wiki/Main_Page"
},
"contacts": [
{
"name": "OSMF Board",
"email": "board@osmfoundation.org"
}
],
"resolved": {
"name": "OpenStreetMap Foundation",
"url": "https://wiki.osmfoundation.org/wiki/Main_Page",
"signupUrl": "https://join.osmfoundation.org/",
"description": "OSMF is a UK-based not-for-profit that supports the OpenStreetMap Project",
"extendedDescription": "OSMF supports the OpenStreetMap project by fundraising, maintaining the servers which power OSM, organizing the annual State of the Map conference, and coordinating the volunteers who keep OSM running. You can show your support and have a voice in the direction of OpenStreetMap by joining as an OSMF member here: https://join.osmfoundation.org/",
"nameHTML": "<a target=\"_blank\" href=\"https://wiki.osmfoundation.org/wiki/Main_Page\">OpenStreetMap Foundation</a>",
"urlHTML": "<a target=\"_blank\" href=\"https://wiki.osmfoundation.org/wiki/Main_Page\">https://wiki.osmfoundation.org/wiki/Main_Page</a>",
"signupUrlHTML": "<a target=\"_blank\" href=\"https://join.osmfoundation.org/\">https://join.osmfoundation.org/</a>",
"descriptionHTML": "OSMF is a UK-based not-for-profit that supports the OpenStreetMap Project",
"extendedDescriptionHTML": "OSMF supports the OpenStreetMap project by fundraising, maintaining the servers which power OSM, organizing the annual State of the Map conference, and coordinating the volunteers who keep OSM running. You can show your support and have a voice in the direction of OpenStreetMap by joining as an OSMF member here: <a target=\"_blank\" href=\"https://join.osmfoundation.org/\">https://join.osmfoundation.org/</a>"
}
}
}

View file

@ -320,7 +320,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": "service:bicycle:pump=yes",

View file

@ -781,7 +781,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": "service:bicycle:pump=yes",

View file

@ -334,7 +334,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"label": {

View file

@ -166,7 +166,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"iconSize": {

View file

@ -213,7 +213,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"iconSize": {

View file

@ -94,7 +94,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
]
}

View file

@ -402,7 +402,7 @@
"opening_hours~*"
]
},
"then": "isOpen"
"then": "icons.isOpen"
}
]
}

View file

@ -1046,7 +1046,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": {

View file

@ -0,0 +1,131 @@
{
"id": "icons",
"description": {
"en": "A layer acting as library for icon-tagrenderings, especially to show as badge next to a POI"
},
"source": {
"osmTags": "id~*"
},
"title": null,
"tagRenderings": [
{
"id": "wikipedialink",
"labels": [
"defaults"
],
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' textmode='\uD83D\uDCD6' alt='Wikipedia'/></a>",
"condition": {
"or": [
"wikipedia~*",
"wikidata~*"
]
},
"mappings": [
{
"#": "ignore-image-in-then",
"if": "wikipedia=",
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
}
]
},
{
"id": "isOpen",
"labels": [
"defaults"
],
"#": "Shows a coloured clock if opening hours are parsed. Uses the metatagging, suitable to use as a (badged) overlay",
"mappings": [
{
"if": "_isOpen=yes",
"then": "clock:#0f0;ring:#0f0"
},
{
"if": "_isOpen=no",
"then": "circle:#f00;clock:#fff"
},
{
"#": "Still loading the country",
"if": {
"and": [
"_isOpen=",
"opening_hours~*"
]
},
"then": "clock:#ff0;ring:#ff0"
},
{
"#": "Still loading the country",
"if": {
"and": [
"_isOpen=parse_error",
"opening_hours~*"
]
},
"then": "circle:#f0f;clock:#fff"
}
]
},
{
"id": "phonelink",
"labels": [
"defaults"
],
"render": "<a href='tel:{phone}'><img textmode='\uD83D\uDCDE' alt='phone' src='./assets/tagRenderings/phone.svg'/></a>",
"condition": "phone~*"
},
{
"id": "emaillink",
"labels": [
"defaults"
],
"render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/tagRenderings/send_email.svg'/></a>",
"condition": "email~*"
},
{
"id": "smokingicon",
"labels": [
"defaults"
],
"mappings": [
{
"#": "ignore-image-in-then",
"if": "smoking=no",
"then": "<img textmode='\uD83D\uDEAD' alt='no-smoking' src='./assets/tagRenderings/no_smoking.svg'/>"
},
{
"#": "ignore-image-in-then",
"if": "smoking=yes",
"then": "<img textmode='\uD83D\uDEAC' alt='smoking-allowed' src='./assets/tagRenderings/smoking.svg'/>"
}
]
},
{
"id": "sharelink",
"labels": [
"defaults"
],
"render": "{share_link()}"
},
{
"id": "osmlink",
"labels": [
"defaults"
],
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img alt='on osm' textmode='\uD83D\uDDFA' src='./assets/svg/osm-logo-us.svg'/></a>",
"mappings": [
{
"if": "id~.*/-.*",
"then": ""
},
{
"#": "ignore-image-in-then",
"if": "_backend~*",
"then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>"
}
],
"condition": "id~(node|way|relation)/[0-9]*"
}
],
"mapRendering": null
}

View file

@ -259,7 +259,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"iconSize": "40,40,center",

View file

@ -120,7 +120,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"label": {

View file

@ -578,7 +578,7 @@
"opening_hours~*"
]
},
"then": "isOpen"
"then": "icons.isOpen"
}
],
"iconSize": {

View file

@ -433,7 +433,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": "shop~*",

View file

@ -326,7 +326,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": {

View file

@ -558,7 +558,7 @@
"opening_hours~*"
]
},
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": {

View file

@ -49,7 +49,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"location": [

View file

@ -700,7 +700,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
],
"location": [

View file

@ -375,7 +375,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
},
{
"if": "shop~*",

View file

@ -79,7 +79,7 @@
"iconBadges": [
{
"if": "opening_hours~*",
"then": "isOpen"
"then": "icons.isOpen"
}
]
},

66
assets/svg/community.svg Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1124.975mm"
height="1111.9373mm"
viewBox="0 0 1124.975 1111.9373"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="community.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-bbox="true"
inkscape:object-paths="true"
inkscape:zoom="0.077809868"
inkscape:cx="2448.2756"
inkscape:cy="3180.8305"
inkscape:window-width="1717"
inkscape:window-height="885"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(429.38456,373.85159)">
<path
id="path858"
style="color:#000000;fill:#000000;stroke:none;stroke-width:18.8976;stroke-linejoin:round;stroke-miterlimit:10.6207;stroke-dasharray:none;stroke-opacity:1"
d="m 465.50195,-1412.9824 c -48.13018,0 -95.14167,6.6605 -140.92968,19.3242 -517.78906,36.4545 -1004.59215,263.4542 -1364.78127,639.11132 -125.578,130.97093 -232.0356,276.80492 -318.709,432.875 l -3.0996,0.006 0.012,5.62891 c -169.1451,306.7883664 -260.6208,653.52706 -260.8652,1009.62695 v 0.0645 c 0,555.45592 220.6943,1088.26072 613.4609,1481.02732 378.0703,378.0702 885.96964,596.0875 1418.85161,611.92 18.51351,1.9864 37.20113,3.0136 56.06054,3.0136 17.25834,0 34.36078,-0.9283 51.33008,-2.5937 224.45873,-4.8504 444.64455,-45.7063 652.83597,-119.1836 -60.8675,-43.0776 -118.1478,-91.0122 -171.27933,-143.334 -121.72268,40.4323 -230.24225,90.5328 -428.68164,58.6602 l -3.38086,-1775.19732 233.78516,-0.44532 c 42.42213,-67.09791 90.54812,-130.41487 143.84375,-189.24804 l -377.98828,0.7207 -1.43946,-755.89648 634.26176,-1.38672 c 32.7782,154.928763 56.3858,319.47347 69.9629,489.41797 58.6612,-33.17515 119.6711,-62.01386 182.541,-86.28516 -13.1747,-138.65014 -32.7954,-273.7956044 -58.7852,-403.55664 l 791.918,-1.73242 c 54.2995,111.507047 97.5037,228.170813 129.2012,348.26953 72.3897,18.82425 143.1313,43.49582 211.5273,73.77148 -80.6791,-402.00209 -278.3137,-774.77273 -572.5098,-1068.96875 -363.7812,-363.78173 -847.7515,-579.57553 -1358.52731,-609.49993 -41.8967,-10.5343 -84.78727,-16.1094 -128.61524,-16.1094 z m 96.98438,204.4375 c 159.67283,49.2174 326.54513,218.8615 462.03907,516.93552 49.6393,109.20202 92.1295,232.60044 128.4531,364.44727 l -588.81053,1.28711 z m -188.97461,0.9179 1.67969,882.16411 -597.98243,1.30664 c 36.4911,-132.99204 79.25432,-257.42426 129.269536,-367.45313 133.780665,-294.30513 298.164144,-463.66612 455.992184,-515.27932 3.68603,-0.2115 7.36574,-0.4597 11.04102,-0.7383 z m 591.27148,61.1954 c 320.2851,85.8427 615.6781,254.12924 854.2344,492.68551 99.0716,99.07158 185.6177,208.20254 259.6738,324.5625 l -729.584,1.59375 c -41.2536,-159.09493 -91.7465,-308.46744 -152.5469,-442.22266 -67.1207,-147.65948 -144.9895,-275.0596 -231.7773,-376.6191 z m -1003.50195,5.9433 c -84.79247,100.5848 -161.00089,225.84047 -226.83789,370.6758 -61.29809,134.85015 -112.17438,285.51379 -153.59961,446.08594 l -720.14645,1.57422 c 67.8012,-107.37947 146.33969,-208.61822 235.49997,-301.60743 238.54174,-248.78579 538.10225,-425.81263 865.08398,-516.72853 z m 414.26953,1004.00002 1.43945,755.8418 -727.68554,1.38671 c 4.20418,-264.24481 32.77031,-520.983169 82.2207,-755.82031 z m -837.92773,1.83203 c -47.33699,237.75431 -73.46184,493.4318 -77.33789,755.75781 l -892.42186,1.70117 c 10.0141,-264.11069 74.8439,-521.07351 188.0937,-755.75 z m 839.72656,942.98437 1.3457,707.00198 -646.08984,1.414 c -46.71986,-220.2353 -75.01919,-459.8106 -82.07617,-707.03121 z m -915.85156,1.74415 c 6.57514,245.41033 32.61831,484.16193 77.38867,707.09573 l -784.53902,1.7148 c -105.8005,-220.1735 -168.8074,-460.0874 -184.1446,-707.11327 z m 917.55859,894.23433 1.68359,884.2715 C 217.38802,2544.4462 45.439072,2373.9426 -93.521484,2068.2422 -142.93255,1959.5424 -185.25941,1836.7732 -221.48047,1705.6211 Z m -796.66211,1.7422 c 41.17164,158.3984 91.48351,307.1603 152.04883,440.3985 69.28935,152.4302 150.07483,283.1451 240.259765,386.2558 C -344.1327,2446.5457 -638.16035,2278.6776 -875.7832,2041.0547 -977.36504,1939.4729 -1065.801,1827.3298 -1141.0586,1707.6328 Z"
transform="scale(0.26458333)"
sodipodi:nodetypes="scscccssscsccccccccccccccccccscsccccccscccsccsccsccscccccccccccccccccccccccscccscscc" />
<path
style="fill:#000000;stroke-width:5.60043"
id="path928"
d="" />
<path
style="fill:#000000;stroke-width:5.60043"
id="path890"
d="" />
<path
id="path3123"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.68756;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 500.42418,99.52416 c -162.9348,1.72512 -108.06353,223.75011 -69.65465,274.06343 l 1.09709,35.29034 c -176.038,13.83721 -191.80907,75.90897 -198.43798,158.82719 -2.02223,25.29521 3.89762,50.2628 9.81667,80.72646 97.56502,-0.71585 177.50757,-0.62612 275.07185,-0.49796 84.27329,-45.47436 147.33813,-122.20106 175.63579,-213.68411 -28.16643,-12.43076 -66.9608,-21.14889 -120.68876,-25.37209 l 1.09762,-35.29036 C 613.0731,322.8775 663.35897,97.79884 500.42418,99.52416 Z"
sodipodi:nodetypes="sccscccccs" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -217,6 +217,14 @@
"authors": [],
"sources": []
},
{
"path": "community.svg",
"license": "CC0",
"authors": [
"Pieter Vander Vennet"
],
"sources": []
},
{
"path": "compass.svg",
"license": "CC0; trivial",

View file

@ -1,103 +0,0 @@
{
"id": "shared_icons",
"defaults": {
"builtin": [
"phonelink",
"emaillink",
"wikipedialink",
"smokingicon",
"osmlink",
"sharelink"
],
"override": {}
},
"wikipedialink": {
"render": "<a href='https://wikipedia.org/wiki/{wikipedia}' target='_blank'><img src='./assets/svg/wikipedia.svg' textmode='\uD83D\uDCD6' alt='Wikipedia'/></a>",
"condition": {
"or": [
"wikipedia~*",
"wikidata~*"
]
},
"mappings": [
{
"#": "ignore-image-in-then",
"if": "wikipedia=",
"then": "<a href='https://www.wikidata.org/wiki/{wikidata}' target='_blank'><img src='./assets/svg/wikidata.svg' alt='WD'/></a>"
}
]
},
"isOpen": {
"#": "Shows a coloured clock if opening hours are parsed. Uses the metatagging, suitable to use as a (badged) overlay",
"mappings": [
{
"if": "_isOpen=yes",
"then": "clock:#0f0;ring:#0f0"
},
{
"if": "_isOpen=no",
"then": "circle:#f00;clock:#fff"
},
{
"#": "Still loading the country",
"if": {
"and": [
"_isOpen=",
"opening_hours~*"
]
},
"then": "clock:#ff0;ring:#ff0"
},
{
"#": "Still loading the country",
"if": {
"and": [
"_isOpen=parse_error",
"opening_hours~*"
]
},
"then": "circle:#f0f;clock:#fff"
}
]
},
"phonelink": {
"render": "<a href='tel:{phone}'><img textmode='\uD83D\uDCDE' alt='phone' src='./assets/tagRenderings/phone.svg'/></a>",
"condition": "phone~*"
},
"emaillink": {
"render": "<a href='mailto:{email}'><img textmode='✉️' alt='email' src='./assets/tagRenderings/send_email.svg'/></a>",
"condition": "email~*"
},
"smokingicon": {
"mappings": [
{
"#": "ignore-image-in-then",
"if": "smoking=no",
"then": "<img textmode='\uD83D\uDEAD' alt='no-smoking' src='./assets/tagRenderings/no_smoking.svg'/>"
},
{
"#": "ignore-image-in-then",
"if": "smoking=yes",
"then": "<img textmode='\uD83D\uDEAC' alt='smoking-allowed' src='./assets/tagRenderings/smoking.svg'/>"
}
]
},
"osmlink": {
"render": "<a href='https://openstreetmap.org/{id}' target='_blank'><img alt='on osm' textmode='\uD83D\uDDFA' src='./assets/svg/osm-logo-us.svg'/></a>",
"mappings": [
{
"if": "id~.*/-.*",
"then": ""
},
{
"#": "ignore-image-in-then",
"if": "_backend~*",
"then": "<a href='{_backend}/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'/></a>"
}
],
"condition": "id~(node|way|relation)/[0-9]*"
},
"sharelink": {
"render": "{share_link()}"
}
}

View file

@ -762,10 +762,6 @@ video {
margin: 2rem;
}
.m-1 {
margin: 0.25rem;
}
.m-5 {
margin: 1.25rem;
}
@ -790,6 +786,10 @@ video {
margin: 0.75rem;
}
.m-1 {
margin: 0.25rem;
}
.m-6 {
margin: 1.5rem;
}
@ -803,6 +803,16 @@ video {
margin-bottom: 0.5rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.mx-10 {
margin-left: 2.5rem;
margin-right: 2.5rem;
@ -813,11 +823,6 @@ video {
margin-bottom: 0.75rem;
}
.my-4 {
margin-top: 1rem;
margin-bottom: 1rem;
}
.mb-4 {
margin-bottom: 1rem;
}
@ -990,15 +995,15 @@ video {
height: 100%;
}
.h-64 {
height: 16rem;
}
.h-min {
height: -webkit-min-content;
height: min-content;
}
.h-64 {
height: 16rem;
}
.h-8 {
height: 2rem;
}
@ -1071,6 +1076,10 @@ video {
max-height: 2rem;
}
.min-h-\[8rem\] {
min-height: 8rem;
}
.w-full {
width: 100%;
}
@ -1127,6 +1136,12 @@ video {
width: 2.75rem;
}
.w-fit {
width: -webkit-fit-content;
width: -moz-fit-content;
width: fit-content;
}
.w-max {
width: -webkit-max-content;
width: max-content;
@ -1141,10 +1156,6 @@ video {
width: min-content;
}
.w-64 {
width: 16rem;
}
.w-auto {
width: auto;
}
@ -1195,6 +1206,11 @@ video {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.\!transform {
-webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
@ -1271,10 +1287,6 @@ video {
align-content: flex-start;
}
.items-end {
align-items: flex-end;
}
.items-center {
align-items: center;
}
@ -1360,28 +1372,32 @@ video {
word-break: break-all;
}
.rounded-full {
border-radius: 9999px;
}
.rounded-3xl {
border-radius: 1.5rem;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-md {
border-radius: 0.375rem;
.rounded-xl {
border-radius: 0.75rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-xl {
border-radius: 0.75rem;
.rounded {
border-radius: 0.25rem;
}
.rounded-2xl {
border-radius: 1rem;
}
.rounded-full {
border-radius: 9999px;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-sm {
@ -1421,6 +1437,10 @@ video {
border-bottom-width: 2px;
}
.border-solid {
border-style: solid;
}
.border-dotted {
border-style: dotted;
}
@ -1430,6 +1450,11 @@ video {
border-color: rgb(0 0 0 / var(--tw-border-opacity));
}
.border-subtle {
--tw-border-opacity: 1;
border-color: rgb(219 234 254 / var(--tw-border-opacity));
}
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -1440,6 +1465,11 @@ video {
border-color: rgb(252 165 165 / var(--tw-border-opacity));
}
.border-lime-500 {
--tw-border-opacity: 1;
border-color: rgb(132 204 22 / var(--tw-border-opacity));
}
.border-gray-400 {
--tw-border-opacity: 1;
border-color: rgb(156 163 175 / var(--tw-border-opacity));
@ -1469,11 +1499,21 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-unsubtle {
--tw-bg-opacity: 1;
background-color: rgb(191 219 254 / var(--tw-bg-opacity));
}
.bg-red-400 {
--tw-bg-opacity: 1;
background-color: rgb(248 113 113 / var(--tw-bg-opacity));
}
.bg-subtle {
--tw-bg-opacity: 1;
background-color: rgb(219 234 254 / var(--tw-bg-opacity));
}
.bg-gray-400 {
--tw-bg-opacity: 1;
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
@ -1514,10 +1554,6 @@ video {
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
}
.p-3 {
padding: 0.75rem;
}
.p-4 {
padding: 1rem;
}
@ -1530,6 +1566,10 @@ video {
padding: 0.25rem;
}
.p-3 {
padding: 0.75rem;
}
.p-0 {
padding: 0px;
}
@ -1538,6 +1578,16 @@ video {
padding: 0.125rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-0 {
padding-left: 0px;
padding-right: 0px;
@ -1741,10 +1791,19 @@ video {
color: rgb(22 163 74 / var(--tw-text-opacity));
}
.text-\[\#999\] {
--tw-text-opacity: 1;
color: rgb(153 153 153 / var(--tw-text-opacity));
}
.underline {
text-decoration-line: underline;
}
.overline {
text-decoration-line: overline;
}
.line-through {
text-decoration-line: line-through;
}
@ -1817,14 +1876,14 @@ video {
transition-duration: 150ms;
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
.transition-\[color\2c background-color\2c box-shadow\] {
transition-property: color,background-color,box-shadow;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.transition-shadow {
transition-property: box-shadow;
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
@ -2579,6 +2638,11 @@ input {
margin-left: 1.5rem;
}
.hover\:bg-unsubtle:hover {
--tw-bg-opacity: 1;
background-color: rgb(191 219 254 / var(--tw-bg-opacity));
}
.hover\:bg-indigo-200:hover {
--tw-bg-opacity: 1;
background-color: rgb(199 210 254 / var(--tw-bg-opacity));
@ -2652,14 +2716,6 @@ input {
flex-direction: row;
}
.sm\:flex-wrap {
flex-wrap: wrap;
}
.sm\:items-start {
align-items: flex-start;
}
.sm\:items-stretch {
align-items: stretch;
}

View file

@ -5,6 +5,12 @@
"retrying": "Loading data failed. Trying again in {count} seconds…",
"zoomIn": "Zoom in to view or edit the data"
},
"communityIndex": {
"available": "This community speaks {native}",
"intro": "Get in touch with other people to get to know them, learn from them, ...",
"notAvailable": "This community does not speak {native}",
"title": "Community index"
},
"delete": {
"cancel": "Cancel",
"cannotBeDeleted": "This feature can not be deleted",

View file

@ -5,6 +5,12 @@
"retrying": "Data inladen mislukt - wordt opnieuw geprobeerd over {count} seconden",
"zoomIn": "Zoom in om de data te zien en te bewerken"
},
"communityIndex": {
"available": "Op dit communicatiekanaal spreekt men {native}",
"intro": "Contacteer anderen die bezig zijn met OpenStreetMap om kennis te maken, tips uit te wisselen of van elkaar bij te leren.",
"notAvailable": "Op dit communicatiekanaal spreekt men geen {native}",
"title": "Community index"
},
"delete": {
"cancel": "Annuleren",
"cannotBeDeleted": "Dit object kan niet van de kaart verwijderd worden",

3777
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,8 @@
"description": "A small website to edit OSM easily",
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
"homepage": "https://mapcomplete.osm.be",
"main": "index.js",
"main": "index.ts",
"type": "module",
"scripts": {
"start": "npm run generate:layeroverview && npm run strt",
"strt": "vite",
@ -13,42 +14,39 @@
"watch:css": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch",
"generate:css": "tailwindcss -i index.css -o css/index-tailwind-output.css",
"generate:doctests": "doctest-ts-improved . --ignore .*.spec.ts --ignore .*ConfigJson.ts",
"test:run-only": "mocha --require ts-node/register --require test/testhooks.ts \"./**/*.doctest.ts\" \"test/*\" \"test/**/*.ts\"",
"test:run-only": "vitest --run test",
"test": "(npm run generate:doctests 2>&1 | grep -v \"No doctests found in\") && npm run test:run-only && npm run clean:tests",
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean && npm run weblate-add-upstream",
"generate:editor-layer-index": "ts-node scripts/downloadFile.ts https://osmlab.github.io/editor-layer-index/imagery.geojson assets/editor-layer-index.json",
"generate:polygon-features": "ts-node scripts/downloadFile.ts https://raw.githubusercontent.com/tyrasd/osm-polygon-features/master/polygon-features.json assets/polygon-features.json",
"generate:images": "ts-node scripts/generateIncludedImages.ts",
"generate:translations": "ts-node scripts/generateTranslations.ts",
"watch:translations": "cd langs && ls | entr -c npm run generate:translations",
"reset:translations": "ts-node scripts/generateTranslations.ts --ignore-weblate",
"generate:layouts": "ts-node scripts/generateLayouts.ts",
"generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts",
"generate:cache:speelplekken:mini": "ts-node scripts/generateCache.ts speelplekken 14 ../MapComplete-data/speelplekken_cache_mini/ 51.181710380278176 4.423413276672363 51.193007664772495 4.444141387939452",
"generate:cache:speelplekken": "npm run generate:layeroverview && ts-node scripts/generateCache.ts speelplekken 14 ../MapComplete-data/speelplekken_cache/ 51.20 4.35 51.09 4.56",
"generate:cache:natuurpunt": "npm run generate:layeroverview && ts-node scripts/generateCache.ts natuurpunt 12 ../MapComplete-data/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:cache:natuurpunt:mini": "ts-node scripts/generateCache.ts natuurpunt 12 ../../git/MapComplete-data/natuurpunt_cache_mini/ 51.00792239979105 4.497699737548828 51.0353492224462554 4.539070129394531 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:layeroverview": "ts-node scripts/generateLayerOverview.ts",
"generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail",
"query:licenses": "ts-node scripts/generateLicenseInfo.ts --query",
"generate:contributor-list": "ts-node scripts/generateContributors.ts",
"generate:schemas": "ts2json-schema -p Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && ts-node scripts/fixSchemas.ts ",
"generate:editor-layer-index": "vite-node scripts/downloadFile.ts -- https://osmlab.github.io/editor-layer-index/imagery.geojson assets/editor-layer-index.json",
"generate:polygon-features": "vite-node scripts/downloadFile.ts -- https://raw.githubusercontent.com/tyrasd/osm-polygon-features/master/polygon-features.json assets/polygon-features.json",
"generate:images": "vite-node scripts/generateIncludedImages.ts",
"generate:translations": "vite-node scripts/generateTranslations.ts",
"reset:translations": "vite-node scripts/generateTranslations.ts -- --ignore-weblate",
"generate:layouts": "vite-node scripts/generateLayouts.ts",
"generate:docs": "vite-node scripts/generateDocs.ts && vite-node scripts/generateTaginfoProjectFiles.ts",
"generate:cache:speelplekken": "npm run generate:layeroverview && vite-node scripts/generateCache.ts -- speelplekken 14 ../MapComplete-data/speelplekken_cache/ 51.20 4.35 51.09 4.56",
"generate:cache:natuurpunt": "npm run generate:layeroverview && vite-node scripts/generateCache.ts -- natuurpunt 12 ../MapComplete-data/natuurpunt_cache/ 50.40 2.1 51.54 6.4 --generate-point-overview nature_reserve,visitor_information_centre",
"generate:layeroverview": "export NODE_OPTIONS=\"--max-old-space-size=8192\" && vite-node scripts/generateLayerOverview.ts",
"generate:licenses": "vite-node scripts/generateLicenseInfo.ts -- --no-fail",
"query:licenses": "vite-node scripts/generateLicenseInfo.ts -- --query",
"generate:contributor-list": "vite-node scripts/generateContributors.ts",
"generate:schemas": "ts2json-schema -p Models/ThemeConfig/Json/ -o Docs/Schemas/ -t tsconfig.json -R . -m \".*ConfigJson\" && vite-node scripts/fixSchemas.ts ",
"generate:service-worker": "tsc service-worker.ts --outFile public/service-worker.js && git_hash=$(git rev-parse HEAD) && sed -i \"s/GITHUB-COMMIT/$git_hash/\" public/service-worker.js",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"generate:stats": "ts-node scripts/GenerateSeries.ts",
"reset:layeroverview": "echo {\\\"themes\\\":[]} > ./assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json && rm -f ./assets/generated/layers/*.json && rm -f ./assets/generated/themes/*.json && npm run generate:layeroverview && ts-node scripts/generateLayerOverview.ts --force",
"generate:stats": "vite-node scripts/GenerateSeries.ts",
"reset:layeroverview": "echo {\\\"themes\\\":[]} > ./assets/generated/known_themes.json && echo {\\\"layers\\\": []} > ./assets/generated/known_layers.json && rm -f ./assets/generated/layers/*.json && rm -f ./assets/generated/themes/*.json && npm run generate:layeroverview && vite-node scripts/generateLayerOverview.ts -- --force",
"generate": "mkdir -p ./assets/generated; npm run generate:licenses; npm run generate:images; npm run generate:charging-stations; npm run generate:translations; npm run reset:layeroverview; npm run generate:service-worker",
"generate:charging-stations": "cd ./assets/layers/charging_station && ts-node csvToJson.ts && cd -",
"generate:charging-stations": "cd ./assets/layers/charging_station && vite-node csvToJson.ts && cd -",
"prepare-deploy": "npm run generate:service-worker && ./scripts/build.sh",
"format": "npx prettier --write '**/*.ts'",
"clean:tests": "(find . -type f -name \"*.doctest.ts\" | xargs rm)",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|import_viewer\\|theme\\).html\" | xargs rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs rm)",
"format": "npx prettier --write --svelte-bracket-new-line=false --html-whitespace-sensitivity=ignore '**/*.ts' '**/*.svelte'",
"clean:tests": "(find . -type f -name \"*.doctest.ts\" | xargs -r rm)",
"clean": "rm -rf .cache/ && (find *.html | grep -v \"^\\(404\\|index\\|land\\|test\\|preferences\\|customGenerator\\|professional\\|automaton\\|import_helper\\|import_viewer\\|theme\\).html\" | xargs -r rm) && (ls | grep \"^index_[a-zA-Z_-]\\+\\.ts$\" | xargs -r rm) && (ls | grep \".*.webmanifest$\" | grep -v \"manifest.webmanifest\" | xargs -r rm)",
"generate:dependency-graph": "node_modules/.bin/depcruise --exclude \"^node_modules\" --output-type dot Logic/State/MapState.ts > dependencies.dot && dot dependencies.dot -T svg -o dependencies.svg && rm dependencies.dot",
"weblate-add-upstream": "git remote add weblate-github git@github.com:weblate/MapComplete.git && git remote add weblate-hosted-core https://hosted.weblate.org/git/mapcomplete/core/ && git remote add weblate-hosted-layers https://hosted.weblate.org/git/mapcomplete/layers/",
"weblate-merge": "git remote update weblate-github; git merge weblate-github/weblate-mapcomplete-core weblate-github/weblate-mapcomplete-layers weblate-github/weblate-mapcomplete-layer-translations",
"weblate-fix-heavy": "git fetch weblate-hosted-layers; git fetch weblate-hosted-core; git merge weblate-hosted-layers/master weblate-hosted-core/master ",
"housekeeping": "git pull && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && ts-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* && git commit -m 'chore: automated housekeeping...'",
"parseSchools": "ts-node scripts/schools/amendSchoolData.ts"
"housekeeping": "git pull && npm run weblate-fix-heavy && npm run generate && npm run generate:docs && npm run generate:contributor-list && vite-node scripts/fetchLanguages.ts && npm run format && git add assets/ langs/ Docs/ **/*.ts Docs/* && git commit -m 'chore: automated housekeeping...'",
"parseSchools": "vite-node scripts/schools/amendSchoolData.ts"
},
"keywords": [
"OpenStreetMap",
@ -63,6 +61,7 @@
"not op_mini all"
],
"dependencies": {
"@rollup/plugin-typescript": "^11.0.0",
"@turf/boolean-intersects": "^6.5.0",
"@turf/buffer": "^6.5.0",
"@turf/collect": "^6.5.0",
@ -80,6 +79,7 @@
"geojson2svg": "^1.3.1",
"i18next-client": "^1.11.4",
"idb-keyval": "^6.0.3",
"jest-mock": "^29.4.1",
"jspdf": "^2.5.1",
"latlon2country": "^1.2.6",
"leaflet": "^1.9.2",
@ -99,6 +99,8 @@
"svg-path-parser": "^1.1.0",
"tailwindcss": "^3.1.8",
"togpx": "^0.5.4",
"vite-node": "^0.28.3",
"vitest": "^0.28.3",
"wikibase-sdk": "^7.14.0",
"wikidata-sdk": "^7.14.0",
"xml2js": "^0.4.23"
@ -108,12 +110,13 @@
"@babel/preset-env": "7.13.8",
"@parcel/service-worker": "^2.6.0",
"@rollup/plugin-json": "^6.0.0",
"@sveltejs/vite-plugin-svelte": "^2.0.2",
"@tsconfig/svelte": "^3.0.0",
"@types/chai": "^4.3.0",
"@types/geojson": "^7946.0.10",
"@types/leaflet-markercluster": "^1.0.3",
"@types/leaflet-providers": "^1.2.0",
"@types/lz-string": "^1.3.34",
"@types/mocha": "^9.1.0",
"@types/node": "^18.11.18",
"@types/papaparse": "^5.3.1",
"@types/prompt-sync": "^4.1.0",
@ -123,12 +126,18 @@
"chai": "^4.3.6",
"dependency-cruiser": "^10.4.0",
"fs": "0.0.1-security",
"mocha": "^9.2.2",
"prettier": "2.7.1",
"jsdom": "^21.1.0",
"prettier": "^2.7.1",
"prettier-plugin-svelte": "^2.9.0",
"read-file": "^0.2.0",
"sass": "^1.58.0",
"sharp": "^0.30.5",
"svelte": "^3.55.1",
"svelte-check": "^3.0.2",
"svelte-preprocess": "^5.0.1",
"ts-node": "^10.9.1",
"ts2json-schema": "^1.4.0",
"tslib": "^2.5.0",
"tslint": "^6.1.3",
"tslint-no-circular-imports": "^0.7.0",
"typescript": "^4.7.4",

5
postcss.config.cjs Normal file
View file

@ -0,0 +1,5 @@
const tailwindcss = require("tailwindcss")
module.exports = {
plugins: [tailwindcss("./tailwind.config.cjs")],
}

View file

@ -9,6 +9,8 @@ rm -rf .cache
mkdir dist 2> /dev/null
mkdir dist/assets 2> /dev/null
export NODE_OPTIONS="--max-old-space-size=8192"
# This script ends every line with '&&' to chain everything. A failure will thus stop the build
npm run generate:editor-layer-index &&
npm run generate &&
@ -56,3 +58,5 @@ cp -r assets/templates/ dist/assets/templates/
cp -r assets/tagRenderings/ dist/assets/tagRenderings/
cp assets/*.png dist/assets/
cp assets/*.svg dist/assets/
export NODE_OPTIONS=""

View file

@ -15,7 +15,7 @@ function main(args: string[]) {
const layerId = args[1]
const themePath = "./assets/themes/" + themeId + "/" + themeId + ".json"
const contents = <LayoutConfigJson>JSON.parse(readFileSync(themePath, "UTF-8"))
const contents = <LayoutConfigJson>JSON.parse(readFileSync(themePath, { encoding: "utf8" }))
const layers = <LayerConfigJson[]>contents.layers.filter((l) => {
if (typeof l === "string") {
return false

View file

@ -21,7 +21,7 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
import TiledFeatureSource from "../Logic/FeatureSource/TiledFeatureSource/TiledFeatureSource"
import Constants from "../Models/Constants"
import { GeoOperations } from "../Logic/GeoOperations"
import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"
import SimpleMetaTaggers, { ReferencingWaysMetaTagger } from "../Logic/SimpleMetaTagger"
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
import Loc from "../Models/Loc"
import { Feature } from "geojson"
@ -488,6 +488,7 @@ function sliceToTiles(
export async function main(args: string[]) {
console.log("Cache builder started with args ", args.join(", "))
ReferencingWaysMetaTagger.enabled = false
if (args.length < 6) {
console.error(
"Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] [--clip]" +

View file

@ -1,10 +1,23 @@
import { exec } from "child_process"
import { writeFile, writeFileSync } from "fs"
function asList(hist: Map<string, number>): {
contributors: { contributor: string; commits: number }[]
} {
const ls = []
interface Contributor {
/**
* The name of the contributor
*/
contributor: string
/**
* The number of commits
*/
commits: number
}
interface ContributorList {
contributors: Contributor[]
}
function asList(hist: Map<string, number>): ContributorList {
const ls: Contributor[] = []
hist.forEach((commits, contributor) => {
ls.push({ commits, contributor })
})

View file

@ -16,7 +16,6 @@ import {
import { Translation } from "../UI/i18n/Translation"
import { TagRenderingConfigJson } from "../Models/ThemeConfig/Json/TagRenderingConfigJson"
import questions from "../assets/tagRenderings/questions.json"
import icons from "../assets/tagRenderings/icons.json"
import PointRenderingConfigJson from "../Models/ThemeConfig/Json/PointRenderingConfigJson"
import { PrepareLayer } from "../Models/ThemeConfig/Conversion/PrepareLayer"
import { PrepareTheme } from "../Models/ThemeConfig/Conversion/PrepareTheme"
@ -168,21 +167,6 @@ class LayerOverviewUtils {
)
dict.set(key, config)
}
for (const key in icons) {
if (key === "id") {
continue
}
if (typeof icons[key] !== "object") {
continue
}
icons[key].id = key
const config = <TagRenderingConfigJson>icons[key]
validator.convertStrict(
config,
"generate-layer-overview:tagRenderings/icons.json:" + key
)
dict.set(key, config)
}
dict.forEach((value, key) => {
if (key === "id") {
@ -255,7 +239,7 @@ class LayerOverviewUtils {
const sharedLayers = this.buildLayerIndex(doesImageExist, forceReload)
const recompiledThemes: string[] = []
const sharedThemes = this.buildThemeIndex(
doesImageExist,
licensePaths,
sharedLayers,
recompiledThemes,
forceReload
@ -384,7 +368,7 @@ class LayerOverviewUtils {
}
private buildThemeIndex(
doesImageExist: DoesImageExist,
licensePaths: Set<string>,
sharedLayers: Map<string, LayerConfigJson>,
recompiledThemes: string[],
forceReload: boolean
@ -399,9 +383,26 @@ class LayerOverviewUtils {
const convertState: DesugaringContext = {
sharedLayers,
tagRenderings: this.getSharedTagRenderings(doesImageExist),
tagRenderings: this.getSharedTagRenderings(
new DoesImageExist(licensePaths, existsSync)
),
publicLayers,
}
const knownTagRenderings = new Set<string>()
convertState.tagRenderings.forEach((_, key) => knownTagRenderings.add(key))
sharedLayers.forEach((layer) => {
for (const tagRendering of layer.tagRenderings ?? []) {
if (tagRendering["id"]) {
knownTagRenderings.add(layer.id + "." + tagRendering["id"])
}
if (tagRendering["labels"]) {
for (const label of tagRendering["labels"]) {
knownTagRenderings.add(layer.id + "." + label)
}
}
}
})
const skippedThemes: string[] = []
for (const themeInfo of themeFiles) {
const themePath = themeInfo.path
@ -436,10 +437,10 @@ class LayerOverviewUtils {
themeFile = new PrepareTheme(convertState).convertStrict(themeFile, themePath)
new ValidateThemeAndLayers(
doesImageExist,
new DoesImageExist(licensePaths, existsSync, knownTagRenderings),
themePath,
true,
convertState.tagRenderings
knownTagRenderings
).convertStrict(themeFile, themePath)
if (themeFile.icon.endsWith(".svg")) {

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
# Little scripts to parse Belgian school data

View file

@ -1,364 +0,0 @@
import { parse } from "csv-parse/sync"
import { readFileSync, writeFileSync } from "fs"
import { Utils } from "../../Utils"
import { GeoJSONObject, geometry } from "@turf/turf"
function parseAndClean(filename: string): Record<any, string>[] {
const csvOptions = {
columns: true,
skip_empty_lines: true,
trim: true,
}
const records: Record<any, string>[] = parse(readFileSync(filename), csvOptions)
return records.map((r) => {
for (const key of Object.keys(r)) {
if (r[key].endsWith("niet van toepassing")) {
delete r[key]
}
}
return r
})
}
const structuren = {
"Voltijds Gewoon Secundair Onderwijs": "secondary",
"Gewoon Lager Onderwijs": "primary",
"Gewoon Kleuteronderwijs": "kindergarten",
Kleuteronderwijs: "kindergarten",
"Buitengewoon Lager Onderwijs": "primary",
"Buitengewoon Secundair Onderwijs": "secondary",
"Buitengewoon Kleuteronderwijs": "kindergarten",
"Deeltijds Beroepssecundair Onderwijs": "secondary",
}
const degreesMapping = {
"Derde graad": "upper_secondary",
"Tweede graad": "middle_secondary",
"Eerste graad": "lower_secondary",
}
const classificationOrder = [
"kindergarten",
"primary",
"secondary",
"lower_secondary",
"middle_secondary",
"upper_secondary",
]
const stelselsMapping = {
"Beide stelsels": "linear_courses;modular_courses",
"Lineair stelsel": "linear_courses",
"Modulair stelsel": "modular_courses",
}
const rmKeys = [
"schoolnummer",
"instellingstype",
"adres",
"begindatum",
"hoofdzetel",
"huisnummer",
"kbo-nummer",
"beheerder(s)",
"bestuur",
"clb",
"ingerichte hoofdstructuren",
"busnummer",
"crab-code",
"crab-huisnr",
"einddatum",
"fax",
"gemeente",
"intern_vplnummer",
"kbo_nummer",
"lx",
"ly",
"niscode",
"onderwijsniveau",
"onderwijsvorm",
"scholengemeenschap",
"postcode",
"provincie",
"provinciecode",
"soort instelling",
"status erkenning",
"straat",
"VWO-vestigingsplaatscode",
"taalstelsel",
"net",
]
const rename = {
"e-mail": "email",
naam: "name",
telefoon: "phone",
}
function fuzzIdenticals(features: { geometry: { coordinates: [number, number] } }[]) {
var seen = new Set<string>()
for (const feature of features) {
var coors = feature.geometry.coordinates
let k = coors[0] + "," + coors[1]
while (seen.has(k)) {
coors[0] += 0.00025
k = coors[0] + "," + coors[1]
}
seen.add(k)
}
}
/**
* Sorts classifications in order
*
* sortClassifications(["primary","secondary","kindergarten"] // => ["kindergarten", "primary", "secondary"]
*/
function sortClassifications(classification: string[]) {
return classification.sort(
(a, b) => classificationOrder.indexOf(a) - classificationOrder.indexOf(b)
)
}
function main() {
console.log("Parsing schools...")
const aantallen = "/home/pietervdvn/Downloads/Scholen/aantallen.csv"
const perSchool = "/home/pietervdvn/Downloads/Scholen/perschool.csv"
const schoolfields = [
"schoolnummer",
"intern_vplnummer",
"net",
"naam",
"hoofdzetel",
"adres",
"straat",
"huisnummer",
"busnummer",
"postcode",
"gemeente",
"niscode",
"provinciecode",
"provincie",
"VWO-vestigingsplaatscode",
"crab-code",
"crab-huisnr",
"lx",
"ly",
"kbo-nummer",
"telefoon",
"fax",
"e-mail",
"website",
"beheerder(s)",
"soort instelling",
"onderwijsniveau",
"instellingstype",
"begindatum",
"einddatum",
"status erkenning",
"clb",
"bestuur",
"scholengemeenschap",
"taalstelsel",
"ingerichte hoofdstructuren",
] as const
const schoolGeojson: {
features: {
properties: Record<typeof schoolfields[number], string>
geometry: {
type: "Point"
coordinates: [number, number]
}
}[]
} = JSON.parse(readFileSync("scripts/schools/scholen.geojson", "utf8"))
fuzzIdenticals(schoolGeojson.features)
const aantallenFields = [
"schooljaar",
"nr koepel",
"koepel",
"instellingscode",
"intern volgnr vpl",
"volgnr vpl",
"naam instelling",
"GON-school",
"GOK-school",
"instellingsnummer scholengemeenschap",
"scholengemeenschap",
"code schoolbestuur",
"schoolbestuur",
"type vestigingsplaats",
"fusiegemeente hoofdvestigingsplaats",
"straatnaam vestigingsplaats",
"huisnr vestigingsplaats",
"bus vestigingsplaats",
"postcode vestigingsplaats",
"deelgemeente vestigingsplaats",
"fusiegemeente vestigingsplaats",
"hoofdstructuur (code)",
"hoofdstructuur",
"administratieve groep (code)",
"administratieve groep",
"graad lager onderwijs",
"pedagogische methode",
"graad secundair onderwijs",
"leerjaar",
"A of B-stroom",
"basisopties",
"beroepenveld",
"onderwijsvorm",
"studiegebied",
"studierichting",
"stelsel",
"okan cluster",
"type buitengewoon onderwijs",
"opleidingsvorm (code)",
"opleidingsvorm",
"fase",
"opleidingen",
"geslacht",
"aantal inschrijvingen",
] as const
const aantallenParsed: Record<typeof aantallenFields[number], string>[] =
parseAndClean(aantallen)
const perschoolFields = [
"schooljaar",
"nr koepel",
"koepel",
"instellingscode",
"naam instelling",
"straatnaam",
"huisnr",
"bus",
"postcode",
"deelgemeente",
"fusiegemeente",
"aantal inschrijvingen",
] as const
const perschoolParsed: Record<typeof perschoolFields[number], string>[] =
parseAndClean(perSchool)
schoolGeojson.features = schoolGeojson.features
.filter((sch) => sch.properties.lx != "0" && sch.properties.ly != "0")
.filter((sch) => sch.properties.instellingstype !== "Universiteit")
const c = schoolGeojson.features.length
console.log("Got ", schoolGeojson.features.length, "items after filtering")
let i = 0
let lastWrite = 0
for (const feature of schoolGeojson.features) {
i++
const now = Date.now()
if (now - lastWrite > 1000) {
lastWrite = now
console.log("Processing " + i + "/" + c)
}
const props = feature.properties
const aantallen = aantallenParsed.filter((i) => i.instellingscode == props.schoolnummer)
if (aantallen.length > 0) {
const fetch = (key: typeof aantallenFields[number]) =>
Utils.NoNull(Utils.Dedup(aantallen.map((x) => x[key])))
props["onderwijsvorm"] = fetch("onderwijsvorm").join(";")
/*
const gonSchool = aantallen.some(x => x["GON-school"] === "GON-school")
const gokSchool = aantallen.some(x => x["GOK-school"] === "GON-school")
const onderwijsvorm = fetch("onderwijsvorm")
const koepel = fetch("koepel")
const stelsel = fetch("stelsel").join(";")
const scholengemeenschap = fetch("scholengemeenschap")
*/
const hoofdstructuur = fetch("hoofdstructuur")
let specialEducation = false
let classification = hoofdstructuur.map((s) => {
const v = structuren[s]
if (s.startsWith("Buitengewoon")) {
specialEducation = true
}
if (v === undefined) {
console.error("Type not found: " + s)
return ""
}
return v
})
const graden = fetch("graad secundair onderwijs")
if (classification[0] === "secondary") {
if (graden.length !== 3) {
classification = graden.map((degree) => degreesMapping[degree])
}
}
sortClassifications(classification)
props["school"] = Utils.Dedup(classification).join("; ")
// props["koepel"] = koepel.join(";")
// props["scholengemeenschap"] = scholengemeenschap.join(";")
// props["stelsel"] = stelselsMapping[stelsel]
if (specialEducation) {
props["school:for"] = "special_education"
}
if (props.taalstelsel === "Nederlandstalig") {
props["language:nl"] = "yes"
}
if (props.instellingstype === "Instelling voor deeltijds kunstonderwijs") {
props["amenity"] = "college"
props["school:subject"] = "art"
}
}
const schoolinfo = perschoolParsed.filter((i) => i.instellingscode == props.schoolnummer)
if (schoolinfo.length == 0) {
// pass
} else if (schoolinfo.length == 1) {
props["capacity"] = schoolinfo[0]["aantal inschrijvingen"]
.split(";")
.map((i) => Number(i))
.reduce((sum, i) => sum + i, 0)
} else {
throw "Multiple schoolinfo's found for " + props.schoolnummer
}
//props["source:ref"] = props.schoolnummer
props["amenity"] = "school"
if (props["school"] === "kindergarten") {
props["amenity"] = "kindergarten"
props["isced:2011:level"] = "early_education"
delete props["school"]
}
for (const renameKey in rename) {
const into = rename[renameKey]
if (props[renameKey] !== undefined) {
props[into] = props[renameKey]
delete props[renameKey]
}
}
for (const rmKey of rmKeys) {
delete props[rmKey]
}
}
//schoolGeojson.features = schoolGeojson.features.filter(f => f.properties["capacity"] !== undefined)
/*schoolGeojson.features.forEach((f, i) => {
f.properties["id"] = "school/"+i
})*/
schoolGeojson.features = schoolGeojson.features.filter(
(f) => f.properties["amenity"] === "kindergarten"
)
writeFileSync("scripts/schools/amended_schools.geojson", JSON.stringify(schoolGeojson), "utf8")
console.log("Done")
}
if (!process.argv[1].endsWith("mocha")) {
main()
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

7
svelte.config.js Normal file
View file

@ -0,0 +1,7 @@
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
}

View file

@ -1,23 +1,27 @@
/** @type {import('tailwindcss').Config} */
const plugin = require("tailwindcss/plugin");
const plugin = require("tailwindcss/plugin")
module.exports = {
content: ["./**/*.html", "./**/*.ts"],
content: ["./**/*.{html,ts,svelte}"],
theme: {
extend: {
maxHeight: {
"65vh": "65vh",
"20vh": "20vh",
},
colors: {
subtle: "#dbeafe",
unsubtle: "#bfdbfe",
},
},
},
plugins: [
plugin(function ({ addVariant, e }) {
addVariant("landscape", ({ modifySelectors, separator }) => {
modifySelectors(({ className }) => {
return `.${e(`landscape${separator}${className}`)}:landscape`;
});
});
return `.${e(`landscape${separator}${className}`)}:landscape`
})
})
}),
],
};
}

109
test.ts
View file

@ -1,90 +1,25 @@
import MangroveReviewsOfFeature, { MangroveIdentity } from "./Logic/Web/MangroveReviews"
import { Feature, Point } from "geojson"
import { OsmTags } from "./Models/OsmFeature"
import { VariableUiElement } from "./UI/Base/VariableUIElement"
import ContactLink from "./UI/BigComponents/ContactLink.svelte"
import SvelteUIElement from "./UI/Base/SvelteUIElement"
import { Utils } from "./Utils"
import List from "./UI/Base/List"
import { UIEventSource } from "./Logic/UIEventSource"
import UserRelatedState from "./Logic/State/UserRelatedState"
import { GeoOperations } from "./Logic/GeoOperations"
import { Tiles } from "./Models/TileRange"
import { Stores } from "./Logic/UIEventSource"
const feature: Feature<Point, OsmTags> = {
type: "Feature",
id: "node/6739848322",
properties: {
"addr:city": "San Diego",
"addr:housenumber": "2816",
"addr:postcode": "92106",
"addr:street": "Historic Decatur Road",
"addr:unit": "116",
amenity: "restaurant",
cuisine: "burger",
delivery: "yes",
"diet:halal": "no",
"diet:vegetarian": "yes",
dog: "yes",
image: "https://i.imgur.com/AQlGNHQ.jpg",
internet_access: "wlan",
"internet_access:fee": "no",
"internet_access:ssid": "Public-stinebrewingCo",
microbrewery: "yes",
name: "Stone Brewing World Bistro & Gardens",
opening_hours: "Mo-Fr, Su 11:30-21:00; Sa 11:30-22:00",
organic: "no",
"payment:cards": "yes",
"payment:cash": "yes",
"service:electricity": "ask",
takeaway: "yes",
website: "https://www.stonebrewing.com/visit/bistros/liberty-station",
wheelchair: "designated",
"_last_edit:contributor": "Drew Dowling",
"_last_edit:timestamp": "2023-01-11T23:22:28Z",
id: "node/6739848322",
timestamp: "2023-01-11T23:22:28Z",
user: "Drew Dowling",
_backend: "https://www.openstreetmap.org",
_lat: "32.7404614",
_lon: "-117.211684",
_layer: "food",
_length: "0",
"_length:km": "0.0",
"_now:date": "2023-01-20",
"_now:datetime": "2023-01-20 17:46:54",
"_loaded:date": "2023-01-20",
"_loaded:datetime": "2023-01-20 17:46:54",
"_geometry:type": "Point",
_surface: "0",
"_surface:ha": "0",
_country: "us",
},
geometry: {
type: "Point",
coordinates: [0, 0],
},
}
const state = new UserRelatedState(undefined)
state.allElements.addOrGetElement(feature)
const reviews = MangroveReviewsOfFeature.construct(feature, state)
reviews.reviews.addCallbackAndRun((r) => {
console.log("Reviews are:", r)
})
window.setTimeout(async () => {
await reviews.createReview({
opinion: "Cool bar",
rating: 90,
metadata: {
nickname: "Pietervdvn",
},
})
console.log("Submitted review")
}, 1000)
new VariableUiElement(
reviews.reviews.map(
(reviews) =>
new List(
reviews.map((r) => r.rating + "% " + r.opinion + " (" + r.metadata.nickname + ")")
)
async function main() {
const location: [number, number] = [3.21, 51.2]
const t = Tiles.embedded_tile(location[1], location[0], 6)
const url = `https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/community_index/tile_${t.z}_${t.x}_${t.y}.geojson`
const be = Stores.FromPromise(Utils.downloadJson(url)).mapD(
(data) => data.features.find((f) => GeoOperations.inside(location, f)).properties
)
).AttachTo("maindiv")
new SvelteUIElement(ContactLink, { country: be }).AttachTo("maindiv")
/*
const links = data.features
.filter((f) => GeoOperations.inside(location, f))
.map((f) => new SvelteUIElement(ContactLink, { country: f.properties }))
new List(links).AttachTo("maindiv")
//*/
}
main().then((_) => {})

View file

@ -1,17 +0,0 @@
import { describe } from "mocha"
import { expect } from "chai"
describe("TestSuite", () => {
describe("function under test", () => {
it("should work", () => {
expect("abc").eq("abc")
})
})
})
it("global test", async () => {
expect("abc").eq("abc")
expect(() => {
throw "hi"
}).throws(/hi/)
})

View file

@ -1,5 +1,5 @@
import { describe } from "mocha"
import { exec } from "child_process"
import { describe, it } from "vitest"
/**
*

View file

@ -1,4 +1,3 @@
import { expect } from "chai"
import { Utils } from "../../../Utils"
import UserRelatedState from "../../../Logic/State/UserRelatedState"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"
@ -11,6 +10,7 @@ import SelectedFeatureHandler from "../../../Logic/Actors/SelectedFeatureHandler
import { ElementStorage } from "../../../Logic/ElementStorage"
import { OsmTags } from "../../../Models/OsmFeature"
import { Feature, Geometry } from "geojson"
import { describe, expect, it } from "vitest"
const latestTags = {
amenity: "public_bookcase",
@ -83,9 +83,9 @@ it("should download the latest version", () => {
SelectedElementTagsUpdater.applyUpdate(state, latestTags, feature.properties.id)
// The name should be updated
expect(feature.properties.name).deep.equal("Stubbekwartier-buurtbibliotheek")
expect(feature.properties.name).toEqual("Stubbekwartier-buurtbibliotheek")
// The fixme should be removed
expect(feature.properties.fixme).deep.equal(undefined)
expect(feature.properties.fixme).toBeUndefined()
})
it("Hash without selected element should download geojson from OSM-API", async () => {
const hash = new UIEventSource("node/5568693115")
@ -97,9 +97,9 @@ it("Hash without selected element should download geojson from OSM-API", async (
})
loc.addCallback((_) => {
expect(selected.data.properties.id).deep.equal("node/5568693115")
expect(loc.data.zoom).deep.equal(14)
expect(loc.data.lat).deep.equal(51.2179199)
expect(selected.data.properties.id).toEqual("node/5568693115")
expect(loc.data.zoom).toEqual(14)
expect(loc.data.lat).toEqual(51.2179199)
})
new SelectedFeatureHandler(hash, {

View file

@ -1,8 +1,7 @@
import { describe } from "mocha"
import { expect } from "chai"
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"
import { Tag } from "../../../Logic/Tags/Tag"
import { Changes } from "../../../Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
describe("CreateMultiPolygonWithPointReuseAction", () => {
it("should produce a correct changeset", () => {
@ -106,21 +105,29 @@ describe("CreateMultiPolygonWithPointReuseAction", () => {
const descriptions = await action.Perform(new Changes())
const ways = descriptions.filter((d) => d.type === "way")
expect(ways[0].id == -18, "unexpected id").true
expect(ways[1].id == -27, "unexpected id").true
// "unexpected id"
expect(ways[0].id == -18).toBe(true)
// "unexpected id"
expect(ways[1].id == -27).toBe(true)
const outer = ways[0].changes["coordinates"]
expect(outer).deep.equal(feature.geometry.coordinates[0])
expect(outer).toEqual(feature.geometry.coordinates[0])
const inner = ways[1].changes["coordinates"]
expect(inner).deep.equal(feature.geometry.coordinates[1])
expect(inner).toEqual(feature.geometry.coordinates[1])
const members = <{ type: string; role: string; ref: number }[]>(
descriptions.find((d) => d.type === "relation").changes["members"]
)
expect(members[0].role, "incorrect role").eq("outer")
expect(members[1].role, "incorrect role").eq("inner")
expect(members[0].type, "incorrect type").eq("way")
expect(members[1].type, "incorrect type").eq("way")
expect(members[0].ref, "incorrect id").eq(-18)
expect(members[1].ref, "incorrect id").eq(-27)
// "incorrect role"
expect(members[0].role).toBe("outer")
// "incorrect role"
expect(members[1].role).toBe("inner")
// "incorrect type"
expect(members[0].type).toBe("way")
// "incorrect type"
expect(members[1].type).toBe("way")
// "incorrect id"
expect(members[0].ref).toBe(-18)
// "incorrect id"
expect(members[1].ref).toBe(-27)
}
})
})

View file

@ -1,7 +1,6 @@
import { describe } from "mocha"
import { expect } from "chai"
import { ExtraFuncParams, ExtraFunctions } from "../../Logic/ExtraFunctions"
import { OsmFeature } from "../../Models/OsmFeature"
import { describe, expect, it } from "vitest"
describe("OverlapFunc", () => {
it("should give doors on the edge", () => {
@ -122,6 +121,6 @@ describe("OverlapFunc", () => {
ExtraFunctions.FullPatchFeature(params, hermanTeirlinck)
const overlap = (<any>hermanTeirlinck).overlapWith("*")
console.log(JSON.stringify(overlap))
expect(overlap[0].feat == door).true
expect(overlap[0].feat == door).toBe(true)
})
})

View file

@ -1,4 +1,3 @@
import { describe } from "mocha"
import OsmFeatureSource from "../../../Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource"
import { UIEventSource } from "../../../Logic/UIEventSource"
import ScriptUtils from "../../../scripts/ScriptUtils"
@ -8,7 +7,7 @@ import { readFileSync } from "fs"
import { Utils } from "../../../Utils"
import { Tag } from "../../../Logic/Tags/Tag"
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
import { expect } from "chai"
import { describe, expect, it } from "vitest"
const expected = {
type: "Feature",
@ -92,7 +91,7 @@ function test(done: () => void) {
handleTile: (tile) => {
fetchedTile = tile
const data = tile.features.data[0].feature
expect(data.properties).deep.eq({
expect(data.properties).toEqual({
id: "relation/5759328",
timestamp: "2022-06-10T00:46:55Z",
version: 6,
@ -107,8 +106,8 @@ function test(done: () => void) {
website: "http://ktahalle.be/",
_backend: "https://osm.org",
})
expect(data.geometry.type).eq("MultiPolygon")
expect(data).deep.eq(expected)
expect(data.geometry.type).toBe("MultiPolygon")
expect(data).toEqual(expected)
done()
},
isActive: new UIEventSource<boolean>(true),

View file

@ -1,7 +1,6 @@
import { describe } from "mocha"
import TileFreshnessCalculator from "../../../Logic/FeatureSource/TileFreshnessCalculator"
import { Tiles } from "../../../Models/TileRange"
import { expect } from "chai"
import { describe, expect, it } from "vitest"
describe("TileFreshnessCalculator", () => {
it("should get the freshness for loaded tiles", () => {
@ -11,13 +10,13 @@ describe("TileFreshnessCalculator", () => {
date.setTime(42)
calc.addTileLoad(Tiles.tile_index(19, 266406, 175534), date)
expect(calc.freshnessFor(19, 266406, 175534).getTime()).eq(42)
expect(calc.freshnessFor(20, 266406 * 2, 175534 * 2 + 1).getTime()).eq(42)
expect(calc.freshnessFor(19, 266406, 175535)).undefined
expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2)).undefined
expect(calc.freshnessFor(19, 266406, 175534).getTime()).toBe(42)
expect(calc.freshnessFor(20, 266406 * 2, 175534 * 2 + 1).getTime()).toBe(42)
expect(calc.freshnessFor(19, 266406, 175535)).toBeUndefined()
expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2)).toBeUndefined()
calc.addTileLoad(Tiles.tile_index(19, 266406, 175534 + 1), date)
calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534), date)
calc.addTileLoad(Tiles.tile_index(19, 266406 + 1, 175534 + 1), date)
expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2).getTime()).eq(42)
expect(calc.freshnessFor(18, 266406 / 2, 175534 / 2).getTime()).toBe(42)
})
})

View file

@ -1,8 +1,7 @@
import { describe } from "mocha"
import { expect } from "chai"
import * as turf from "@turf/turf"
import { GeoOperations } from "../../Logic/GeoOperations"
import { Feature, LineString, Polygon } from "geojson"
import { describe, expect, it } from "vitest"
describe("GeoOperations", () => {
describe("calculateOverlap", () => {
@ -124,9 +123,9 @@ describe("GeoOperations", () => {
}
const p0 = turf.polygon(polyGrb.geometry.coordinates)
expect(p0).not.null
expect(p0).not.toBeNull()
const p1 = turf.polygon(polyHouse.geometry.coordinates)
expect(p1).not.null
expect(p1).not.toBeNull()
const overlaps = GeoOperations.calculateOverlap(polyGrb, [polyHouse])
expect(overlaps).empty

View file

@ -1,8 +1,7 @@
import { describe } from "mocha"
import { expect } from "chai"
import AllImageProviders from "../../../Logic/ImageProviders/AllImageProviders"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { Utils } from "../../../Utils"
import { describe, expect, it } from "vitest"
describe("ImageProviders", () => {
it("should work on a variaty of inputs", () => {
@ -16,9 +15,9 @@ describe("ImageProviders", () => {
if (img === undefined) {
throw "No image found"
}
expect(img.url).deep.equal(url)
expect(img.url).toEqual(url)
if (providerName) {
expect(providerName).deep.equal(img.provider.constructor.name)
expect(providerName).toEqual(img.provider.constructor.name)
}
console.log("OK")
})

View file

@ -1,5 +1,3 @@
import { describe } from "mocha"
import { expect } from "chai"
import { Utils } from "../../../../Utils"
import { OsmObject, OsmRelation } from "../../../../Logic/Osm/OsmObject"
import {
@ -7,6 +5,7 @@ import {
TurnRestrictionRSH,
} from "../../../../Logic/Osm/Actions/RelationSplitHandler"
import { Changes } from "../../../../Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
describe("RelationSplitHandler", () => {
Utils.injectJsonDownloadForTests("https://www.openstreetmap.org/api/0.6/node/1124134958/ways", {
@ -651,10 +650,8 @@ describe("RelationSplitHandler", () => {
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
const allIds = changeDescription[0].changes["members"].map((m) => m.ref).join(",")
const expected = "687866206,295132739,-1,690497698"
expect(
allIds.indexOf(expected) >= 0,
"didn't find the expected order of ids in the relation to test"
).true
// "didn't find the expected order of ids in the relation to test"
expect(allIds.indexOf(expected) >= 0).toBe(true)
})
it("should split turn restrictions (split of https://www.openstreetmap.org/way/143298912)", async () => {
@ -705,7 +702,7 @@ describe("RelationSplitHandler", () => {
.map((m) => m.type + "/" + m.ref + "-->" + m.role)
.join(",")
const expected = "way/318616190-->from,node/1407529979-->via,way/-1-->to"
expect(allIds).deep.equal(expected)
expect(allIds).toEqual(expected)
// Reversing the ids has no effect
const splitterReverse = new TurnRestrictionRSH(
@ -719,6 +716,6 @@ describe("RelationSplitHandler", () => {
"no-theme"
)
const changesReverse = await splitterReverse.CreateChangeDescriptions(new Changes())
expect(changesReverse.length).deep.equal(0)
expect(changesReverse.length).toEqual(0)
})
})

View file

@ -1,13 +1,11 @@
import { describe } from "mocha"
import { expect } from "chai"
import Minimap from "../../../../UI/Base/Minimap"
import { Utils } from "../../../../Utils"
import LayoutConfig from "../../../../Models/ThemeConfig/LayoutConfig"
import State from "../../../../State"
import { BBox } from "../../../../Logic/BBox"
import ReplaceGeometryAction from "../../../../Logic/Osm/Actions/ReplaceGeometryAction"
import ShowDataLayerImplementation from "../../../../UI/ShowDataLayer/ShowDataLayerImplementation"
import ShowDataLayer from "../../../../UI/ShowDataLayer/ShowDataLayer"
import { describe, expect, it } from "vitest"
describe("ReplaceGeometryAction", () => {
const grbStripped = {
@ -896,7 +894,7 @@ describe("ReplaceGeometryAction", () => {
})
const closestIds = await action.GetClosestIds()
expect(closestIds.closestIds).deep.equal([
expect(closestIds.closestIds).toEqual([
9219979643,
1728823481,
4978289383,
@ -910,14 +908,14 @@ describe("ReplaceGeometryAction", () => {
undefined,
])
expect(closestIds.reprojectedNodes.size).deep.equal(1)
expect(closestIds.reprojectedNodes.size).toEqual(1)
const reproj = closestIds.reprojectedNodes.get(1728823549)
expect(reproj.projectAfterIndex).deep.equal(1)
expect(reproj.newLon).deep.equal(3.2168880864669203)
expect(reproj.newLat).deep.equal(51.214739524104694)
expect(closestIds.detachedNodes.size).deep.equal(0)
expect(reproj.projectAfterIndex).toEqual(1)
expect(reproj.newLon).toEqual(3.2168880864669203)
expect(reproj.newLat).toEqual(51.214739524104694)
expect(closestIds.detachedNodes.size).toEqual(0)
const changes = await action.Perform(state.changes)
expect(changes[11].changes["coordinates"]).deep.equal([
expect(changes[11].changes["coordinates"]).toEqual([
[3.216690793633461, 51.21474084112525],
[3.2167256623506546, 51.214696737309964],
[3.2168880864669203, 51.214739524104694],

View file

@ -1,8 +1,7 @@
import { describe } from "mocha"
import { expect } from "chai"
import { Utils } from "../../../../Utils"
import SplitAction from "../../../../Logic/Osm/Actions/SplitAction"
import { Changes } from "../../../../Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
describe("SplitAction", () => {
{
@ -2690,20 +2689,20 @@ describe("SplitAction", () => {
})
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
expect(changeDescription[0].type).eq("node")
expect(changeDescription[0].id).eq(-1)
expect(changeDescription[0].changes["lat"]).eq(51.181710380278176)
expect(changeDescription[0].changes["lon"]).eq(3.246733546257019)
expect(changeDescription[1].type).eq("way")
expect(changeDescription[1].id).eq(-2)
expect(changeDescription[1].changes["coordinates"].length).eq(6)
expect(changeDescription[1].changes["coordinates"][5][0]).eq(splitPoint[0])
expect(changeDescription[1].changes["coordinates"][5][1]).eq(splitPoint[1])
expect(changeDescription[2].type).eq("way")
expect(changeDescription[2].id).eq(295132739)
expect(changeDescription[2].changes["coordinates"].length).eq(10)
expect(changeDescription[2].changes["coordinates"][0][0]).eq(splitPoint[0])
expect(changeDescription[2].changes["coordinates"][0][1]).eq(splitPoint[1])
expect(changeDescription[0].type).toBe("node")
expect(changeDescription[0].id).toBe(-1)
expect(changeDescription[0].changes["lat"]).toBe(51.181710380278176)
expect(changeDescription[0].changes["lon"]).toBe(3.246733546257019)
expect(changeDescription[1].type).toBe("way")
expect(changeDescription[1].id).toBe(-2)
expect(changeDescription[1].changes["coordinates"].length).toBe(6)
expect(changeDescription[1].changes["coordinates"][5][0]).toBe(splitPoint[0])
expect(changeDescription[1].changes["coordinates"][5][1]).toBe(splitPoint[1])
expect(changeDescription[2].type).toBe("way")
expect(changeDescription[2].id).toBe(295132739)
expect(changeDescription[2].changes["coordinates"].length).toBe(10)
expect(changeDescription[2].changes["coordinates"][0][0]).toBe(splitPoint[0])
expect(changeDescription[2].changes["coordinates"][0][1]).toBe(splitPoint[1])
})
it("split 295132739 on already existing node", async () => {
@ -2715,13 +2714,13 @@ describe("SplitAction", () => {
})
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
expect(changeDescription.length).eq(2)
expect(changeDescription[0].type).eq("way")
expect(changeDescription[1].type).eq("way")
expect(changeDescription.length).toBe(2)
expect(changeDescription[0].type).toBe("way")
expect(changeDescription[1].type).toBe("way")
expect(
changeDescription[0].changes["nodes"][changeDescription[0].changes["nodes"].length - 1]
).eq(changeDescription[1].changes["nodes"][0])
expect(changeDescription[1].changes["nodes"][0]).eq(1507524610)
).toBe(changeDescription[1].changes["nodes"][0])
expect(changeDescription[1].changes["nodes"][0]).toBe(1507524610)
})
it("split 61435323 on already existing node", async () => {
@ -2733,8 +2732,8 @@ describe("SplitAction", () => {
const changeDescription = await splitter.CreateChangeDescriptions(new Changes())
// Should be a new node
expect(changeDescription[0].type).eq("node")
expect(changeDescription[3].type).eq("relation")
expect(changeDescription[0].type).toBe("node")
expect(changeDescription[3].type).toBe("relation")
})
it("Split test line", async () => {
@ -2764,11 +2763,11 @@ describe("SplitAction", () => {
8715440363
*/
expect(changes[0].changes["nodes"]).deep.equal([
expect(changes[0].changes["nodes"]).toEqual([
6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, 8715440370,
8715440369, 8715440368,
])
expect(changes[1].changes["nodes"]).deep.equal([
expect(changes[1].changes["nodes"]).toEqual([
8715440368, 8715440367, 8715440366, 8715440365, 8715440364, 8715440363,
])
})
@ -2784,14 +2783,14 @@ describe("SplitAction", () => {
const changes = await splitAction.Perform(new Changes())
// THe first change is the creation of the new node
expect(changes[0].type).deep.equal("node")
expect(changes[0].id).deep.equal(-1)
expect(changes[0].type).toEqual("node")
expect(changes[0].id).toEqual(-1)
expect(changes[1].changes["nodes"]).deep.equal([
expect(changes[1].changes["nodes"]).toEqual([
6490126559, 8715440375, 8715440374, 8715440373, 8715440372, 8715440371, 8715440370,
8715440369, -1,
])
expect(changes[2].changes["nodes"]).deep.equal([
expect(changes[2].changes["nodes"]).toEqual([
-1, 8715440368, 8715440367, 8715440366, 8715440365, 8715440364, 8715440363,
])
})

View file

@ -1,6 +1,6 @@
import { expect } from "chai"
import { ChangeDescription } from "../../../Logic/Osm/Actions/ChangeDescription"
import { Changes } from "../../../Logic/Osm/Changes"
import { expect, it } from "vitest"
it("Generate preXML from changeDescriptions", () => {
const changeDescrs: ChangeDescription[] = [
@ -29,11 +29,11 @@ it("Generate preXML from changeDescriptions", () => {
]
const c = new Changes()
const descr = c.CreateChangesetObjects(changeDescrs, [])
expect(descr.modifiedObjects).length(0)
expect(descr.deletedObjects).length(0)
expect(descr.newObjects).length(1)
expect(descr.modifiedObjects).toHaveLength(0)
expect(descr.deletedObjects).toHaveLength(0)
expect(descr.newObjects).toHaveLength(1)
const ch = descr.newObjects[0]
expect(ch.tags["foo"]).eq("bar")
expect(ch.tags["someKey"]).eq("someValue")
expect(ch.tags["foo"]).toBe("bar")
expect(ch.tags["someKey"]).toBe("someValue")
})

View file

@ -1,11 +1,10 @@
import { describe } from "mocha"
import { expect } from "chai"
import { Utils } from "../../../Utils"
import { ChangesetHandler, ChangesetTag } from "../../../Logic/Osm/ChangesetHandler"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import { ElementStorage } from "../../../Logic/ElementStorage"
import { Changes } from "../../../Logic/Osm/Changes"
import { describe, expect, it } from "vitest"
describe("ChangesetHanlder", () => {
describe("RewriteTagsOf", () => {
@ -56,18 +55,16 @@ describe("ChangesetHanlder", () => {
oldChangesetMeta
)
const d = Utils.asDict(rewritten)
expect(d.size).deep.equal(10)
expect(d.get("answer")).deep.equal("5")
expect(d.get("comment")).deep.equal(
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
)
expect(d.get("created_by")).deep.equal("MapComplete 0.16.6")
expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).deep.equal("osm")
expect(d.get("source")).deep.equal("survey")
expect(d.get("source:node/-1")).deep.equal("note/1234")
expect(d.get("theme")).deep.equal("toerisme_vlaanderen")
expect(d.get("newTag")).deep.equal("newValue")
expect(d.size).toEqual(10)
expect(d.get("answer")).toEqual("5")
expect(d.get("comment")).toEqual("Adding data with #MapComplete for theme #toerisme_vlaanderen")
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
expect(d.get("host")).toEqual("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).toEqual("osm")
expect(d.get("source")).toEqual("survey")
expect(d.get("source:node/-1")).toEqual("note/1234")
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
expect(d.get("newTag")).toEqual("newValue")
})
it("should aggregate numeric tags", () => {
const changesetHandler = new ChangesetHandler(
@ -116,17 +113,15 @@ describe("ChangesetHanlder", () => {
)
const d = Utils.asDict(rewritten)
expect(d.size).deep.equal(9)
expect(d.get("answer")).deep.equal("42")
expect(d.get("comment")).deep.equal(
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
)
expect(d.get("created_by")).deep.equal("MapComplete 0.16.6")
expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).deep.equal("osm")
expect(d.get("source")).deep.equal("survey")
expect(d.get("source:node/-1")).deep.equal("note/1234")
expect(d.get("theme")).deep.equal("toerisme_vlaanderen")
expect(d.size).toEqual(9)
expect(d.get("answer")).toEqual("42")
expect(d.get("comment")).toEqual("Adding data with #MapComplete for theme #toerisme_vlaanderen")
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
expect(d.get("host")).toEqual("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).toEqual("osm")
expect(d.get("source")).toEqual("survey")
expect(d.get("source:node/-1")).toEqual("note/1234")
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
})
it("should rewrite special reasons with the correct ID", () => {
const changesetHandler = new ChangesetHandler(
@ -169,17 +164,15 @@ describe("ChangesetHanlder", () => {
)
const d = Utils.asDict(rewritten)
expect(d.size).deep.equal(9)
expect(d.get("answer")).deep.equal("5")
expect(d.get("comment")).deep.equal(
"Adding data with #MapComplete for theme #toerisme_vlaanderen"
)
expect(d.get("created_by")).deep.equal("MapComplete 0.16.6")
expect(d.get("host")).deep.equal("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).deep.equal("osm")
expect(d.get("source")).deep.equal("survey")
expect(d.get("source:node/42")).deep.equal("note/1234")
expect(d.get("theme")).deep.equal("toerisme_vlaanderen")
expect(d.size).toEqual(9)
expect(d.get("answer")).toEqual("5")
expect(d.get("comment")).toEqual("Adding data with #MapComplete for theme #toerisme_vlaanderen")
expect(d.get("created_by")).toEqual("MapComplete 0.16.6")
expect(d.get("host")).toEqual("https://mapcomplete.osm.be/toerisme_vlaanderen.html")
expect(d.get("imagery")).toEqual("osm")
expect(d.get("source")).toEqual("survey")
expect(d.get("source:node/42")).toEqual("note/1234")
expect(d.get("theme")).toEqual("toerisme_vlaanderen")
})
})
@ -200,12 +193,13 @@ describe("ChangesetHanlder", () => {
extraMetaTags,
changes
)
expect(hasSpecialMotivationChanges, "Special rewrite did not trigger").true
// "Special rewrite did not trigger"
expect(hasSpecialMotivationChanges).toBe(true)
// Rewritten inline by rewriteMetaTags
expect(extraMetaTags[1].key).deep.equal("source:node/42")
expect(extraMetaTags[1].value).deep.equal("note/1234")
expect(extraMetaTags[0].key).deep.equal("created_by")
expect(extraMetaTags[0].value).deep.equal("mapcomplete")
expect(extraMetaTags[1].key).toEqual("source:node/42")
expect(extraMetaTags[1].value).toEqual("note/1234")
expect(extraMetaTags[0].key).toEqual("created_by")
expect(extraMetaTags[0].value).toEqual("mapcomplete")
})
})
})

Some files were not shown because too many files have changed in this diff Show more