diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index 9a97ff39e..1a2d4da2b 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -5,6 +5,7 @@ import BaseUIElement from "../UI/BaseUIElement"; import List from "../UI/Base/List"; import Title from "../UI/Base/Title"; import {BBox} from "./BBox"; +import {Feature, Geometry, MultiPolygon, Polygon} from "@turf/turf"; export interface ExtraFuncParams { /** @@ -12,9 +13,9 @@ export interface ExtraFuncParams { * Note that more features then requested can be given back. * Format: [ [ geojson, geojson, geojson, ... ], [geojson, ...], ...] */ - getFeaturesWithin: (layerId: string, bbox: BBox) => any[][], + getFeaturesWithin: (layerId: string, bbox: BBox) => Feature[][], memberships: RelationsTracker - getFeatureById: (id: string) => any + getFeatureById: (id: string) => Feature } /** @@ -24,21 +25,65 @@ interface ExtraFunction { readonly _name: string; readonly _args: string[]; readonly _doc: string; - readonly _f: (params: ExtraFuncParams, feat: any) => any; + readonly _f: (params: ExtraFuncParams, feat: Feature) => any; } +class EnclosingFunc implements ExtraFunction { + _name = "enclosingFeatures" + _doc = ["Gives a list of all features in the specified layers which fully contain this object. Returned features will always be (multi)polygons. (LineStrings and Points from the other layers are ignored)","", + "The result is a list of features: `{feat: Polygon}[]`", + "This function will never return the feature itself."].join("\n") + _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] + _f(params: ExtraFuncParams, feat: Feature) { + return (...layerIds: string[]) => { + const result: { feat: any }[] = [] + const bbox = BBox.get(feat) + const seenIds = new Set() + seenIds.add(feat.properties.id) + for (const layerId of layerIds) { + const otherFeaturess = params.getFeaturesWithin(layerId, bbox) + if (otherFeaturess === undefined) { + continue; + } + if (otherFeaturess.length === 0) { + continue; + } + for (const otherFeatures of otherFeaturess) { + for (const otherFeature of otherFeatures) { + if(seenIds.has(otherFeature.properties.id)){ + continue + } + seenIds.add(otherFeature.properties.id) + if(otherFeature.geometry.type !== "Polygon" && otherFeature.geometry.type !== "MultiPolygon"){ + continue; + } + if(GeoOperations.completelyWithin(feat, > otherFeature)){ + result.push({feat: otherFeature}) + } + } + } + } + + return result; + } + } +} class OverlapFunc implements ExtraFunction { _name = "overlapWith"; - _doc = "Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well." + - "If the current feature is a point, all features that this point is embeded in are given.\n\n" + - "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" + - "The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list\n" + - "\n" + - "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`" + _doc = ["Gives a list of features from the specified layer which this feature (partly) overlaps with. A point which is embedded in the feature is detected as well.", + "If the current feature is a point, all features that this point is embeded in are given." , + "", + "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point." , + "The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list." , + "", + "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", + "", + "Also see [enclosingFeatures](#enclosingFeatures) which can be used to get all objects which fully contain this feature" + ].join("\n") _args = ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"] _f(params, feat) { @@ -46,15 +91,15 @@ class OverlapFunc implements ExtraFunction { const result: { feat: any, overlap: number }[] = [] const bbox = BBox.get(feat) for (const layerId of layerIds) { - const otherLayers = params.getFeaturesWithin(layerId, bbox) - if (otherLayers === undefined) { + const otherFeaturess = params.getFeaturesWithin(layerId, bbox) + if (otherFeaturess === undefined) { continue; } - if (otherLayers.length === 0) { + if (otherFeaturess.length === 0) { continue; } - for (const otherLayer of otherLayers) { - result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); + for (const otherFeatures of otherFeaturess) { + result.push(...GeoOperations.calculateOverlap(feat, otherFeatures)); } } @@ -392,6 +437,7 @@ export class ExtraFunctions { private static readonly allFuncs: ExtraFunction[] = [ new DistanceToFunc(), new OverlapFunc(), + new EnclosingFunc(), new IntersectionFunc(), new ClosestObjectFunc(), new ClosestNObjectFunc(), diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 87d4aedda..2a6015695 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -23,6 +23,7 @@ import TileFreshnessCalculator from "./TileFreshnessCalculator"; import FullNodeDatabaseSource from "./TiledFeatureSource/FullNodeDatabaseSource"; import MapState from "../State/MapState"; import {ElementStorage} from "../ElementStorage"; +import {Feature, Geometry} from "@turf/turf"; /** @@ -337,7 +338,7 @@ export default class FeaturePipeline { } - public GetAllFeaturesWithin(bbox: BBox): any[][] { + public GetAllFeaturesWithin(bbox: BBox): Feature[][] { const self = this const tiles = [] Array.from(this.perLayerHierarchy.keys()) diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index c5b5a5f40..240312a9b 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -3,7 +3,7 @@ import {BBox} from "./BBox"; import togpx from "togpx" import Constants from "../Models/Constants"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import {Coord} from "@turf/turf"; +import {booleanWithin, Coord, Feature, Geometry, MultiPolygon, Polygon, Properties} from "@turf/turf"; export class GeoOperations { @@ -142,7 +142,10 @@ export class GeoOperations { return result; } - public static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) { + /** + * Helper function which does the heavy lifting for 'inside' + */ + private static pointInPolygonCoordinates(x: number, y: number, coordinates: [number, number][][]) { const inside = GeoOperations.pointWithinRing(x, y, /*This is the outer ring of the polygon */coordinates[0]) if (!inside) { return false; @@ -737,6 +740,38 @@ export class GeoOperations { return turf.bearing(a, b) } + /** + * Returns 'true' if one feature contains the other feature + * + * const pond: Feature = { + * "type": "Feature", + * "properties": {"natural":"water","water":"pond"}, + * "geometry": { + * "type": "Polygon", + * "coordinates": [[ + * [4.362924098968506,50.8435422298544 ], + * [4.363272786140442,50.8435219059949 ], + * [4.363213777542114,50.8437420806679 ], + * [4.362924098968506,50.8435422298544 ] + * ]]}} + * const park: Feature = { + * "type": "Feature", + * "properties": {"leisure":"park"}, + * "geometry": { + * "type": "Polygon", + * "coordinates": [[ + * [ 4.36073541641235,50.84323737103244 ], + * [ 4.36469435691833, 50.8423905305197 ], + * [ 4.36659336090087, 50.8458997374786 ], + * [ 4.36254858970642, 50.8468007074916 ], + * [ 4.36073541641235, 50.8432373710324 ] + * ]]}} + * GeoOperations.completelyWithin(pond, park) // => true + * GeoOperations.completelyWithin(park, pond) // => false + */ + static completelyWithin(feature: Feature, possiblyEncloingFeature: Feature) : boolean { + return booleanWithin(feature, possiblyEncloingFeature); + } } diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index a73476d36..d01af1919 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -155,7 +155,6 @@ export default class MetaTagging { // Lazy function const f = (feature: any) => { - const oldValue = feature.properties[key] delete feature.properties[key] Object.defineProperty(feature.properties, key, { configurable: true, diff --git a/Logic/SimpleMetaTagger.ts b/Logic/SimpleMetaTagger.ts index 714da8e75..fa2501bd1 100644 --- a/Logic/SimpleMetaTagger.ts +++ b/Logic/SimpleMetaTagger.ts @@ -486,7 +486,7 @@ export default class SimpleMetaTaggers { const subElements: (string | BaseUIElement)[] = [ new Combine([ "Metatags are extra tags available, in order to display more data or to give better questions.", - "The are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", + "They are calculated automatically on every feature when the data arrives in the webbrowser. This document gives an overview of the available metatags.", "**Hint:** when using metatags, add the [query parameter](URL_Parameters.md) `debug=true` to the URL. This will include a box in the popup for features which shows all the properties of the object" ]).SetClass("flex-col") diff --git a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts index c9a4abc9d..65655ac32 100644 --- a/Models/ThemeConfig/Json/PointRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/PointRenderingConfigJson.ts @@ -60,7 +60,7 @@ export default interface PointRenderingConfigJson { rotation?: string | TagRenderingConfigJson; /** * A HTML-fragment that is shown below the icon, for example: - *
{name}
+ *
{name}
* * If the icon is undefined, then the label is shown in the center of the feature. * Note that, if the wayhandling hides the icon then no label is shown as well. diff --git a/assets/layers/kindergarten_childcare/childcare.svg b/assets/layers/kindergarten_childcare/childcare.svg new file mode 100644 index 000000000..1ab4c36fb --- /dev/null +++ b/assets/layers/kindergarten_childcare/childcare.svg @@ -0,0 +1,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/layers/kindergarten_childcare/kindergarten_childcare.json b/assets/layers/kindergarten_childcare/kindergarten_childcare.json new file mode 100644 index 000000000..a1a5d0455 --- /dev/null +++ b/assets/layers/kindergarten_childcare/kindergarten_childcare.json @@ -0,0 +1,153 @@ +{ + "id": "kindergarten_childcare", + "name": { + "en": "Kindergartens and childcare" + }, + "description": "Shows kindergartens and preschools. Both are grouped in one layer, as they are regularly confused with each other", + "minzoom": 12, + "source": { + "osmTags": { + "or": [ + "amenity=childcare", + "amenity=kindergarten" + ] + } + }, + "title": { + "mappings": [ + { + "if": "amenity=kindergarten", + "then": { + "en": "Kindergarten {name}" + } + }, + { + "if": "amenity=childcare", + "then": { + "en": "Childcare {name}" + } + } + ] + }, + "tagRenderings": [ + { + "id": "childcare-type", + "question": { + "en": "What type of facility is this?" + }, + "mappings": [ + { + "if": "amenity=kindergarten", + "then": { + "en": "This is a kindergarten (also known as preschool) where small kids receive early education." + }, + "addExtraTags": [ + "isced:level=0", + "isced:2011:level=early_childhood" + ] + }, + { + "if": "amenity=childcare", + "then": { + "en": "This is a childcare facility, such as a nursery or daycare where small kids are looked after. They do not offer an education and are ofter run as private businesses" + }, + "addExtraTags": [ + "isced:level=", + "isced:2011:level=" + ] + } + ] + }, + { + "id": "name", + "question": "What is the name of this facility?", + "render": "This facility is named {name}", + "freeform": { + "key": "name" + } + }, + "website", + "email", + "phone", + { + "builtin": "opening_hours", + "override": { + "question": { + "en": "When is this childcare opened?" + }, + "condition": "amenity=childcare" + } + }, + { + "id": "capacity", + "question": { + "en": "How much kids (at most) can be enrolled here?" + }, + "render": { + "en": "This facility has room for {capacity} kids" + }, + "freeform": { + "key": "capacity", + "type": "pnat" + } + } + ], + "presets": [ + { + "title": { + "en": "a kindergarten" + }, + "description": "A kindergarten (also known as preschool) is a school where small kids receive early education.", + "tags": [ + "amenity=kindergarten", + "isced:level=0", + "isced:2011:level=early_childhood" + ] + }, + { + "title": { + "en": "a childcare" + }, + "description": "A childcare (also known as a nursery or daycare) is a facility which looks after small kids, but does not offer them an education program.", + "tags": [ + "amenity=kindergarten" + ] + } + ], + "mapRendering": [ + { + "location": [ + "point", + "centroid" + ], + "label": { + "mappings": [ + { + "if": "name~*", + "then": "
{name}
" + } + ] + }, + "icon": { + "mappings": [ + { + "if": "amenity=kindergarten", + "then": "circle:white;./assets/layers/kindergarten_childcare/childcare.svg" + }, + { + "if": "amenity=childcare", + "then": "circle:white;./assets/layers/kindergarten_childcare/childcare.svg" + } + ] + } + }, + { + "color": "#62fc6c", + "width": 1 + } + ], + "allowMove": { + "enableRelocation": true, + "enableImproveAccuracy": true + } +} \ No newline at end of file diff --git a/assets/layers/kindergarten_childcare/license_info.json b/assets/layers/kindergarten_childcare/license_info.json new file mode 100644 index 000000000..bfeac3b2a --- /dev/null +++ b/assets/layers/kindergarten_childcare/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "childcare.svg", + "license": "CC-BY", + "authors": [ + "Diego Naive" + ], + "sources": [ + "https://thenounproject.com/icon/child-care-332981/" + ] + } +] \ No newline at end of file diff --git a/assets/layers/school/college.svg b/assets/layers/school/college.svg new file mode 100644 index 000000000..a741c7f5a --- /dev/null +++ b/assets/layers/school/college.svg @@ -0,0 +1,33 @@ + + + + + + diff --git a/assets/layers/school/license_info.json b/assets/layers/school/license_info.json new file mode 100644 index 000000000..f8c34dde0 --- /dev/null +++ b/assets/layers/school/license_info.json @@ -0,0 +1,23 @@ +[ + { + "path": "college.svg", + "license": "CC0", + "authors": [ + "Maki" + ], + "sources": [ + "https://labs.mapbox.com/maki-icons/" + ] + }, + { + "path": "school.svg", + "license": "CC0", + "authors": [ + "Temaki" + ], + "sources": [ + "https://github.com/ideditor/temaki", + "https://ideditor.github.io/temaki/docs/" + ] + } +] \ No newline at end of file diff --git a/assets/layers/school/school.json b/assets/layers/school/school.json new file mode 100644 index 000000000..27d0d2e3d --- /dev/null +++ b/assets/layers/school/school.json @@ -0,0 +1,183 @@ +{ + "id": "school", + "name": { + "en": "Primary and secondary schools" + }, + "minzoom": 12, + "title": { + "render": { + "en": "School {name}" + } + }, + "calculatedTags": [ + "_enclosing=feat.enclosingFeatures('school').map(f => f.feat.properties.id)", + "_is_enclosed=feat.properties._enclosing != '[]'" + ], + "isShown": { + "render": "yes", + "mappings": [ + { + "if": { + "and": [ + "building~*", + "_is_enclosed=true" + ] + }, + "then": "no" + } + ] + }, + "tagRenderings": [ + { + "render": { + "en": "This school is named {name}" + }, + "question": { + "en": "What is the name of this school?" + }, + "freeform": { + "key": "name" + }, + "id": "school-name" + }, + { + "id": "capacity", + "question": "How much students can at most enroll in this school?", + "render": { + "en": "This school can enroll at most {capacity} students" + }, + "freeform": { + "key": "capacity", + "type": "pnat" + } + }, + { + "id": "education-level", + "question": "What level of education is given on this school?", + "mappings": [ + { + "if": "isced:2011:level=primary", + "then": { + "en": "This is a school where one learns primary skills such as basic literacy and numerical skills.
Pupils typically enroll from 6 years old till 12 years old
" + } + }, + { + "if": "isced:2011:level=vocational_lower_secondary", + "then": { + "en": "This is a school where one learns vocational lower secondary skills with a focus to acquire the necessary knowledge and skill for a particular occupation or trade.
This includes programs with a work-based component such as apprenticeships or dual-system education. This is commonly called middle education. Pupils typically enroll from 12 years old till 14 or 15 years old
" + } + }, + { + "if": "isced:2011:level=general_lower_secondary", + "then": { + "en": "This is a school where one learns general lower secondary skills with a focus on general skills in order to prepare student for further studies.
This is commonly called middle education. Pupils typically enroll from 12 years old till 14 or 15 years old
" + } + }, + + { + "if": "isced:2011:level=vocational_upper_secondary", + "then": { + "en": "This is a school where one learns vocational upper secondary skills with a focus to acquire the necessary knowledge and skill for a particular occupation or trade.
This includes programs with a work-based component such as apprenticeships or dual-system education. Pupils typically enroll from 14 or 15 years old till 18 years old
" + } + }, + { + "if": "isced:2011:level=general_upper_secondary", + "then": { + "en": "This is a school where one learns general upper secondary skills with a focus on general skills in order to prepare student for further studies.
Pupils typically enroll from 14 or 15 years old till 18 years old
" + } + } + ], + "multiAnswer": true + }, + { + "id": "target-audience", + "question": "What is the target audience for this school?", + "multiAnswer": true, + "mappings": [ + { + "if": "school:for=normal_pupils", + "then": { + "en": "This is a school where students study skills at their age-adequate level" + } + }, + { + "if": "school:for=adults", + "then": { + "en": "This is a school where adults are taught skills on the level as specified." + } + }, + { + "if": "school:for=autism", + "then": { + "en": "This is a school with facilities for students on the autism specturm" + } + }, + { + "if": "school:for=learning_disabilities", + "then": { + "en": "This is a school with facilities for students with learning disabilities" + } + }, + { + "if": "school:for=blind", + "then": { + "en": "This is a school with facilities for blind students or students with sight impairments" + } + }, + { + "if": "school:for=deaf", + "then": { + "en": "This is a school with facilities for deaf students or students with hearing impairments" + } + }, + { + "if": "school:for=disabilities", + "then": { + "en": "This is a school with facilities for students with disabilities" + } + } + ] + }, + "website", + "phone", + "email" + ], + "presets": [ + { + "tags": [ + "amenity=school","fixme=Added with MapComplete, the precise geometry should still be drawn" + ], + + "title": { + "en": "a primary or secondary school" + } + } + ], + "source": { + "osmTags": "amenity=school" + }, + "mapRendering": [ + { + "icon": "circle:white;./assets/layers/school/school.svg", + "label": { + "mappings": [ + { + "if": "name~*", + "then": "
{name}
" + } + ] + }, + "iconSize": { + "render": "40,40,center" + }, + "location": [ + "point", + "centroid" + ] + }, + { + "color": "#fcd862", + "width": 1 + } + ] +} \ No newline at end of file diff --git a/assets/layers/school/school.svg b/assets/layers/school/school.svg new file mode 100644 index 000000000..d7d142d1c --- /dev/null +++ b/assets/layers/school/school.svg @@ -0,0 +1,37 @@ + + + + + + + diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 245e1964a..d23389910 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -293,6 +293,10 @@ "if": "theme=postboxes", "then": "./assets/themes/postboxes/postbox.svg" }, + { + "if": "theme=schools", + "then": "./assets/layers/school/college.svg" + }, { "if": "theme=shops", "then": "./assets/themes/shops/shop.svg" diff --git a/assets/themes/schools/International Standard Classification of Education (ISCED) 2011.pdf b/assets/themes/schools/International Standard Classification of Education (ISCED) 2011.pdf new file mode 100644 index 000000000..cd2dea91f Binary files /dev/null and b/assets/themes/schools/International Standard Classification of Education (ISCED) 2011.pdf differ diff --git a/assets/themes/schools/schools.json b/assets/themes/schools/schools.json new file mode 100644 index 000000000..d0765bcff --- /dev/null +++ b/assets/themes/schools/schools.json @@ -0,0 +1,20 @@ +{ + "id": "schools", + "description": { + "en": "On this map, you'll find information about all types of schools and eduction and can easily add more information" + }, + "title": { + "en": "Education" + }, + "defaultBackgroundId": "CartoDB.Voyager", + "maintainer": "MapComplete", + "version": "0.0.1", + "startLat": 0, + "startLon": 0, + "startZoom": 0, + "icon": "./assets/layers/school/college.svg", + "layers": [ + "kindergarten_childcare", + "school" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 827c62ae0..7a28bd23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@babel/preset-env": "7.13.8", "@parcel/service-worker": "^2.6.0", + "@turf/boolean-intersects": "^6.5.0", "@turf/buffer": "^6.5.0", "@turf/collect": "^6.5.0", "@turf/distance": "^6.5.0", diff --git a/package.json b/package.json index dfe653f92..43940fd12 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "dependencies": { "@babel/preset-env": "7.13.8", "@parcel/service-worker": "^2.6.0", + "@turf/boolean-intersects": "^6.5.0", "@turf/buffer": "^6.5.0", "@turf/collect": "^6.5.0", "@turf/distance": "^6.5.0", diff --git a/scripts/generateLicenseInfo.ts b/scripts/generateLicenseInfo.ts index ea129440b..fbfaecaee 100644 --- a/scripts/generateLicenseInfo.ts +++ b/scripts/generateLicenseInfo.ts @@ -70,6 +70,21 @@ knownLicenses.set("streetcomplete", { license: "CC0", sources: ["https://github.com/streetcomplete/StreetComplete/tree/master/res/graphics", "https://f-droid.org/packages/de.westnordost.streetcomplete/"] }) + +knownLicenses.set("temaki", { + authors: ["Temaki"], + path: undefined, + license: "CC0", + sources: ["https://github.com/ideditor/temaki","https://ideditor.github.io/temaki/docs/"] +}) + +knownLicenses.set("maki", { + authors: ["Maki"], + path: undefined, + license: "CC0", + sources: ["https://labs.mapbox.com/maki-icons/"] +}) + knownLicenses.set("t", { authors: [], path: undefined,