diff --git a/Docs/Integrating_Maproulette.md b/Docs/Integrating_Maproulette.md index 701b44e3d1..3cc527fc61 100644 --- a/Docs/Integrating_Maproulette.md +++ b/Docs/Integrating_Maproulette.md @@ -1,9 +1,11 @@ # Integrating MapRoulette -[MapRoulette](https://www.maproulette.org/) is a website which has challenges. A challenge is a collection of _microtasks_, i.e. mapping -tasks which can be solved in a few minutes. +[MapRoulette](https://www.maproulette.org/) is a website which has challenges. A challenge is a collection of _microtasks_, i.e. mapping tasks which can be solved in a few minutes. + +A perfect example of this is to setup such a challenge to e.g. import new points. + +> [Important: always follow the import guidelines if you want to import data.](https://wiki.openstreetmap.org/wiki/Import/Guidelines) -A perfect example of this is to setup such a challenge to e.g. import new points. [Important: always follow the import guidelines if you want to import data.](https://wiki.openstreetmap.org/wiki/Import/Guidelines) (Another approach to set up a guided import is to create a map note for every point with the [import helper](https://mapcomplete.osm.be/import_helper). This however litters the map notes and will upset mappers if used with to much points. However, this flow is easier to setup as no changes to theme files are needed, nor is a maproulette-account needed) ## The API diff --git a/Models/Constants.ts b/Models/Constants.ts index f42f2cad85..1c744046bb 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -1,7 +1,7 @@ import { Utils } from "../Utils" export default class Constants { - public static vNumber = "0.27.1" + public static vNumber = "0.27.2" public static ImgurApiKey = "7070e7167f0a25a" public static readonly mapillary_client_token_v4 = diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 63887079d4..7ae449437e 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -1,17 +1,17 @@ import Svg from "../../Svg" import Combine from "../Base/Combine" -import { SubtleButton } from "../Base/SubtleButton" +import {SubtleButton} from "../Base/SubtleButton" import Translations from "../i18n/Translations" import BaseUIElement from "../BaseUIElement" -import LayoutConfig, { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" -import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" +import LayoutConfig, {LayoutInformation} from "../../Models/ThemeConfig/LayoutConfig" +import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource" import Loc from "../../Models/Loc" import UserRelatedState from "../../Logic/State/UserRelatedState" -import { Utils } from "../../Utils" +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 {Translation} from "../i18n/Translation" +import {TextField} from "../Input/TextField" import Locale from "../i18n/Locale" import SvelteUIElement from "../Base/SvelteUIElement" import ThemesList from "./ThemesList.svelte" @@ -37,7 +37,7 @@ export default class MoreScreen extends Combine { searchTerm = searchTerm.toLowerCase() if (searchTerm === "personal") { window.location.href = MoreScreen.createUrlFor( - { id: "personal" }, + {id: "personal"}, false, state ).data @@ -59,13 +59,13 @@ export default class MoreScreen extends Combine { (th) => th.hideFromOverview == false && th.id !== "personal" && - MoreScreen.MatchesLayoutFunc(th)(searchTerm) + MoreScreen.MatchesLayout(th, searchTerm) ) if (publicTheme !== undefined) { window.location.href = MoreScreen.createUrlFor(publicTheme, false, state).data } const hiddenTheme = MoreScreen.officialThemes.find( - (th) => th.id !== "personal" && MoreScreen.MatchesLayoutFunc(th)(searchTerm) + (th) => th.id !== "personal" && MoreScreen.MatchesLayout(th, searchTerm) ) if (hiddenTheme !== undefined) { window.location.href = MoreScreen.createUrlFor(hiddenTheme, false, state).data @@ -109,6 +109,85 @@ export default class MoreScreen extends Combine { ]) } + /** + * Creates a button linking to the given theme + * @private + */ + public static createLinkButton( + state: { + locationControl?: UIEventSource + layoutToUse?: LayoutConfig + }, + layout: { + id: string + icon: string + title: any + shortDescription: any + definition?: any + mustHaveLanguage?: boolean + }, + isCustom: boolean = false + ): BaseUIElement { + const url = MoreScreen.createUrlFor(layout, isCustom, state) + let content = new Combine([ + new Translation( + layout.title, + !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined + ), + new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", + ]).SetClass("overflow-hidden flex flex-col") + + if (state.layoutToUse === undefined) { + // Currently on the index screen: we style the buttons equally large + content = new Combine([content]).SetClass("flex flex-col justify-center h-24") + } + + return new SubtleButton(layout.icon, content, {url, newTab: false}) + } + + public static CreateProffessionalSerivesButton() { + const t = Translations.t.professional.indexPage + return new Combine([ + new Title(t.hook, 4), + t.hookMore, + new SubtleButton(undefined, t.button, {url: "./professional.html"}), + ]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg") + } + + public static MatchesLayout(layout: { + id: string + title: any + shortDescription: any + keywords?: any[] + }, search: string): boolean { + if(search === undefined){ + return true + } + search = search.toLocaleLowerCase() + if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { + return true + } + if(layout.id === "personal"){ + return false + } + const entitiesToSearch = [ + layout.shortDescription, + layout.title, + ...(layout.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 + } + private static createUrlFor( layout: { id: string; definition?: string }, isCustom: boolean, @@ -164,79 +243,4 @@ export default class MoreScreen extends Combine { }) ?? new ImmutableStore(`${linkPrefix}`) ) } - - /** - * Creates a button linking to the given theme - * @private - */ - public static createLinkButton( - state: { - locationControl?: UIEventSource - layoutToUse?: LayoutConfig - }, - layout: { - id: string - icon: string - title: any - shortDescription: any - definition?: any - mustHaveLanguage?: boolean - }, - isCustom: boolean = false - ): BaseUIElement { - const url = MoreScreen.createUrlFor(layout, isCustom, state) - let content = new Combine([ - new Translation( - layout.title, - !isCustom && !layout.mustHaveLanguage ? "themes:" + layout.id + ".title" : undefined - ), - new Translation(layout.shortDescription)?.SetClass("subtle") ?? "", - ]).SetClass("overflow-hidden flex flex-col") - - if (state.layoutToUse === undefined) { - // Currently on the index screen: we style the buttons equally large - content = new Combine([content]).SetClass("flex flex-col justify-center h-24") - } - - return new SubtleButton(layout.icon, content, { url, newTab: false }) - } - - public static CreateProffessionalSerivesButton() { - const t = Translations.t.professional.indexPage - return new Combine([ - new Title(t.hook, 4), - t.hookMore, - new SubtleButton(undefined, t.button, { url: "./professional.html" }), - ]).SetClass("flex flex-col border border-gray-300 p-2 rounded-lg") - } - - private static MatchesLayoutFunc(layout: { - id: string - title: any - shortDescription: any - keywords?: any[] - }): (search: string) => boolean { - return (search: string) => { - search = search.toLocaleLowerCase() - if (layout.id.toLowerCase().indexOf(search) >= 0) { - return true - } - const entitiesToSearch = [ - layout.shortDescription, - layout.title, - ...(layout.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 - } - } } diff --git a/UI/BigComponents/ThemesList.svelte b/UI/BigComponents/ThemesList.svelte index 3f4bc51a58..f4e3abb7c5 100644 --- a/UI/BigComponents/ThemesList.svelte +++ b/UI/BigComponents/ThemesList.svelte @@ -9,6 +9,7 @@ import ProfessionalServicesButton from "./ProfessionalServicesButton.svelte" import ThemeButton from "./ThemeButton.svelte" import { LayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" + import MoreScreen from "./MoreScreen"; export let search: UIEventSource export let themes: LayoutInformation[] @@ -18,26 +19,7 @@ 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 - }) + $: filteredThemes = themes.filter((theme) => MoreScreen.MatchesLayout(theme, $search))
diff --git a/assets/layers/bike_shop/bike_shop.json b/assets/layers/bike_shop/bike_shop.json index c9eae8908a..8352eba45a 100644 --- a/assets/layers/bike_shop/bike_shop.json +++ b/assets/layers/bike_shop/bike_shop.json @@ -821,5 +821,46 @@ "render": "1" } } + ], + "filter": [ + "open_now", + { + "id": "sells_second-hand", + "options": [ + { + "question": { + "en": "Sells second hand bicycles", + "de": "Verkauft gebrauchte Fahrräder", + "nl": "Verkoopt twedehands fietsen", + "it": "Vende biciclette usate" + }, + "osmTags": { + "or": [ + "service:bicycle:second_hand=yes", + "service:bicycle:second_hand=only" + ] + } + } + ] + }, + { + "id": "offers_diy_repair", + "options": [ + { + "question": { + "en": "Offers DIY bike repair", + "de": "Bietet Selbstreparatur an", + "nl": "Biedt doe-het-zelfreparaties aan", + "it": "Offre riparazioni fai da te" + }, + "osmTags": { + "or": [ + "service:bicycle:diy=yes", + "service:bicycle:diy=only" + ] + } + } + ] + } ] -} \ No newline at end of file +} diff --git a/assets/layers/maproulette_challenge/maproulette_challenge.json b/assets/layers/maproulette_challenge/maproulette_challenge.json index 327086c632..3ab70fdd0b 100644 --- a/assets/layers/maproulette_challenge/maproulette_challenge.json +++ b/assets/layers/maproulette_challenge/maproulette_challenge.json @@ -2,7 +2,7 @@ "id": "maproulette_challenge", "name": null, "description": { - "en": "Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to the documentation on how to do this.", + "en": "Layer showing tasks of a single MapRoulette challenge. This layer is intended to be reused and extended in themes; refer to [the documentation](https://github.com/pietervdvn/MapComplete/blob/develop/Docs/Integrating_Maproulette.md) on how to do this.", "de": "Ebene mit Aufgaben einer einzelnen MapRoulette-Herausforderung. Diese Ebene soll in Themen wiederverwendet und erweitert werden; Informationen dazu finden Sie in der Dokumentation.", "nl": "Laag met taken van een MapRoulette uitdaging" }, @@ -224,4 +224,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/assets/layers/recycling/bicycles.svg b/assets/layers/recycling/bicycles.svg new file mode 100644 index 0000000000..989d558de6 --- /dev/null +++ b/assets/layers/recycling/bicycles.svg @@ -0,0 +1,45 @@ + + + + + + + diff --git a/assets/layers/recycling/license_info.json b/assets/layers/recycling/license_info.json index 6da2ad2584..8635871204 100644 --- a/assets/layers/recycling/license_info.json +++ b/assets/layers/recycling/license_info.json @@ -19,6 +19,14 @@ "https://github.com/streetcomplete/StreetComplete/blob/master/res/graphics/recycling%20icons/beverage_cartons.svg" ] }, + { + "path": "bicycles.svg", + "license": "CC0", + "authors": [ + "Antwerpenize Bike Font" + ], + "sources": [] + }, { "path": "cans.svg", "license": "CC-BY-SA", @@ -113,6 +121,14 @@ "https://openclipart.org/detail/175842/basic-light-bulb" ] }, + { + "path": "needles.svg", + "license": "CC0", + "authors": [], + "sources": [ + "https://svgsilh.com/image/1294131.html" + ] + }, { "path": "newspaper.svg", "license": "CC-BY-SA", diff --git a/assets/layers/recycling/needles.svg b/assets/layers/recycling/needles.svg new file mode 100644 index 0000000000..7146cc54b2 --- /dev/null +++ b/assets/layers/recycling/needles.svg @@ -0,0 +1,41 @@ + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + + + diff --git a/assets/layers/recycling/recycling.json b/assets/layers/recycling/recycling.json index b1abe26630..1bf6cf054e 100644 --- a/assets/layers/recycling/recycling.json +++ b/assets/layers/recycling/recycling.json @@ -273,6 +273,15 @@ ] }, "then": "circle:white;./assets/layers/waste_disposal/waste_disposal.svg" + }, + { + "if": { + "and": [ + "_waste_amount=1", + "recycling:needles=yes" + ] + }, + "then": "circle:white;./assets/layers/recycling/needles.svg" } ] }, @@ -475,6 +484,24 @@ ] }, "then": "circle:white;./assets/layers/waste_disposal/waste_disposal.svg" + }, + { + "if": { + "and": [ + "_waste_amount>1", + "recycling:needles=yes" + ] + }, + "then": "circle:white;./assets/layers/recycling/needles.svg" + }, + { + "if": { + "and": [ + "_waste_amount>1", + "recycling:bicycles=yes" + ] + }, + "then": "circle:white;./assets/layers/recycling/bicycles.svg" } ] } @@ -576,6 +603,20 @@ "recycling:small_electrical_appliances=", "recycling:waste=" ] + }, + { + "if": "recycling_type=pickup_point", + "then": { + "en": "This is a pickup point. The waste material is placed here without placing it in a dedicated container.", + "nl": "Dit is een verzamelplaats zonder container waar het afval later opgepikt wordt." + } + }, + { + "if": "recycling_type=dump", + "then": { + "en": "This is a dump where the waste material is stacked.", + "nl": "Dit is een afvalhoop waar het afvalmateriaal bovenop elkaar gestapeld wordt." + } } ] }, @@ -1024,6 +1065,10 @@ "de": "Nadeln können hier recycelt werden", "it": "Aghi e oggetti appuntiti", "ca": "Aquí es poden reciclar agulles" + }, + "icon": { + "path": "./assets/layers/recycling/needles.svg", + "class": "medium" } }, { @@ -1041,6 +1086,18 @@ "class": "medium" }, "hideInAnswer": "recycling_type=container" + }, + { + "if": "recycling:bicycles=yes", + "ifnot": "recycling:bicycles=", + "then": { + "en": "Bicycles can be recycled here", + "nl": "Fietsen (en fietswrakken) kunnen hier gerecycled worden" + }, + "icon": { + "path": "./assets/layers/recycling/bicycles.svg", + "class": "medium" + } } ] }, @@ -1338,7 +1395,7 @@ "it": "Riciclo di rottami metallici", "ca": "Reciclatge de ferralla" }, - "osmTags": "recycling:printer_cartridges=yes" + "osmTags": "recycling:scrap_metal=yes" }, { "question": { @@ -1349,16 +1406,6 @@ "it": "Riciclo di piccoli elettrodomestici", "ca": "Reciclatge de petits electrodomèstics" }, - "osmTags": "recycling:scrap_metal=yes" - }, - { - "question": { - "en": "Recycling of residual waste", - "nl": "Recycling van restafval", - "de": "Recycling von Restabfällen", - "it": "Riciclo di secco", - "ca": "Reciclatge del rebuig" - }, "osmTags": { "or": [ "recycling:small_appliances=yes", @@ -1375,6 +1422,20 @@ "ca": "Reciclatge del rebuig" }, "osmTags": "recycling:waste=yes" + }, + { + "question": { + "en": "Recycling of printer cartridges", + "nl": "Recycling van inktpatronen" + }, + "osmTags": "recycling:printer_cartridges=yes" + }, + { + "question": { + "en": "Recycling of bicycles", + "nl": "Recycling van fietsen" + }, + "osmTags": "recycling:bicycles=yes" } ] }, @@ -1409,4 +1470,4 @@ "enableRelocation": true, "enableImproveAccuracy": true } -} \ No newline at end of file +} diff --git a/scripts/downloadFromOverpass.ts b/scripts/downloadFromOverpass.ts new file mode 100644 index 0000000000..acaedab579 --- /dev/null +++ b/scripts/downloadFromOverpass.ts @@ -0,0 +1,24 @@ +import Script from "./Script"; +import {TagUtils} from "../Logic/Tags/TagUtils"; +import {And} from "../Logic/Tags/And"; +import Constants from "../Models/Constants"; +import {ImmutableStore} from "../Logic/UIEventSource"; +import {BBox} from "../Logic/BBox"; +import {Overpass} from "../Logic/Osm/Overpass"; +const fs = require("fs") +class DownloadFromOverpass extends Script { + + constructor() { + super("Downloads data from openstreetmap, will save this as 'export.geojson'. All arguments will be interpreted as key=value pairs"); + } + async main(args: string[]): Promise { + const tags = new And(args.map(k => TagUtils.Tag(k))) + const overpass = new Overpass(tags,[], Constants.defaultOverpassUrls[0], new ImmutableStore(500)) + const [data, _] = await overpass.queryGeoJson(BBox.global) + fs.writeFileSync("export.geojson", JSON.stringify(data), "utf8") + console.log("Written", data.features.length,"entries") + } + +} + +new DownloadFromOverpass().run() diff --git a/scripts/generateImageAnalysis.ts b/scripts/generateImageAnalysis.ts index 7aba9aafb0..4133380a1d 100644 --- a/scripts/generateImageAnalysis.ts +++ b/scripts/generateImageAnalysis.ts @@ -28,7 +28,7 @@ export default class GenerateImageAnalysis extends Script { tag, [], "https://overpass.kumi.systems/api/interpreter", - new ImmutableStore(180), + new ImmutableStore(500), undefined, false ) @@ -110,10 +110,10 @@ export default class GenerateImageAnalysis extends Script { const msg = `${i}/${ allImages.size - } downloaded: ${d},skipped: ${s}, failed: ${f}, running: ${runningSecs}sec, ETA: ${estimatedActualMinutes}:${ + } downloaded: ${d},skipped: ${s}, failed: ${f}, running: ${Math.floor(runningSecs)}sec, ETA: ${estimatedActualMinutes}:${ estimatedActualSeconds % 60 }` - ScriptUtils.erasableLog(msg) + ScriptUtils.erasableLog( " ", msg) if (downloaded) { d++ } else { @@ -227,10 +227,10 @@ export default class GenerateImageAnalysis extends Script { } async main(args: string[]): Promise { - const datapath = args[0] ?? "../MapComplete-data/ImageLicenseInfo" + const datapath = args[0] ?? "../../git/MapComplete-data/ImageLicenseInfo" await this.downloadData(datapath) - //await this.downloadMetadata(datapath) + await this.downloadMetadata(datapath) this.analyze(datapath) } }