diff --git a/Customizations/JSON/TagRenderingConfig.ts b/Customizations/JSON/TagRenderingConfig.ts index d3d4404939..7b36dae44b 100644 --- a/Customizations/JSON/TagRenderingConfig.ts +++ b/Customizations/JSON/TagRenderingConfig.ts @@ -27,7 +27,8 @@ export default class TagRenderingConfig { readonly type: string, readonly addExtraTags: TagsFilter[]; readonly inline: boolean, - readonly default?: string + readonly default?: string, + readonly helperArgs?: (string | number | boolean)[] }; readonly multiAnswer: boolean; @@ -76,8 +77,8 @@ export default class TagRenderingConfig { addExtraTags: json.freeform.addExtraTags?.map((tg, i) => FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [], inline: json.freeform.inline ?? false, - default: json.freeform.default - + default: json.freeform.default, + helperArgs: json.freeform.helperArgs } if (json.freeform["extraTags"] !== undefined) { @@ -336,20 +337,20 @@ export default class TagRenderingConfig { * Note: this might be hidden by conditions */ public hasMinimap(): boolean { - const translations : Translation[]= Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); + const translations: Translation[] = Utils.NoNull([this.render, ...(this.mappings ?? []).map(m => m.then)]); for (const translation of translations) { for (const key in translation.translations) { - if(!translation.translations.hasOwnProperty(key)){ + if (!translation.translations.hasOwnProperty(key)) { continue } const template = translation.translations[key] const parts = SubstitutedTranslation.ExtractSpecialComponents(template) - const hasMiniMap = parts.filter(part =>part.special !== undefined ).some(special => special.special.func.funcName === "minimap") - if(hasMiniMap){ + const hasMiniMap = parts.filter(part => part.special !== undefined).some(special => special.special.func.funcName === "minimap") + if (hasMiniMap) { return true; } } } return false; - } + } } \ No newline at end of file diff --git a/Customizations/JSON/TagRenderingConfigJson.ts b/Customizations/JSON/TagRenderingConfigJson.ts index 89871ec748..8438895255 100644 --- a/Customizations/JSON/TagRenderingConfigJson.ts +++ b/Customizations/JSON/TagRenderingConfigJson.ts @@ -30,6 +30,7 @@ export interface TagRenderingConfigJson { * Allow freeform text input from the user */ freeform?: { + /** * If this key is present, then 'render' is used to display the value. * If this is undefined, the rendering is _always_ shown @@ -40,6 +41,11 @@ export interface TagRenderingConfigJson { * See Docs/SpecialInputElements.md and UI/Input/ValidatedTextField.ts for supported values */ type?: string, + /** + * Extra parameters to initialize the input helper arguments. + * For semantics, see the 'SpecialInputElements.md' + */ + helperArgs?: (string | number | boolean)[]; /** * If a value is added with the textfield, these extra tag is addded. * Useful to add a 'fixme=freeform textfield used - to be checked' diff --git a/Docs/URL_Parameters.md b/Docs/URL_Parameters.md index 6f299adcf9..5c3158fd7e 100644 --- a/Docs/URL_Parameters.md +++ b/Docs/URL_Parameters.md @@ -20,126 +20,158 @@ the URL-parameters are stated in the part between the `?` and the `#`. There are Finally, the URL-hash is the part after the `#`. It is `node/1234` in this case. - layer-control-toggle ----------------------- - - Whether or not the layer control is shown The default value is _false_ - - - tab ------ - - The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_ - - - z ---- - - The initial/current zoom level The default value is _0_ - - - lat ------ - - The initial/current latitude The default value is _0_ - - - lon ------ - - The initial/current longitude of the app The default value is _0_ - - - fs-userbadge --------------- - - Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_ - - - fs-search ------------ - - Disables/Enables the search bar The default value is _true_ - - - fs-layers ------------ - - Disables/Enables the layer control The default value is _true_ - - - fs-add-new ------------- - - Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_ - - - fs-welcome-message --------------------- - - Disables/enables the help menu or welcome message The default value is _true_ - - - fs-iframe ------------ - - Disables/Enables the iframe-popup The default value is _false_ - - - fs-more-quests ----------------- - - Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_ - - - fs-share-screen ------------------ - - Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_ - - - fs-geolocation ----------------- - - Disables/Enables the geolocation button The default value is _true_ - - - fs-all-questions ------------------- - - Always show all questions The default value is _false_ - - - test ------- - - If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ - - - debug -------- - - If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ - - - backend +backend --------- - The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ +The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test' The default value is _osm_ - custom-css +test +------ + +If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org The default value is _false_ + + +layout +-------- + +The layout to load into MapComplete The default value is __ + + +userlayout ------------ - If specified, the custom css from the given link will be loaded additionaly The default value is __ +If not 'false', a custom (non-official) theme is loaded. This custom layout can be done in multiple ways: + +- The hash of the URL contains a base64-encoded .json-file containing the theme definition +- The hash of the URL contains a lz-compressed .json-file, as generated by the custom theme generator +- The parameter itself is an URL, in which case that URL will be downloaded. It should point to a .json of a theme The default value is _false_ - background +layer-control-toggle +---------------------- + +Whether or not the layer control is shown The default value is _false_ + + +tab +----- + +The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >50 changesets) The default value is _0_ + + +z +--- + +The initial/current zoom level The default value is _14_ + + +lat +----- + +The initial/current latitude The default value is _51.2095_ + + +lon +----- + +The initial/current longitude of the app The default value is _3.2228_ + + +fs-userbadge +-------------- + +Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode. The default value is _true_ + + +fs-search +----------- + +Disables/Enables the search bar The default value is _true_ + + +fs-layers +----------- + +Disables/Enables the layer control The default value is _true_ + + +fs-add-new ------------ - The id of the background layer to start with The default value is _osm_ +Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) The default value is _true_ +fs-welcome-message +-------------------- + +Disables/enables the help menu or welcome message The default value is _true_ + + +fs-iframe +----------- + +Disables/Enables the iframe-popup The default value is _false_ + + +fs-more-quests +---------------- + +Disables/Enables the 'More Quests'-tab in the welcome message The default value is _true_ + + +fs-share-screen +----------------- + +Disables/Enables the 'Share-screen'-tab in the welcome message The default value is _true_ + + +fs-geolocation +---------------- + +Disables/Enables the geolocation button The default value is _true_ + + +fs-all-questions +------------------ + +Always show all questions The default value is _false_ + + +fs-export +----------- + +If set, enables the 'download'-button to download everything as geojson The default value is _false_ + + +fake-user +----------- + +If true, 'dryrun' mode is activated and a fake user account is loaded The default value is _false_ + + +debug +------- + +If true, shows some extra debugging help such as all the available tags on every object The default value is _false_ + + +custom-css +------------ + +If specified, the custom css from the given link will be loaded additionaly The default value is __ + + +background +------------ + +The id of the background layer to start with The default value is _osm_ + + +oauth_token +------------- + +Used to complete the login No default value set layer- ------------------ diff --git a/Logic/Actors/StrayClickHandler.ts b/Logic/Actors/StrayClickHandler.ts index b4d6300700..3e7609fd41 100644 --- a/Logic/Actors/StrayClickHandler.ts +++ b/Logic/Actors/StrayClickHandler.ts @@ -47,7 +47,12 @@ export default class StrayClickHandler { popupAnchor: [0, -45] }) }); - const popup = L.popup().setContent("
"); + const popup = L.popup({ + autoPan: true, + autoPanPaddingTopLeft: [15,15], + closeOnEscapeKey: true, + autoClose: true + }).setContent("
"); self._lastMarker.addTo(leafletMap.data); self._lastMarker.bindPopup(popup); diff --git a/Logic/GeoOperations.ts b/Logic/GeoOperations.ts index 31cb88ad2d..768a5fe24b 100644 --- a/Logic/GeoOperations.ts +++ b/Logic/GeoOperations.ts @@ -273,6 +273,14 @@ export class GeoOperations { } return undefined; } + /** + * Generates the closest point on a way from a given point + * @param way The road on which you want to find a point + * @param point Point defined as [lon, lat] + */ + public static nearestPoint(way, point: [number, number]){ + return turf.nearestPointOnLine(way, point, {units: "kilometers"}); + } } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 37c8fa1d2e..92a0823f65 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -47,6 +47,7 @@ export class OsmConnection { public auth; public userDetails: UIEventSource; public isLoggedIn: UIEventSource + private fakeUser: boolean; _dryRun: boolean; public preferencesHandler: OsmPreferences; public changesetHandler: ChangesetHandler; @@ -59,12 +60,15 @@ export class OsmConnection { url: string }; - constructor(dryRun: boolean, oauth_token: UIEventSource, + constructor(dryRun: boolean, + fakeUser: boolean, + oauth_token: UIEventSource, // Used to keep multiple changesets open and to write to the correct changeset layoutName: string, singlePage: boolean = true, osmConfiguration: "osm" | "osm-test" = 'osm' ) { + this.fakeUser = fakeUser; this._singlePage = singlePage; this._oauth_config = OsmConnection.oauth_configs[osmConfiguration] ?? OsmConnection.oauth_configs.osm; console.debug("Using backend", this._oauth_config.url) @@ -72,7 +76,15 @@ export class OsmConnection { this._iframeMode = Utils.runningFromConsole ? false : window !== window.top; this.userDetails = new UIEventSource(new UserDetails(this._oauth_config.url), "userDetails"); - this.userDetails.data.dryRun = dryRun; + this.userDetails.data.dryRun = dryRun || fakeUser; + if(fakeUser){ + const ud = this.userDetails.data; + ud.csCount = 5678 + ud.loggedIn= true; + ud.unreadMessages = 0 + ud.name = "Fake user" + ud.totalMessages = 42; + } const self =this; this.isLoggedIn = this.userDetails.map(user => user.loggedIn).addCallback(isLoggedIn => { if(self.userDetails.data.loggedIn == false && isLoggedIn == true){ @@ -138,6 +150,10 @@ export class OsmConnection { } public AttemptLogin() { + if(this.fakeUser){ + console.log("AttemptLogin called, but ignored as fakeUser is set") + return; + } const self = this; console.log("Trying to log in..."); this.updateAuthObject(); diff --git a/State.ts b/State.ts index 90289bcab4..4186572668 100644 --- a/State.ts +++ b/State.ts @@ -59,8 +59,8 @@ export default class State { public favouriteLayers: UIEventSource; public layerUpdater: OverpassFeatureSource; - - public osmApiFeatureSource : OsmApiFeatureSource ; + + public osmApiFeatureSource: OsmApiFeatureSource; public filteredLayers: UIEventSource<{ @@ -81,7 +81,7 @@ export default class State { * Keeps track of relations: which way is part of which other way? * Set by the overpass-updater; used in the metatagging */ - public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships") + public readonly knownRelations = new UIEventSource>(undefined, "Relation memberships") public readonly featureSwitchUserbadge: UIEventSource; public readonly featureSwitchSearch: UIEventSource; @@ -96,8 +96,8 @@ export default class State { public readonly featureSwitchIsDebugging: UIEventSource; public readonly featureSwitchShowAllQuestions: UIEventSource; public readonly featureSwitchApiURL: UIEventSource; - public readonly featureSwitchEnableExport: UIEventSource; - + public readonly featureSwitchEnableExport: UIEventSource; + public readonly featureSwitchFakeUser: UIEventSource; public readonly featurePipeline: FeaturePipeline; @@ -131,7 +131,7 @@ export default class State { public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); - + constructor(layoutToUse: LayoutConfig) { const self = this; @@ -193,6 +193,12 @@ export default class State { "Disables/Enables the layer control"); this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"); + this.featureSwitchUserbadge.addCallbackAndRun(userbadge => { + if (!userbadge) { + this.featureSwitchAddNew.setData(false) + } + }) + this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, "Disables/enables the help menu or welcome message"); this.featureSwitchIframe = featSw("fs-iframe", () => false, @@ -205,20 +211,24 @@ export default class State { "Disables/Enables the geolocation button"); this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false, "Always show all questions"); - this.featureSwitchEnableExport = featSw("fs-export",(layoutToUse) => layoutToUse?.enableExportButton ?? false, + this.featureSwitchEnableExport = featSw("fs-export", (layoutToUse) => layoutToUse?.enableExportButton ?? false, "If set, enables the 'download'-button to download everything as geojson") this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false", "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org") .map(str => str === "true", [], b => "" + b); - - this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug","false", + + this.featureSwitchFakeUser = QueryParameters.GetQueryParameter("fake-user", "false", + "If true, 'dryrun' mode is activated and a fake user account is loaded") + .map(str => str === "true", [], b => "" + b); + + this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug", "false", "If true, shows some extra debugging help such as all the available tags on every object") .map(str => str === "true", [], b => "" + b) - this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","osm", + this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend", "osm", "The OSM backend to use - can be used to redirect mapcomplete to the testing backend when using 'osm-test'") - + } { // Some other feature switches @@ -229,18 +239,19 @@ export default class State { this.backgroundLayerId = QueryParameters.GetQueryParameter("background", - layoutToUse?.defaultBackgroundId ?? "osm", - "The id of the background layer to start with") + layoutToUse?.defaultBackgroundId ?? "osm", + "The id of the background layer to start with") } - - - if(Utils.runningFromConsole){ + + + if (Utils.runningFromConsole) { return; } this.osmConnection = new OsmConnection( this.featureSwitchIsTesting.data, + this.featureSwitchFakeUser.data, QueryParameters.GetQueryParameter("oauth_token", undefined, "Used to complete the login"), layoutToUse?.id, @@ -253,7 +264,7 @@ export default class State { this.allElements = new ElementStorage(); this.changes = new Changes(); this.osmApiFeatureSource = new OsmApiFeatureSource() - + new PendingChangesUploader(this.changes, this.selectedElement); this.mangroveIdentity = new MangroveIdentity( diff --git a/Svg.ts b/Svg.ts index cc028a5d5c..acff7d662a 100644 --- a/Svg.ts +++ b/Svg.ts @@ -194,6 +194,11 @@ export default class Svg { public static layersAdd_svg() { return new Img(Svg.layersAdd, true);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} + public static length_crosshair = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static length_crosshair_img = Img.AsImageElement(Svg.length_crosshair) + public static length_crosshair_svg() { return new Img(Svg.length_crosshair, true);} + public static length_crosshair_ui() { return new FixedUiElement(Svg.length_crosshair_img);} + public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) public static logo_svg() { return new Img(Svg.logo, true);} @@ -354,4 +359,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"barrier.svg": Svg.barrier,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"cycle-infra.svg": Svg.cycle_infra,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"barrier.svg": Svg.barrier,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair-empty.svg": Svg.crosshair_empty,"crosshair-locked.svg": Svg.crosshair_locked,"crosshair.svg": Svg.crosshair,"cycle-infra.svg": Svg.cycle_infra,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"length-crosshair.svg": Svg.length_crosshair,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 2c38e8b74b..6ebf37a756 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -5,6 +5,7 @@ import Loc from "../../Models/Loc"; import BaseLayer from "../../Models/BaseLayer"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import {Map} from "leaflet"; +import {Utils} from "../../Utils"; export default class Minimap extends BaseUIElement { @@ -15,11 +16,13 @@ export default class Minimap extends BaseUIElement { private readonly _location: UIEventSource; private _isInited = false; private _allowMoving: boolean; + private readonly _leafletoptions: any; constructor(options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any } ) { super() @@ -28,10 +31,11 @@ export default class Minimap extends BaseUIElement { this._location = options?.location ?? new UIEventSource(undefined) this._id = "minimap" + Minimap._nextId; this._allowMoving = options.allowMoving ?? true; + this._leafletoptions = options.leafletOptions ?? {} Minimap._nextId++ } - + protected InnerConstructElement(): HTMLElement { const div = document.createElement("div") div.id = this._id; @@ -52,7 +56,7 @@ export default class Minimap extends BaseUIElement { return wrapper; } - + private InitMap() { if (this._constructedHtmlElement === undefined) { // This element isn't initialized yet @@ -71,8 +75,8 @@ export default class Minimap extends BaseUIElement { const location = this._location; let currentLayer = this._background.data.layer() - const map = L.map(this._id, { - center: [location.data?.lat ?? 0, location.data?.lon ?? 0], + const options = { + center: <[number, number]> [location.data?.lat ?? 0, location.data?.lon ?? 0], zoom: location.data?.zoom ?? 2, layers: [currentLayer], zoomControl: false, @@ -82,9 +86,13 @@ export default class Minimap extends BaseUIElement { doubleClickZoom: this._allowMoving, keyboard: this._allowMoving, touchZoom: this._allowMoving, - zoomAnimation: this._allowMoving, + // Disabling this breaks the geojson layer - don't ask me why! zoomAnimation: this._allowMoving, fadeAnimation: this._allowMoving - }); + } + + Utils.Merge(this._leafletoptions, options) + + const map = L.map(this._id, options); map.setMaxBounds( [[-100, -200], [100, 200]] diff --git a/UI/Input/LengthInput.ts b/UI/Input/LengthInput.ts new file mode 100644 index 0000000000..0558069b29 --- /dev/null +++ b/UI/Input/LengthInput.ts @@ -0,0 +1,185 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import Svg from "../../Svg"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; +import {GeoOperations} from "../../Logic/GeoOperations"; +import DirectionInput from "./DirectionInput"; +import {RadioButton} from "./RadioButton"; +import {FixedInputElement} from "./FixedInputElement"; + + +/** + * Selects a length after clicking on the minimap, in meters + */ +export default class LengthInput extends InputElement { + private readonly _location: UIEventSource; + + public readonly IsSelected: UIEventSource = new UIEventSource(false); + private readonly value: UIEventSource; + private background; + + constructor(mapBackground: UIEventSource, + location: UIEventSource, + value?: UIEventSource) { + super(); + this._location = location; + this.value = value ?? new UIEventSource(undefined); + this.background = mapBackground; + this.SetClass("block") + + } + + GetValue(): UIEventSource { + return this.value; + } + + IsValid(str: string): boolean { + const t = Number(str) + return !isNaN(t) && t >= 0 && t <= 360; + } + + protected InnerConstructElement(): HTMLElement { + const modeElement = new RadioButton([ + new FixedInputElement("Measure", "measure"), + new FixedInputElement("Move", "move") + ]) + // @ts-ignore + let map = undefined + if (!Utils.runningFromConsole) { + map = DirectionInput.constructMinimap({ + background: this.background, + allowMoving: false, + location: this._location, + leafletOptions: { + tap: true + } + }) + } + const element = new Combine([ + new Combine([Svg.length_crosshair_svg().SetStyle( + `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`) + ]) + .SetClass("block length-crosshair-svg relative") + .SetStyle("z-index: 1000; visibility: hidden"), + map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"), + ]) + .SetClass("relative block bg-white border border-black rounded-3xl overflow-hidden") + .ConstructElement() + + + this.RegisterTriggers(element, map?.leafletMap) + element.style.overflow = "hidden" + element.style.display = "block" + + return element + } + + private RegisterTriggers(htmlElement: HTMLElement, leafletMap: UIEventSource) { + + let firstClickXY: [number, number] = undefined + let lastClickXY: [number, number] = undefined + const self = this; + + + function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) { + if (x === undefined || y === undefined) { + // Touch end + firstClickXY = undefined; + lastClickXY = undefined; + return; + } + + const rect = htmlElement.getBoundingClientRect(); + // From the central part of location + const dx = x - rect.left; + const dy = y - rect.top; + if (isDown) { + if (lastClickXY === undefined && firstClickXY === undefined) { + firstClickXY = [dx, dy]; + } else if (firstClickXY !== undefined && lastClickXY === undefined) { + lastClickXY = [dx, dy] + } else if (firstClickXY !== undefined && lastClickXY !== undefined) { + // we measure again + firstClickXY = [dx, dy] + lastClickXY = undefined; + } + } + if (isUp) { + const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) + if (distance > 15) { + lastClickXY = [dx, dy] + } + + + } else if (lastClickXY !== undefined) { + return; + } + + + const measurementCrosshair = htmlElement.getElementsByClassName("length-crosshair-svg")[0] as HTMLElement + + const measurementCrosshairInner: HTMLElement = measurementCrosshair.firstChild + if (firstClickXY === undefined) { + measurementCrosshair.style.visibility = "hidden" + } else { + measurementCrosshair.style.visibility = "unset" + measurementCrosshair.style.left = firstClickXY[0] + "px"; + measurementCrosshair.style.top = firstClickXY[1] + "px" + + const angle = 180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx) / Math.PI; + const angleGeo = (angle + 270) % 360 + measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`; + + const distance = Math.sqrt((dy - firstClickXY[1]) * (dy - firstClickXY[1]) + (dx - firstClickXY[0]) * (dx - firstClickXY[0])) + measurementCrosshairInner.style.width = (distance * 2) + "px" + measurementCrosshairInner.style.marginLeft = -distance + "px" + measurementCrosshairInner.style.marginTop = -distance + "px" + + + const leaflet = leafletMap?.data + if (leaflet) { + const first = leaflet.layerPointToLatLng(firstClickXY) + const last = leaflet.layerPointToLatLng([dx, dy]) + const geoDist = Math.floor(GeoOperations.distanceBetween([first.lng, first.lat], [last.lng, last.lat]) * 100000) / 100 + self.value.setData("" + geoDist) + } + + } + + } + + + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true); + ev.preventDefault(); + } + + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false); + ev.preventDefault(); + } + + htmlElement.ontouchend = (ev: TouchEvent) => { + onPosChange(undefined, undefined, false, true); + ev.preventDefault(); + } + + htmlElement.onmousedown = (ev: MouseEvent) => { + onPosChange(ev.clientX, ev.clientY, true); + ev.preventDefault(); + } + + htmlElement.onmouseup = (ev) => { + onPosChange(ev.clientX, ev.clientY, false, true); + ev.preventDefault(); + } + + htmlElement.onmousemove = (ev: MouseEvent) => { + onPosChange(ev.clientX, ev.clientY, false); + ev.preventDefault(); + } + } + +} \ No newline at end of file diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 2eeff8a543..ec3aa62ce4 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -13,6 +13,8 @@ import {Utils} from "../../Utils"; import Loc from "../../Models/Loc"; import {Unit} from "../../Customizations/JSON/Denomination"; import BaseUIElement from "../BaseUIElement"; +import LengthInput from "./LengthInput"; +import {GeoOperations} from "../../Logic/GeoOperations"; interface TextFieldDef { name: string, @@ -21,14 +23,16 @@ interface TextFieldDef { reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { location: [number, number], - mapBackgroundLayer?: UIEventSource + mapBackgroundLayer?: UIEventSource, + args: (string | number | boolean)[] + feature?: any }) => InputElement, - inputmode?: string } export default class ValidatedTextField { + public static bestLayerAt: (location: UIEventSource, preferences: UIEventSource) => any public static tpList: TextFieldDef[] = [ ValidatedTextField.tp( @@ -63,6 +67,83 @@ export default class ValidatedTextField { return [year, month, day].join('-'); }, (value) => new SimpleDatePicker(value)), + ValidatedTextField.tp( + "direction", + "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", + (str) => { + str = "" + str; + return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 + }, str => str, + (value, options) => { + const args = options.args ?? [] + let zoom = 19 + if (args[0]) { + zoom = Number(args[0]) + if (isNaN(zoom)) { + throw "Invalid zoom level for argument at 'length'-input" + } + } + const location = new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: zoom + }) + if (args[1]) { + // We have a prefered map! + options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + location, new UIEventSource(args[1].split(",")) + ) + } + const di = new DirectionInput(options.mapBackgroundLayer, location, value) + di.SetStyle("height: 20rem;"); + + return di; + }, + "numeric" + ), + ValidatedTextField.tp( + "length", + "A geographical length in meters (rounded at two points). Will give an extra minimap with a measurement tool. Arguments: [ zoomlevel, preferredBackgroundMapType (comma seperated) ], e.g. `[\"21\", \"map,photo\"]", + (str) => { + const t = Number(str) + return !isNaN(t) + }, + str => str, + (value, options) => { + const args = options.args ?? [] + let zoom = 19 + if (args[0]) { + zoom = Number(args[0]) + if (isNaN(zoom)) { + throw "Invalid zoom level for argument at 'length'-input" + } + } + + // Bit of a hack: we project the centerpoint to the closes point on the road - if available + if(options.feature){ + const lonlat: [number, number] = [...options.location] + lonlat.reverse() + options.location = <[number,number]> GeoOperations.nearestPoint(options.feature, lonlat).geometry.coordinates + options.location.reverse() + } + options.feature + + const location = new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: zoom + }) + if (args[1]) { + // We have a prefered map! + options.mapBackgroundLayer = ValidatedTextField.bestLayerAt( + location, new UIEventSource(args[1].split(",")) + ) + } + const li = new LengthInput(options.mapBackgroundLayer, location, value) + li.SetStyle("height: 20rem;") + return li; + } + ), ValidatedTextField.tp( "wikidata", "A wikidata identifier, e.g. Q42", @@ -113,22 +194,6 @@ export default class ValidatedTextField { undefined, undefined, "numeric"), - ValidatedTextField.tp( - "direction", - "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", - (str) => { - str = "" + str; - return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 - }, str => str, - (value, options) => { - return new DirectionInput(options.mapBackgroundLayer , new UIEventSource({ - lat: options.location[0], - lon: options.location[1], - zoom: 19 - }),value); - }, - "numeric" - ), ValidatedTextField.tp( "float", "A decimal", @@ -222,6 +287,7 @@ export default class ValidatedTextField { * {string (typename) --> TextFieldDef} */ public static AllTypes = ValidatedTextField.allTypesDict(); + public static InputForType(type: string, options?: { placeholder?: string | BaseUIElement, value?: UIEventSource, @@ -233,7 +299,9 @@ export default class ValidatedTextField { country?: () => string, location?: [number /*lat*/, number /*lon*/], mapBackgroundLayer?: UIEventSource, - unit?: Unit + unit?: Unit, + args?: (string | number | boolean)[] // Extra arguments for the inputHelper, + feature?: any }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -247,7 +315,7 @@ export default class ValidatedTextField { if (str === undefined) { return false; } - if(options.unit) { + if (options.unit) { str = options.unit.stripUnitParts(str) } return isValidTp(str, country ?? options.country) && optValid(str, country ?? options.country); @@ -268,7 +336,7 @@ export default class ValidatedTextField { }) } - if(options.unit) { + if (options.unit) { // We need to apply a unit. // This implies: // We have to create a dropdown with applicable denominations, and fuse those values @@ -288,17 +356,16 @@ export default class ValidatedTextField { input, unitDropDown, // combine the value from the textfield and the dropdown into the resulting value that should go into OSM - (text, denom) => denom?.canonicalValue(text, true) ?? undefined, + (text, denom) => denom?.canonicalValue(text, true) ?? undefined, (valueWithDenom: string) => { // Take the value from OSM and feed it into the textfield and the dropdown const withDenom = unit.findDenomination(valueWithDenom); - if(withDenom === undefined) - { + if (withDenom === undefined) { // Not a valid value at all - we give it undefined and leave the details up to the other elements return [undefined, undefined] } const [strippedText, denom] = withDenom - if(strippedText === undefined){ + if (strippedText === undefined) { return [undefined, undefined] } return [strippedText, denom] @@ -306,18 +373,20 @@ export default class ValidatedTextField { ).SetClass("flex") } if (tp.inputHelper) { - const helper = tp.inputHelper(input.GetValue(), { + const helper = tp.inputHelper(input.GetValue(), { location: options.location, - mapBackgroundLayer: options.mapBackgroundLayer - + mapBackgroundLayer: options.mapBackgroundLayer, + args: options.args, + feature: options.feature }) input = new CombinedInputElement(input, helper, (a, _) => a, // We can ignore b, as they are linked earlier a => [a, a] - ); + ); } return input; } + public static HelpText(): string { const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations @@ -329,7 +398,9 @@ export default class ValidatedTextField { reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { location: [number, number], - mapBackgroundLayer: UIEventSource + mapBackgroundLayer: UIEventSource, + args: string[], + feature: any }) => InputElement, inputmode?: string): TextFieldDef { diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index f35f73ceb8..b456c0ab96 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -36,7 +36,7 @@ export default class FeatureInfoBox extends ScrollableFullScreen { .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); const titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, - "block w-8 h-8 align-baseline box-content sm:p-0.5") + "block w-8 h-8 align-baseline box-content sm:p-0.5", "width: 2rem;") )) .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 6c8fd257ec..c8953dd011 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -16,31 +16,31 @@ export default class TagRenderingAnswer extends VariableUiElement { throw "Trying to generate a tagRenderingAnswer without configuration..." } super(tagsSource.map(tags => { - if(tags === undefined){ + if (tags === undefined) { return undefined; } - - if(configuration.condition){ - if(!configuration.condition.matchesProperties(tags)){ + + if (configuration.condition) { + if (!configuration.condition.matchesProperties(tags)) { return undefined; } } - - const trs = Utils.NoNull(configuration.GetRenderValues(tags)); - if(trs.length === 0){ - return undefined; - } - - const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) - if(valuesToRender.length === 1){ - return valuesToRender[0]; - }else if(valuesToRender.length > 1){ - return new List(valuesToRender) - } - return undefined; - }).map((element : BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) - this.SetClass("flex items-center flex-row text-lg link-underline tag-renering-answer") + const trs = Utils.NoNull(configuration.GetRenderValues(tags)); + if (trs.length === 0) { + return undefined; + } + + const valuesToRender: BaseUIElement[] = trs.map(tr => new SubstitutedTranslation(tr, tagsSource)) + if (valuesToRender.length === 1) { + return valuesToRender[0]; + } else if (valuesToRender.length > 1) { + return new List(valuesToRender) + } + return undefined; + }).map((element: BaseUIElement) => element?.SetClass(contentClasses)?.SetStyle(contentStyle))) + + this.SetClass("flex items-center flex-row text-lg link-underline") this.SetStyle("word-wrap: anywhere;"); } diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 20c0b00d21..c723759590 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -330,12 +330,15 @@ export default class TagRenderingQuestion extends Combine { } const tagsData = tags.data; + const feature = State.state.allElements.ContainingFeatures.get(tagsData.id) const input: InputElement = ValidatedTextField.InputForType(configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => tagsData._country, location: [tagsData._lat, tagsData._lon], mapBackgroundLayer: State.state.backgroundLayer, - unit: applicableUnit + unit: applicableUnit, + args: configuration.freeform.helperArgs, + feature: feature }); input.GetValue().setData(tagsData[freeform.key] ?? freeform.default); diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index df45af45e2..59225640f2 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -146,7 +146,9 @@ export default class ShowDataLayer { const popup = L.popup({ autoPan: true, closeOnEscapeKey: true, - closeButton: false + closeButton: false, + autoPanPaddingTopLeft: [15,15], + }, leafletLayer); leafletLayer.bindPopup(popup); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 309060b364..5a38e8184a 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -39,7 +39,8 @@ export default class SpecialVisualizations { static constructMiniMap: (options?: { background?: UIEventSource, location?: UIEventSource, - allowMoving?: boolean + allowMoving?: boolean, + leafletOptions?: any }) => BaseUIElement; static constructShowDataLayer: (features: UIEventSource<{ feature: any; freshness: Date }[]>, leafletMap: UIEventSource, layoutToUse: UIEventSource, enablePopups?: boolean, zoomToFeatures?: boolean) => any; public static specialVisualizations: SpecialVisualization[] = diff --git a/assets/layers/bike_repair_station/bike_repair_station.json b/assets/layers/bike_repair_station/bike_repair_station.json index ef3adb308a..6f59d8a5cf 100644 --- a/assets/layers/bike_repair_station/bike_repair_station.json +++ b/assets/layers/bike_repair_station/bike_repair_station.json @@ -508,7 +508,8 @@ } } ] - } + }, + "level" ], "icon": { "render": { diff --git a/assets/svg/length-crosshair.svg b/assets/svg/length-crosshair.svg new file mode 100644 index 0000000000..0446f22c4a --- /dev/null +++ b/assets/svg/length-crosshair.svg @@ -0,0 +1,115 @@ + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 7bff3270a3..66d034cdc3 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -123,5 +123,43 @@ "all_tags": { "#": "Prints all the tags", "render": "{all_tags()}" + }, + "level": { + "question": { + "nl": "Op welke verdieping bevindt dit punt zich?", + "en": "On what level is this feature located?" + }, + "render": { + "en": "Located on the {level}th floor", + "nl": "Bevindt zich op de {level}de verdieping" + }, + "freeform": { + "key": "level", + "type": "float" + }, + "mappings": [ + { + "if": "location=underground", + "then": { + "en": "Located underground", + "nl": "Bevindt zich ondergronds" + }, + "hideInAnswer": true + }, + { + "if": "level=0", + "then": { + "en": "Located on the ground floor", + "nl": "Bevindt zich gelijkvloers" + } + }, + { + "if": "level=1", + "then": { + "en": "Located on the first floor", + "nl": "Bevindt zich op de eerste verdieping" + } + } + ] } } \ No newline at end of file diff --git a/assets/themes/campersites/campersites.json b/assets/themes/campersites/campersites.json index 3c580c5fd5..cc248f2780 100644 --- a/assets/themes/campersites/campersites.json +++ b/assets/themes/campersites/campersites.json @@ -427,7 +427,8 @@ "it": "Sito web ufficiale: {website}", "ja": "公式Webサイト: {website}", "nb_NO": "Offisiell nettside: {website}", - "zh_Hant": "官方網站:{website}" + "zh_Hant": "官方網站:{website}", + "nl": "Officiële website: : {website}" }, "freeform": { "type": "url", @@ -823,7 +824,8 @@ "then": { "en": "Anyone can use this dump station", "ja": "誰でもこのゴミ捨て場を使用できます", - "it": "Chiunque può farne uso" + "it": "Chiunque può farne uso", + "ru": "Любой может воспользоваться этой станцией утилизации" }, "hideInAnswer": true }, @@ -836,7 +838,8 @@ "then": { "en": "Anyone can use this dump station", "ja": "誰でもこのゴミ捨て場を使用できます", - "it": "Chiunque può farne uso" + "it": "Chiunque può farne uso", + "ru": "Любой может воспользоваться этой станцией утилизации" } } ] @@ -845,12 +848,14 @@ "render": { "en": "This station is part of network {network}", "ja": "このステーションはネットワーク{network}の一部です", - "it": "Questo luogo è parte della rete {network}" + "it": "Questo luogo è parte della rete {network}", + "ru": "Эта станция - часть сети {network}" }, "question": { "en": "What network is this place a part of? (skip if none)", "ja": "ここは何のネットワークの一部ですか?(なければスキップ)", - "it": "Di quale rete fa parte questo luogo? (se non fa parte di nessuna rete, salta)" + "it": "Di quale rete fa parte questo luogo? (se non fa parte di nessuna rete, salta)", + "ru": "К какой сети относится эта станция? (пропустите, если неприменимо)" }, "freeform": { "key": "network" diff --git a/assets/themes/charging_stations/charging_stations.json b/assets/themes/charging_stations/charging_stations.json index 159cd5819f..16bc127864 100644 --- a/assets/themes/charging_stations/charging_stations.json +++ b/assets/themes/charging_stations/charging_stations.json @@ -14,7 +14,8 @@ "ru": "Карта зарядных станций по всему миру", "ja": "充電ステーションの世界地図", "zh_Hant": "全世界的充電站地圖", - "it": "Una mappa mondiale delle stazioni di ricarica" + "it": "Una mappa mondiale delle stazioni di ricarica", + "nl": "Een wereldwijde kaart van oplaadpunten" }, "description": { "en": "On this open map, one can find and mark information about charging stations", @@ -229,11 +230,12 @@ "ja": "{network}", "zh_Hant": "{network}", "nb_NO": "{network}", - "it": "{network}" + "it": "{network}", + "nl": "{network}" }, "question": { "en": "What network of this charging station under?", - "ru": "К какой сети относится эта станция?", + "ru": "К какой сети относится эта зарядная станция?", "ja": "この充電ステーションの運営チェーンはどこですか?", "zh_Hant": "充電站所屬的網路是?", "it": "A quale rete appartiene questa stazione di ricarica?" @@ -253,7 +255,8 @@ "ru": "Не является частью более крупной сети", "ja": "大規模な運営チェーンの一部ではない", "zh_Hant": "不屬於大型網路", - "it": "Non appartiene a una rete" + "it": "Non appartiene a una rete", + "nl": "Maakt geen deel uit van een netwerk" } }, { @@ -267,7 +270,8 @@ "ru": "AeroVironment", "ja": "AeroVironment", "zh_Hant": "AeroVironment", - "it": "AeroVironment" + "it": "AeroVironment", + "nl": "AeroVironment" } }, { @@ -281,7 +285,8 @@ "ru": "Blink", "ja": "Blink", "zh_Hant": "Blink", - "it": "Blink" + "it": "Blink", + "nl": "Blink" } }, { @@ -295,7 +300,8 @@ "ru": "eVgo", "ja": "eVgo", "zh_Hant": "eVgo", - "it": "eVgo" + "it": "eVgo", + "nl": "eVgo" } } ] diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index 342888881a..fbf13310c3 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -171,14 +171,16 @@ "en": "Climbing club", "nl": "Klimclub", "ja": "クライミングクラブ", - "nb_NO": "Klatreklubb" + "nb_NO": "Klatreklubb", + "ru": "Клуб скалолазания" }, "description": { "de": "Ein Kletterverein", "nl": "Een klimclub", "en": "A climbing club", "ja": "クライミングクラブ", - "nb_NO": "En klatreklubb" + "nb_NO": "En klatreklubb", + "ru": "Клуб скалолазания" } }, { @@ -241,7 +243,8 @@ "description": { "de": "Eine Kletterhalle", "en": "A climbing gym", - "ja": "クライミングジム" + "ja": "クライミングジム", + "nl": "Een klimzaal" }, "tagRenderings": [ "images", @@ -484,7 +487,8 @@ "presets": [ { "title": { - "en": "Climbing route" + "en": "Climbing route", + "nl": "Klimroute" }, "tags": [ "sport=climbing", @@ -790,7 +794,8 @@ "fr": "{name}", "id": "{name}", "ru": "{name}", - "ja": "{name}" + "ja": "{name}", + "nl": "{name}" }, "condition": "name~*" }, @@ -897,7 +902,8 @@ "en": "Is there a (unofficial) website with more informations (e.g. topos)?", "de": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?", "ja": "もっと情報のある(非公式の)ウェブサイトはありますか(例えば、topos)?", - "nl": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?" + "nl": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?", + "ru": "Есть ли (неофициальный) веб-сайт с более подробной информацией (напр., topos)?" }, "condition": { "and": [ @@ -976,7 +982,8 @@ { "if": "access=members", "then": { - "en": "Only club members" + "en": "Only club members", + "ru": "Только членам клуба" } }, { diff --git a/assets/themes/cyclestreets/cyclestreets.json b/assets/themes/cyclestreets/cyclestreets.json index 8a5956cd1a..b3b3abbf4c 100644 --- a/assets/themes/cyclestreets/cyclestreets.json +++ b/assets/themes/cyclestreets/cyclestreets.json @@ -27,7 +27,8 @@ "ja", "zh_Hant", "nb_NO", - "it" + "it", + "ru" ], "startLat": 51.2095, "startZoom": 14, @@ -222,7 +223,8 @@ "en": "All streets", "ja": "すべての道路", "nb_NO": "Alle gater", - "it": "Tutte le strade" + "it": "Tutte le strade", + "ru": "Все улицы" }, "description": { "nl": "Laag waar je een straat als fietsstraat kan markeren", @@ -247,7 +249,8 @@ "nl": "Straat", "en": "Street", "ja": "ストリート", - "it": "Strada" + "it": "Strada", + "ru": "Улица" }, "mappings": [ { diff --git a/assets/themes/drinking_water/drinking_water.json b/assets/themes/drinking_water/drinking_water.json index da21f5296b..9dee5a8a36 100644 --- a/assets/themes/drinking_water/drinking_water.json +++ b/assets/themes/drinking_water/drinking_water.json @@ -15,7 +15,8 @@ "fr": "Cette carte affiche les points d'accès public à de l'eau potable, et permet d'en ajouter facilement", "ja": "この地図には、一般にアクセス可能な飲料水スポットが示されており、簡単に追加することができる", "zh_Hant": "在這份地圖上,公共可及的飲水點可以顯示出來,也能輕易的增加", - "it": "Questa mappa mostra tutti i luoghi in cui è disponibile acqua potabile ed è possibile aggiungerne di nuovi" + "it": "Questa mappa mostra tutti i luoghi in cui è disponibile acqua potabile ed è possibile aggiungerne di nuovi", + "ru": "На этой карте показываются и могут быть легко добавлены общедоступные точки питьевой воды" }, "language": [ "en", diff --git a/assets/themes/facadegardens/facadegardens.json b/assets/themes/facadegardens/facadegardens.json index b472fdf051..8cc9369815 100644 --- a/assets/themes/facadegardens/facadegardens.json +++ b/assets/themes/facadegardens/facadegardens.json @@ -165,7 +165,8 @@ "nl": "Ligt de tuin in zon/half schaduw of schaduw?", "en": "Is the garden shaded or sunny?", "ja": "庭は日陰ですか、日当たりがいいですか?", - "it": "Il giardino è al sole o in ombra?" + "it": "Il giardino è al sole o in ombra?", + "ru": "Сад расположен на солнечной стороне или в тени?" } }, { @@ -185,7 +186,8 @@ "nl": "Er is een regenton", "en": "There is a rain barrel", "ja": "雨樽がある", - "it": "C'è un contenitore per raccogliere la pioggia" + "it": "C'è un contenitore per raccogliere la pioggia", + "ru": "Есть бочка с дождевой водой" } }, { @@ -198,7 +200,8 @@ "nl": "Er is geen regenton", "en": "There is no rain barrel", "ja": "雨樽はありません", - "it": "Non c'è un contenitore per raccogliere la pioggia" + "it": "Non c'è un contenitore per raccogliere la pioggia", + "ru": "Нет бочки с дождевой водой" } } ] @@ -263,7 +266,8 @@ "nl": "Wat voor planten staan hier?", "en": "What kinds of plants grow here?", "ja": "ここではどんな植物が育つんですか?", - "it": "Che tipi di piante sono presenti qui?" + "it": "Che tipi di piante sono presenti qui?", + "ru": "Какие виды растений обитают здесь?" }, "mappings": [ { @@ -310,13 +314,15 @@ "nl": "Meer details: {description}", "en": "More details: {description}", "ja": "詳細情報: {description}", - "it": "Maggiori dettagli: {description}" + "it": "Maggiori dettagli: {description}", + "ru": "Подробнее: {description}" }, "question": { "nl": "Aanvullende omschrijving van de tuin (indien nodig, en voor zover nog niet omschreven hierboven)", "en": "Extra describing info about the garden (if needed and not yet described above)", "ja": "庭園に関する追加の説明情報(必要な場合でまだ上記に記載されていない場合)", - "it": "Altre informazioni per descrivere il giardino (se necessarie e non riportate qui sopra)" + "it": "Altre informazioni per descrivere il giardino (se necessarie e non riportate qui sopra)", + "ru": "Дополнительная информация о саде (если требуется или еще не указана выше)" }, "freeform": { "key": "description", diff --git a/assets/themes/fritures/fritures.json b/assets/themes/fritures/fritures.json index 36cef08a01..3f3c51f425 100644 --- a/assets/themes/fritures/fritures.json +++ b/assets/themes/fritures/fritures.json @@ -118,7 +118,8 @@ "nl": "Wat is de website van deze frituur?", "fr": "Quel est le site web de cette friterie?", "ja": "このお店のホームページは何ですか?", - "it": "Qual è il sito web di questo negozio?" + "it": "Qual è il sito web di questo negozio?", + "ru": "Какой веб-сайт у этого магазина?" }, "freeform": { "key": "website", @@ -136,7 +137,8 @@ "fr": "Quel est le numéro de téléphone de cette friterie?", "ja": "電話番号は何番ですか?", "nb_NO": "Hva er telefonnummeret?", - "it": "Qual è il numero di telefono?" + "it": "Qual è il numero di telefono?", + "ru": "Какой телефон?" }, "freeform": { "key": "phone", @@ -275,7 +277,8 @@ "then": { "nl": "Je mag geen eigen containers meenemen om je bestelling in mee te nemen", "en": "Bringing your own container is not allowed", - "ja": "独自の容器を持参することはできません" + "ja": "独自の容器を持参することはできません", + "ru": "Приносить свою тару не разрешено" } }, { diff --git a/assets/themes/hailhydrant/hailhydrant.json b/assets/themes/hailhydrant/hailhydrant.json index fef5c8ab28..d465b72eb2 100644 --- a/assets/themes/hailhydrant/hailhydrant.json +++ b/assets/themes/hailhydrant/hailhydrant.json @@ -3,12 +3,14 @@ "title": { "en": "Hydrants, Extinguishers, Fire stations, and Ambulance stations.", "ja": "消火栓、消火器、消防署、救急ステーションです。", - "zh_Hant": "消防栓、滅火器、消防隊、以及急救站。" + "zh_Hant": "消防栓、滅火器、消防隊、以及急救站。", + "ru": "Пожарные гидранты, огнетушители, пожарные станции и станции скорой помощи." }, "shortDescription": { "en": "Map to show hydrants, extinguishers, fire stations, and ambulance stations.", "ja": "消火栓、消火器、消防署消火栓、消火器、消防署、および救急ステーションを表示します。", - "zh_Hant": "顯示消防栓、滅火器、消防隊與急救站的地圖。" + "zh_Hant": "顯示消防栓、滅火器、消防隊與急救站的地圖。", + "ru": "Карта пожарных гидрантов, огнетушителей, пожарных станций и станций скорой помощи." }, "description": { "en": "On this map you can find and update hydrants, fire stations, ambulance stations, and extinguishers in your favorite neighborhoods. \n\nYou can track your precise location (mobile only) and select layers that are relevant for you in the bottom left corner. You can also use this tool to add or edit pins (points of interest) to the map and provide additional details by answering available questions. \n\nAll changes you make will automatically be saved in the global database of OpenStreetMap and can be freely re-used by others.", @@ -39,7 +41,8 @@ "en": "Map of hydrants", "ja": "消火栓の地図", "zh_Hant": "消防栓地圖", - "nb_NO": "Kart over brannhydranter" + "nb_NO": "Kart over brannhydranter", + "ru": "Карта пожарных гидрантов" }, "minzoom": 14, "source": { @@ -61,19 +64,22 @@ "en": "Map layer to show fire hydrants.", "ja": "消火栓を表示するマップレイヤ。", "zh_Hant": "顯示消防栓的地圖圖層。", - "nb_NO": "Kartlag for å vise brannhydranter." + "nb_NO": "Kartlag for å vise brannhydranter.", + "ru": "Слой карты, отображающий пожарные гидранты." }, "tagRenderings": [ { "question": { "en": "What color is the hydrant?", "ja": "消火栓の色は何色ですか?", - "nb_NO": "Hvilken farge har brannhydranten?" + "nb_NO": "Hvilken farge har brannhydranten?", + "ru": "Какого цвета гидрант?" }, "render": { "en": "The hydrant color is {colour}", "ja": "消火栓の色は{color}です", - "nb_NO": "Brannhydranter er {colour}" + "nb_NO": "Brannhydranter er {colour}", + "ru": "Цвет гидранта {colour}" }, "freeform": { "key": "colour" @@ -87,7 +93,8 @@ }, "then": { "en": "The hydrant color is unknown.", - "ja": "消火栓の色は不明です。" + "ja": "消火栓の色は不明です。", + "ru": "Цвет гидранта не определён." }, "hideInAnswer": true }, @@ -99,7 +106,8 @@ }, "then": { "en": "The hydrant color is yellow.", - "ja": "消火栓の色は黄色です。" + "ja": "消火栓の色は黄色です。", + "ru": "Гидрант жёлтого цвета." } }, { @@ -111,7 +119,8 @@ "then": { "en": "The hydrant color is red.", "ja": "消火栓の色は赤です。", - "it": "L'idrante è rosso." + "it": "L'idrante è rosso.", + "ru": "Гидрант красного цвета." } } ] @@ -120,7 +129,8 @@ "question": { "en": "What type of hydrant is it?", "ja": "どんな消火栓なんですか?", - "it": "Di che tipo è questo idrante?" + "it": "Di che tipo è questo idrante?", + "ru": "К какому типу относится этот гидрант?" }, "freeform": { "key": "fire_hydrant:type" @@ -141,7 +151,8 @@ "then": { "en": "The hydrant type is unknown.", "ja": "消火栓の種類は不明です。", - "it": "Il tipo di idrante è sconosciuto." + "it": "Il tipo di idrante è sconosciuto.", + "ru": "Тип гидранта не определён." }, "hideInAnswer": true }, @@ -214,7 +225,8 @@ }, "then": { "en": "The hydrant is (fully or partially) working.", - "ja": "消火栓は(完全にまたは部分的に)機能しています。" + "ja": "消火栓は(完全にまたは部分的に)機能しています。", + "ru": "Гидрант (полностью или частично) в рабочем состоянии." } }, { @@ -238,7 +250,8 @@ }, "then": { "en": "The hydrant has been removed.", - "ja": "消火栓が撤去されました。" + "ja": "消火栓が撤去されました。", + "ru": "Гидрант демонтирован." } } ] @@ -282,7 +295,8 @@ "name": { "en": "Map of fire extinguishers.", "ja": "消火器の地図です。", - "nb_NO": "Kart over brannhydranter" + "nb_NO": "Kart over brannhydranter", + "ru": "Карта огнетушителей." }, "minzoom": 14, "source": { @@ -304,17 +318,20 @@ "en": "Map layer to show fire hydrants.", "ja": "消火栓を表示するマップレイヤ。", "zh_Hant": "顯示消防栓的地圖圖層。", - "nb_NO": "Kartlag for å vise brannslokkere." + "nb_NO": "Kartlag for å vise brannslokkere.", + "ru": "Слой карты, отображающий огнетушители." }, "tagRenderings": [ { "render": { "en": "Location: {location}", - "ja": "場所:{location}" + "ja": "場所:{location}", + "ru": "Местоположение: {location}" }, "question": { "en": "Where is it positioned?", - "ja": "どこにあるんですか?" + "ja": "どこにあるんですか?", + "ru": "Где это расположено?" }, "mappings": [ { @@ -325,7 +342,8 @@ }, "then": { "en": "Found indoors.", - "ja": "屋内にある。" + "ja": "屋内にある。", + "ru": "Внутри." } }, { @@ -336,7 +354,8 @@ }, "then": { "en": "Found outdoors.", - "ja": "屋外にある。" + "ja": "屋外にある。", + "ru": "Снаружи." } } ], @@ -367,11 +386,13 @@ "title": { "en": "Fire extinguisher", "ja": "消火器", - "nb_NO": "Brannslukker" + "nb_NO": "Brannslukker", + "ru": "Огнетушитель" }, "description": { "en": "A fire extinguisher is a small, portable device used to stop a fire", - "ja": "消火器は、火災を止めるために使用される小型で携帯可能な装置である" + "ja": "消火器は、火災を止めるために使用される小型で携帯可能な装置である", + "ru": "Огнетушитель - небольшое переносное устройство для тушения огня" } } ], @@ -383,7 +404,8 @@ "en": "Map of fire stations", "ja": "消防署の地図", "nb_NO": "Kart over brannstasjoner", - "it": "Mappa delle caserme dei vigili del fuoco" + "it": "Mappa delle caserme dei vigili del fuoco", + "ru": "Карта пожарных частей" }, "minzoom": 12, "source": { @@ -406,7 +428,8 @@ "description": { "en": "Map layer to show fire stations.", "ja": "消防署を表示するためのマップレイヤ。", - "it": "Livello che mostra le caserme dei vigili del fuoco." + "it": "Livello che mostra le caserme dei vigili del fuoco.", + "ru": "Слой карты, отображающий пожарные части." }, "tagRenderings": [ { @@ -416,13 +439,14 @@ "question": { "en": "What is the name of this fire station?", "ja": "この消防署の名前は何ですか?", - "ru": "Как называется пожарная часть?", + "ru": "Как называется эта пожарная часть?", "it": "Come si chiama questa caserma dei vigili del fuoco?" }, "render": { "en": "This station is called {name}.", "ja": "このステーションの名前は{name}です。", - "it": "Questa caserma si chiama {name}." + "it": "Questa caserma si chiama {name}.", + "ru": "Эта часть называется {name}." } }, { @@ -432,24 +456,28 @@ "question": { "en": " What is the street name where the station located?", "ja": " 救急ステーションの所在地はどこですか?", - "it": " Qual è il nome della via in cui si trova la caserma?" + "it": " Qual è il nome della via in cui si trova la caserma?", + "ru": " По какому адресу расположена эта часть?" }, "render": { "en": "This station is along a highway called {addr:street}.", - "ja": "{addr:street} 沿いにあります。" + "ja": "{addr:street} 沿いにあります。", + "ru": "Часть расположена вдоль шоссе {addr:street}." } }, { "question": { "en": "Where is the station located? (e.g. name of neighborhood, villlage, or town)", - "ja": "このステーションの住所は?(例: 地区、村、または町の名称)" + "ja": "このステーションの住所は?(例: 地区、村、または町の名称)", + "ru": "Где расположена часть? (напр., название населённого пункта)" }, "freeform": { "key": "addr:place" }, "render": { "en": "This station is found within {addr:place}.", - "ja": "このステーションは{addr:place}にあります。" + "ja": "このステーションは{addr:place}にあります。", + "ru": "Эта часть расположена в {addr:place}." } }, { @@ -560,7 +588,8 @@ ], "title": { "en": "Fire station", - "ja": "消防署" + "ja": "消防署", + "ru": "Пожарная часть" }, "description": { "en": "A fire station is a place where the fire trucks and firefighters are located when not in operation.", @@ -573,7 +602,8 @@ "id": "ambulancestation", "name": { "en": "Map of ambulance stations", - "ja": "救急ステーションの地図" + "ja": "救急ステーションの地図", + "ru": "Карта станций скорой помощи" }, "minzoom": 12, "source": { @@ -602,11 +632,12 @@ "question": { "en": "What is the name of this ambulance station?", "ja": "この救急ステーションの名前は何ですか?", - "ru": "Как называется станция скорой помощи?" + "ru": "Как называется эта станция скорой помощи?" }, "render": { "en": "This station is called {name}.", - "ja": "このステーションの名前は{name}です。" + "ja": "このステーションの名前は{name}です。", + "ru": "Эта станция называется {name}." } }, { @@ -615,17 +646,20 @@ }, "question": { "en": " What is the street name where the station located?", - "ja": " 救急ステーションの所在地はどこですか?" + "ja": " 救急ステーションの所在地はどこですか?", + "ru": " По какому адресу расположена эта станция?" }, "render": { "en": "This station is along a highway called {addr:street}.", - "ja": "{addr:street} 沿いにあります。" + "ja": "{addr:street} 沿いにあります。", + "ru": "Эта станция расположена вдоль шоссе {addr:street}." } }, { "question": { "en": "Where is the station located? (e.g. name of neighborhood, villlage, or town)", - "ja": "このステーションの住所は?(例: 地区、村、または町の名称)" + "ja": "このステーションの住所は?(例: 地区、村、または町の名称)", + "ru": "Где расположена станция? (напр., название населённого пункта)" }, "freeform": { "key": "addr:place" @@ -735,7 +769,8 @@ }, "description": { "en": "Add an ambulance station to the map", - "ja": "救急ステーション(消防署)をマップに追加する" + "ja": "救急ステーション(消防署)をマップに追加する", + "ru": "Добавить станцию скорой помощи на карту" } } ], diff --git a/assets/themes/maps/maps.json b/assets/themes/maps/maps.json index cc0d114e3f..e34cdce30b 100644 --- a/assets/themes/maps/maps.json +++ b/assets/themes/maps/maps.json @@ -5,7 +5,8 @@ "nl": "Een kaart met Kaarten", "fr": "Carte des cartes", "ja": "マップのマップ", - "zh_Hant": "地圖的地圖" + "zh_Hant": "地圖的地圖", + "ru": "Карта карт" }, "shortDescription": { "en": "This theme shows all (touristic) maps that OpenStreetMap knows of", @@ -26,7 +27,8 @@ "nl", "fr", "ja", - "zh_Hant" + "zh_Hant", + "ru" ], "maintainer": "MapComplete", "icon": "./assets/themes/maps/logo.svg", diff --git a/assets/themes/personalLayout/personalLayout.json b/assets/themes/personalLayout/personalLayout.json index b834fbdc3c..02e26cdbd2 100644 --- a/assets/themes/personalLayout/personalLayout.json +++ b/assets/themes/personalLayout/personalLayout.json @@ -20,7 +20,8 @@ "fr": "Crée un thème personnalisé basé sur toutes les couches disponibles de tous les thèmes", "de": "Erstellen Sie ein persönliches Thema auf der Grundlage aller verfügbaren Ebenen aller Themen", "ja": "すべてのテーマの使用可能なすべてのレイヤーに基づいて個人用テーマを作成する", - "zh_Hant": "從所有可用的主題圖層創建個人化主題" + "zh_Hant": "從所有可用的主題圖層創建個人化主題", + "ru": "Создать персональную тему на основе доступных слоёв тем" }, "language": [ "en", @@ -31,7 +32,8 @@ "fr", "de", "ja", - "zh_Hant" + "zh_Hant", + "ru" ], "maintainer": "MapComplete", "icon": "./assets/svg/addSmall.svg", diff --git a/assets/themes/playgrounds/playgrounds.json b/assets/themes/playgrounds/playgrounds.json index ce3a2b398b..418aa36bc4 100644 --- a/assets/themes/playgrounds/playgrounds.json +++ b/assets/themes/playgrounds/playgrounds.json @@ -5,28 +5,32 @@ "en": "Playgrounds", "fr": "Aires de jeux", "ja": "遊び場", - "zh_Hant": "遊樂場" + "zh_Hant": "遊樂場", + "ru": "Игровые площадки" }, "shortDescription": { "nl": "Een kaart met speeltuinen", "en": "A map with playgrounds", "fr": "Une carte des aires de jeux", "ja": "遊び場のある地図", - "zh_Hant": "遊樂場的地圖" + "zh_Hant": "遊樂場的地圖", + "ru": "Карта игровых площадок" }, "description": { "nl": "Op deze kaart vind je speeltuinen en kan je zelf meer informatie en foto's toevoegen", "en": "On this map, you find playgrounds and can add more information", "fr": "Cette carte affiche les aires de jeux et permet d'ajouter plus d'informations", "ja": "この地図では遊び場を見つけ情報を追加することができます", - "zh_Hant": "在這份地圖上,你可以尋找遊樂場以及其相關資訊" + "zh_Hant": "在這份地圖上,你可以尋找遊樂場以及其相關資訊", + "ru": "На этой карте можно найти игровые площадки и добавить дополнительную информацию" }, "language": [ "nl", "en", "fr", "ja", - "zh_Hant" + "zh_Hant", + "ru" ], "maintainer": "", "icon": "./assets/themes/playgrounds/playground.svg", diff --git a/assets/themes/shops/shops.json b/assets/themes/shops/shops.json index 3883a9ef83..571d0e1b6d 100644 --- a/assets/themes/shops/shops.json +++ b/assets/themes/shops/shops.json @@ -4,7 +4,8 @@ "en": "Open Shop Map", "fr": "Carte des magasins", "ja": "オープン ショップ マップ", - "zh_Hant": "開放商店地圖" + "zh_Hant": "開放商店地圖", + "ru": "Открыть карту магазинов" }, "shortDescription": { "en": "An editable map with basic shop information", @@ -94,7 +95,8 @@ "en": "A shop", "fr": "Un magasin", "ja": "ショップ", - "nl": "Een winkel" + "nl": "Een winkel", + "ru": "Магазин" }, "tagRenderings": [ "images", @@ -102,7 +104,7 @@ "question": { "en": "What is the name of this shop?", "fr": "Qu'est-ce que le nom de ce magasin?", - "ru": "Как называется магазин?", + "ru": "Как называется этот магазин?", "ja": "このお店の名前は何ですか?", "nl": "Wat is de naam van deze winkel?" }, @@ -120,7 +122,8 @@ "question": { "en": "What does this shop sell?", "fr": "Que vends ce magasin ?", - "ja": "このお店では何を売っていますか?" + "ja": "このお店では何を売っていますか?", + "ru": "Что продаётся в этом магазине?" }, "freeform": { "key": "shop" @@ -232,7 +235,8 @@ "en": "What is the phone number?", "fr": "Quel est le numéro de téléphone ?", "ja": "電話番号は何番ですか?", - "nl": "Wat is het telefoonnummer?" + "nl": "Wat is het telefoonnummer?", + "ru": "Какой телефон?" }, "freeform": { "key": "phone", @@ -252,7 +256,8 @@ "en": "What is the website of this shop?", "fr": "Quel est le site internet de ce magasin ?", "ja": "このお店のホームページは何ですか?", - "nl": "Wat is de website van deze winkel?" + "nl": "Wat is de website van deze winkel?", + "ru": "Какой веб-сайт у этого магазина?" }, "freeform": { "key": "website", @@ -270,7 +275,8 @@ "question": { "en": "What is the email address of this shop?", "fr": "Quelle est l'adresse électronique de ce magasin ?", - "ja": "このお店のメールアドレスは何ですか?" + "ja": "このお店のメールアドレスは何ですか?", + "ru": "Каков адрес электронной почты этого магазина?" }, "freeform": { "key": "email", @@ -288,7 +294,8 @@ "en": "What are the opening hours of this shop?", "fr": "Quels sont les horaires d'ouverture de ce magasin ?", "ja": "この店の営業時間は何時から何時までですか?", - "nl": "Wat zijn de openingsuren van deze winkel?" + "nl": "Wat zijn de openingsuren van deze winkel?", + "ru": "Каковы часы работы этого магазина?" }, "freeform": { "key": "opening_hours", diff --git a/assets/themes/speelplekken/speelplekken_temp.json b/assets/themes/speelplekken/speelplekken_temp.json index f18fbbad10..bf3975dc36 100644 --- a/assets/themes/speelplekken/speelplekken_temp.json +++ b/assets/themes/speelplekken/speelplekken_temp.json @@ -28,7 +28,7 @@ "builtin": "play_forest", "override": { "source": { - "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", + "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", "geoJsonZoomLevel": 14, "isOsmCache": true }, @@ -105,11 +105,31 @@ { "builtin": "slow_roads", "override": { + "+tagRenderings": [ + { + "question": "Is dit een publiek toegankelijk pad?", + "mappings": [ + { + "if": "access=private", + "then": "Dit is een privaat pad" + }, + { + "if": "access=no", + "then": "Dit is een privaat pad", + "hideInAnswer": true + }, + { + "if": "access=permissive", + "then": "Dit pad is duidelijk in private eigendom, maar er hangen geen verbodsborden dus mag men erover" + } + ] + } + ], "calculatedTags": [ "_part_of_walking_routes=Array.from(new Set(feat.memberships().map(r => \"\" + r.relation.tags.name + \"\"))).join(', ')", "_is_shadowed=feat.overlapWith('shadow').length > 0 ? 'yes': ''" ], - "minzoom": 9, + "minzoom": 18, "source": { "geoJsonLocal": "http://127.0.0.1:8080/speelplekken_{layer}_{z}_{x}_{y}.geojson", "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{layer}_{z}_{x}_{y}.geojson", @@ -235,21 +255,21 @@ "maxZoom": 16, "minNeededElements": 100 }, - "roamingRenderings": [ - { - "render": "Maakt deel uit van {_part_of_walking_routes}", - "condition": "_part_of_walking_routes~*" - }, - { - "render": "Een kinder-reportage vinden jullie hier", - "freeform": { - "key": "video", - "type": "url" - }, - "question": "Wat is de link naar de video-reportage?" - } - ], "overrideAll": { + "+tagRenderings": [ + { + "render": "Maakt deel uit van {_part_of_walking_routes}", + "condition": "_part_of_walking_routes~*" + }, + { + "render": "Een kinder-reportage vinden jullie hier", + "freeform": { + "key": "video", + "type": "url" + }, + "question": "Wat is de link naar de video-reportage?" + } + ], "isShown": { "render": "yes", "mappings": [ diff --git a/assets/themes/sport_pitches/sport_pitches.json b/assets/themes/sport_pitches/sport_pitches.json index 5d872c8dca..7f1fdeebda 100644 --- a/assets/themes/sport_pitches/sport_pitches.json +++ b/assets/themes/sport_pitches/sport_pitches.json @@ -5,14 +5,16 @@ "fr": "Terrains de sport", "en": "Sport pitches", "ja": "スポーツ競技場", - "zh_Hant": "運動場地" + "zh_Hant": "運動場地", + "ru": "Спортивные площадки" }, "shortDescription": { "nl": "Deze kaart toont sportvelden", "fr": "Une carte montrant les terrains de sport", "en": "A map showing sport pitches", "ja": "スポーツ競技場を示す地図", - "zh_Hant": "顯示運動場地的地圖" + "zh_Hant": "顯示運動場地的地圖", + "ru": "Карта, отображающая спортивные площадки" }, "description": { "nl": "Een sportveld is een ingerichte plaats met infrastructuur om een sport te beoefenen", @@ -26,7 +28,8 @@ "fr", "en", "ja", - "zh_Hant" + "zh_Hant", + "ru" ], "maintainer": "", "icon": "./assets/layers/sport_pitch/table_tennis.svg", diff --git a/assets/themes/trees/trees.json b/assets/themes/trees/trees.json index daf26fe5b7..8545caa9a9 100644 --- a/assets/themes/trees/trees.json +++ b/assets/themes/trees/trees.json @@ -15,7 +15,8 @@ "fr": "Carte des arbres", "it": "Mappa tutti gli alberi", "ja": "すべての樹木をマッピングする", - "zh_Hant": "所有樹木的地圖" + "zh_Hant": "所有樹木的地圖", + "ru": "Карта деревьев" }, "description": { "nl": "Breng bomen in kaart!", @@ -23,7 +24,8 @@ "fr": "Cartographions tous les arbres !", "it": "Mappa tutti gli alberi!", "ja": "すべての樹木をマッピングします!", - "zh_Hant": "繪製所有樹木!" + "zh_Hant": "繪製所有樹木!", + "ru": "Нанесите все деревья на карту!" }, "language": [ "nl", diff --git a/assets/themes/widths/width.json b/assets/themes/widths/width.json index 48a1e883a5..298b9a1281 100644 --- a/assets/themes/widths/width.json +++ b/assets/themes/widths/width.json @@ -64,7 +64,13 @@ }, "tagRenderings": [ { - "render": "Deze straat is {width:carriageway}m breed" + "render": "Deze straat is {width:carriageway}m breed", + "question": "Hoe breed is deze straat?", + "freeform": { + "key": "width:carriageway", + "type": "length", + "helperArgs": [21, "map"] + } }, { "render": "Deze straat heeft {_width:difference}m te weinig:", diff --git a/index.ts b/index.ts index 70b06bf305..634ad8533f 100644 --- a/index.ts +++ b/index.ts @@ -19,10 +19,13 @@ import DirectionInput from "./UI/Input/DirectionInput"; import SpecialVisualizations from "./UI/SpecialVisualizations"; import ShowDataLayer from "./UI/ShowDataLayer"; import * as L from "leaflet"; +import ValidatedTextField from "./UI/Input/ValidatedTextField"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); DirectionInput.constructMinimap = options => new Minimap(options) +ValidatedTextField.bestLayerAt = (location, layerPref) => AvailableBaseLayers.SelectBestLayerAccordingTo(location, layerPref) SpecialVisualizations.constructMiniMap = options => new Minimap(options) SpecialVisualizations.constructShowDataLayer = (features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 0cd328a6ae..c0f5353343 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -487,6 +487,11 @@ } } } + }, + "presets": { + "0": { + "title": "Обслуживание велосипедов/магазин" + } } }, "defibrillator": { @@ -1064,6 +1069,7 @@ "1": { "question": "Вы хотите добавить описание?" } - } + }, + "name": "Смотровая площадка" } -} \ No newline at end of file +} diff --git a/langs/pt_BR.json b/langs/pt_BR.json index 638ab0d39a..268c8e4e60 100644 --- a/langs/pt_BR.json +++ b/langs/pt_BR.json @@ -122,8 +122,10 @@ "thanksForSharing": "Obrigado por compartilhar!", "copiedToClipboard": "Link copiado para a área de transferência", "addToHomeScreen": "

Adicionar à sua tela inicial

Você pode adicionar facilmente este site à tela inicial do smartphone para uma sensação nativa. Clique no botão 'adicionar à tela inicial' na barra de URL para fazer isso.", - "intro": "

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:" - } + "intro": "

Compartilhe este mapa

Compartilhe este mapa copiando o link abaixo e enviando-o para amigos e familiares:", + "embedIntro": "

Incorpore em seu site

Por favor, incorpore este mapa em seu site.
Nós o encorajamos a fazer isso - você nem precisa pedir permissão.
É gratuito e sempre será. Quanto mais pessoas usarem isso, mais valioso se tornará." + }, + "aboutMapcomplete": "

Sobre o MapComplete

Com o MapComplete, você pode enriquecer o OpenStreetMap com informações sobre umúnico tema.Responda a algumas perguntas e, em minutos, suas contribuições estarão disponíveis em todo o mundo! Omantenedor do temadefine elementos, questões e linguagens para o tema.

Saiba mais

MapComplete sempreoferece a próxima etapapara saber mais sobre o OpenStreetMap.

  • Quando incorporado em um site, o iframe vincula-se a um MapComplete em tela inteira
  • A versão em tela inteira oferece informações sobre o OpenStreetMap
  • A visualização funciona sem login, mas a edição requer um login do OSM.
  • Se você não estiver conectado, será solicitado que você faça o login
  • Depois de responder a uma única pergunta, você pode adicionar novos aponta para o mapa
  • Depois de um tempo, as tags OSM reais são mostradas, posteriormente vinculadas ao wiki


Você percebeuum problema? Você tem umasolicitação de recurso ? Querajudar a traduzir? Acesse o código-fonteou rastreador de problemas.

Quer verseu progresso? Siga a contagem de edição emOsmCha.

" }, "index": { "pickTheme": "Escolha um tema abaixo para começar.", @@ -142,10 +144,13 @@ "no_reviews_yet": "Não há comentários ainda. Seja o primeiro a escrever um e ajude a abrir os dados e os negócios!", "name_required": "É necessário um nome para exibir e criar comentários", "title_singular": "Um comentário", - "title": "{count} comentários" + "title": "{count} comentários", + "tos": "Se você criar um comentário, você concorda com o TOS e a política de privacidade de Mangrove.reviews ", + "affiliated_reviewer_warning": "(Revisão de afiliados)" }, "favourite": { "reload": "Recarregar dados", - "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais" + "panelIntro": "

Seu tema pessoal

Ative suas camadas favoritas de todos os temas oficiais", + "loginNeeded": "

Entrar

Um layout pessoal está disponível apenas para usuários do OpenStreetMap" } } diff --git a/langs/shared-questions/de.json b/langs/shared-questions/de.json index ff0b97af86..6faff774e2 100644 --- a/langs/shared-questions/de.json +++ b/langs/shared-questions/de.json @@ -6,6 +6,27 @@ "opening_hours": { "question": "Was sind die Öffnungszeiten von {name}?", "render": "

Öffnungszeiten

{opening_hours_table(opening_hours)}" + }, + "level": { + "mappings": { + "2": { + "then": "Ist im ersten Stock" + }, + "1": { + "then": "Ist im Erdgeschoss" + } + }, + "render": "Befindet sich im {level}ten Stock", + "question": "In welchem Stockwerk befindet sich dieses Objekt?" + }, + "description": { + "question": "Gibt es noch etwas, das die vorhergehenden Fragen nicht abgedeckt haben? Hier wäre Platz dafür.
Bitte keine bereits erhobenen Informationen." + }, + "website": { + "question": "Was ist die Website von {name}?" + }, + "email": { + "question": "Was ist die Mail-Adresse von {name}?" } } -} \ No newline at end of file +} diff --git a/langs/shared-questions/en.json b/langs/shared-questions/en.json index a8b983d9e6..d11785526e 100644 --- a/langs/shared-questions/en.json +++ b/langs/shared-questions/en.json @@ -15,6 +15,21 @@ "opening_hours": { "question": "What are the opening hours of {name}?", "render": "

Opening hours

{opening_hours_table(opening_hours)}" + }, + "level": { + "question": "On what level is this feature located?", + "render": "Located on the {level}th floor", + "mappings": { + "0": { + "then": "Located underground" + }, + "1": { + "then": "Located on the ground floor" + }, + "2": { + "then": "Located on the first floor" + } + } } } } \ No newline at end of file diff --git a/langs/shared-questions/nl.json b/langs/shared-questions/nl.json index 2e50d77b1e..fd5a2a9b69 100644 --- a/langs/shared-questions/nl.json +++ b/langs/shared-questions/nl.json @@ -15,6 +15,21 @@ "opening_hours": { "question": "Wat zijn de openingsuren van {name}?", "render": "

Openingsuren

{opening_hours_table(opening_hours)}" + }, + "level": { + "question": "Op welke verdieping bevindt dit punt zich?", + "render": "Bevindt zich op de {level}de verdieping", + "mappings": { + "0": { + "then": "Bevindt zich ondergronds" + }, + "1": { + "then": "Bevindt zich gelijkvloers" + }, + "2": { + "then": "Bevindt zich op de eerste verdieping" + } + } } } } \ No newline at end of file diff --git a/langs/shared-questions/pt_BR.json b/langs/shared-questions/pt_BR.json index 0967ef424b..9c577c3966 100644 --- a/langs/shared-questions/pt_BR.json +++ b/langs/shared-questions/pt_BR.json @@ -1 +1,30 @@ -{} +{ + "undefined": { + "level": { + "render": "Localizado no {level}o andar", + "mappings": { + "2": { + "then": "Localizado no primeiro andar" + }, + "1": { + "then": "Localizado no térreo" + }, + "0": { + "then": "Localizado no subsolo" + } + } + }, + "opening_hours": { + "question": "Qual o horário de funcionamento de {name}?" + }, + "website": { + "question": "Qual o site de {name}?" + }, + "email": { + "question": "Qual o endereço de e-mail de {name}?" + }, + "phone": { + "question": "Qual o número de telefone de {name}?" + } + } +} diff --git a/langs/shared-questions/ru.json b/langs/shared-questions/ru.json index a06bc76078..93c56dc441 100644 --- a/langs/shared-questions/ru.json +++ b/langs/shared-questions/ru.json @@ -15,6 +15,20 @@ "opening_hours": { "question": "Какое время работы у {name}?", "render": "

Часы работы

{opening_hours_table(opening_hours)}" + }, + "level": { + "mappings": { + "2": { + "then": "Расположено на первом этаже" + }, + "1": { + "then": "Расположено на первом этаже" + }, + "0": { + "then": "Расположено под землей" + } + }, + "render": "Расположено на {level}ом этаже" } } -} \ No newline at end of file +} diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 3f20598ae6..7569536467 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -273,6 +273,9 @@ "3": { "render": "Deze plaats vraagt {charge}", "question": "Hoeveel kost deze plaats?" + }, + "9": { + "render": "Officiële website: : {website}" } } } @@ -280,13 +283,33 @@ }, "charging_stations": { "title": "Oplaadpunten", + "shortDescription": "Een wereldwijde kaart van oplaadpunten", "layers": { "0": { "name": "Oplaadpunten", "title": { "render": "Oplaadpunt" }, - "description": "Een oplaadpunt" + "description": "Een oplaadpunt", + "tagRenderings": { + "6": { + "render": "{network}", + "mappings": { + "0": { + "then": "Maakt geen deel uit van een netwerk" + }, + "1": { + "then": "AeroVironment" + }, + "2": { + "then": "Blink" + }, + "3": { + "then": "eVgo" + } + } + } + } } } }, @@ -333,6 +356,7 @@ } } }, + "description": "Een klimzaal", "tagRenderings": { "3": { "render": "{name}", @@ -368,6 +392,11 @@ "question": "Hoe moeilijk is deze klimroute volgens het Franse/Belgische systeem?", "render": "De klimmoeilijkheid is {climbing:grade:french} volgens het Franse/Belgische systeem" } + }, + "presets": { + "0": { + "title": "Klimroute" + } } }, "3": { @@ -419,6 +448,9 @@ }, "description": "Een klimgelegenheid?", "tagRenderings": { + "1": { + "render": "{name}" + }, "2": { "mappings": { "0": { diff --git a/langs/themes/ru.json b/langs/themes/ru.json index cb0a7dbe71..74f15b8753 100644 --- a/langs/themes/ru.json +++ b/langs/themes/ru.json @@ -278,7 +278,19 @@ } }, "6": { - "question": "Кто может использовать эту станцию утилизации?" + "question": "Кто может использовать эту станцию утилизации?", + "mappings": { + "2": { + "then": "Любой может воспользоваться этой станцией утилизации" + }, + "3": { + "then": "Любой может воспользоваться этой станцией утилизации" + } + } + }, + "7": { + "render": "Эта станция - часть сети {network}", + "question": "К какой сети относится эта станция? (пропустите, если неприменимо)" } } } @@ -301,7 +313,7 @@ }, "6": { "render": "{network}", - "question": "К какой сети относится эта станция?", + "question": "К какой сети относится эта зарядная станция?", "mappings": { "0": { "then": "Не является частью более крупной сети" @@ -335,6 +347,12 @@ "0": { "render": "{name}" } + }, + "presets": { + "0": { + "title": "Клуб скалолазания", + "description": "Клуб скалолазания" + } } }, "1": { @@ -367,6 +385,16 @@ } }, "roamingRenderings": { + "0": { + "question": "Есть ли (неофициальный) веб-сайт с более подробной информацией (напр., topos)?" + }, + "2": { + "mappings": { + "3": { + "then": "Только членам клуба" + } + } + }, "9": { "mappings": { "0": { @@ -379,18 +407,49 @@ } } }, + "fietsstraten": { + "layers": { + "2": { + "name": "Все улицы", + "title": { + "render": "Улица" + } + } + } + }, "cyclofix": { "title": "Cyclofix - открытая карта для велосипедистов" }, "drinking_water": { - "title": "Питьевая вода" + "title": "Питьевая вода", + "description": "На этой карте показываются и могут быть легко добавлены общедоступные точки питьевой воды" }, "facadegardens": { "layers": { "0": { "tagRenderings": { + "2": { + "question": "Сад расположен на солнечной стороне или в тени?" + }, + "3": { + "mappings": { + "0": { + "then": "Есть бочка с дождевой водой" + }, + "1": { + "then": "Нет бочки с дождевой водой" + } + } + }, "4": { "render": "Дата строительства сада: {start_date}" + }, + "6": { + "question": "Какие виды растений обитают здесь?" + }, + "7": { + "render": "Подробнее: {description}", + "question": "Дополнительная информация о саде (если требуется или еще не указана выше)" } } } @@ -401,26 +460,70 @@ "0": { "tagRenderings": { "3": { - "render": "{website}" + "render": "{website}", + "question": "Какой веб-сайт у этого магазина?" + }, + "4": { + "question": "Какой телефон?" + }, + "8": { + "mappings": { + "1": { + "then": "Приносить свою тару не разрешено" + } + } } } } } }, "hailhydrant": { + "title": "Пожарные гидранты, огнетушители, пожарные станции и станции скорой помощи.", + "shortDescription": "Карта пожарных гидрантов, огнетушителей, пожарных станций и станций скорой помощи.", "layers": { "0": { + "name": "Карта пожарных гидрантов", "title": { "render": "Гидрант" }, + "description": "Слой карты, отображающий пожарные гидранты.", "tagRenderings": { + "0": { + "question": "Какого цвета гидрант?", + "render": "Цвет гидранта {colour}", + "mappings": { + "0": { + "then": "Цвет гидранта не определён." + }, + "1": { + "then": "Гидрант жёлтого цвета." + }, + "2": { + "then": "Гидрант красного цвета." + } + } + }, "1": { + "question": "К какому типу относится этот гидрант?", "render": " Тип гидранта: {fire_hydrant:type}", "mappings": { + "0": { + "then": "Тип гидранта не определён." + }, "3": { "then": " Тип стены." } } + }, + "2": { + "mappings": { + "0": { + "then": "Гидрант (полностью или частично) в рабочем состоянии." + }, + "2": { + "then": "Гидрант демонтирован." + } + } } }, "presets": { @@ -430,38 +533,98 @@ } }, "1": { + "name": "Карта огнетушителей.", "title": { "render": "Огнетушители" + }, + "description": "Слой карты, отображающий огнетушители.", + "tagRenderings": { + "0": { + "render": "Местоположение: {location}", + "question": "Где это расположено?", + "mappings": { + "0": { + "then": "Внутри." + }, + "1": { + "then": "Снаружи." + } + } + } + }, + "presets": { + "0": { + "title": "Огнетушитель", + "description": "Огнетушитель - небольшое переносное устройство для тушения огня" + } } }, "2": { + "name": "Карта пожарных частей", "title": { "render": "Пожарная часть" }, + "description": "Слой карты, отображающий пожарные части.", "tagRenderings": { "0": { - "question": "Как называется пожарная часть?" + "question": "Как называется эта пожарная часть?", + "render": "Эта часть называется {name}." + }, + "1": { + "question": " По какому адресу расположена эта часть?", + "render": "Часть расположена вдоль шоссе {addr:street}." + }, + "2": { + "question": "Где расположена часть? (напр., название населённого пункта)", + "render": "Эта часть расположена в {addr:place}." + } + }, + "presets": { + "0": { + "title": "Пожарная часть" } } }, "3": { + "name": "Карта станций скорой помощи", "title": { "render": "Станция скорой помощи" }, "tagRenderings": { "0": { - "question": "Как называется станция скорой помощи?" + "question": "Как называется эта станция скорой помощи?", + "render": "Эта станция называется {name}." + }, + "1": { + "question": " По какому адресу расположена эта станция?", + "render": "Эта станция расположена вдоль шоссе {addr:street}." + }, + "2": { + "question": "Где расположена станция? (напр., название населённого пункта)" } }, "presets": { "0": { - "title": "Станция скорой помощи" + "title": "Станция скорой помощи", + "description": "Добавить станцию скорой помощи на карту" } } } } }, + "maps": { + "title": "Карта карт" + }, + "personal": { + "description": "Создать персональную тему на основе доступных слоёв тем" + }, + "playgrounds": { + "title": "Игровые площадки", + "shortDescription": "Карта игровых площадок", + "description": "На этой карте можно найти игровые площадки и добавить дополнительную информацию" + }, "shops": { + "title": "Открыть карту магазинов", "layers": { "0": { "name": "Магазин", @@ -476,11 +639,13 @@ } } }, + "description": "Магазин", "tagRenderings": { "1": { - "question": "Как называется магазин?" + "question": "Как называется этот магазин?" }, "2": { + "question": "Что продаётся в этом магазине?", "mappings": { "1": { "then": "Супермаркет" @@ -497,16 +662,20 @@ } }, "3": { - "render": "{phone}" + "render": "{phone}", + "question": "Какой телефон?" }, "4": { - "render": "{website}" + "render": "{website}", + "question": "Какой веб-сайт у этого магазина?" }, "5": { - "render": "{email}" + "render": "{email}", + "question": "Каков адрес электронной почты этого магазина?" }, "6": { - "render": "{opening_hours_table(opening_hours)}" + "render": "{opening_hours_table(opening_hours)}", + "question": "Каковы часы работы этого магазина?" } }, "presets": { @@ -518,11 +687,17 @@ } } }, + "sport_pitches": { + "title": "Спортивные площадки", + "shortDescription": "Карта, отображающая спортивные площадки" + }, "toilets": { "title": "Открытая карта туалетов", "description": "Карта общественных туалетов" }, "trees": { - "title": "Деревья" + "title": "Деревья", + "shortDescription": "Карта деревьев", + "description": "Нанесите все деревья на карту!" } } \ No newline at end of file diff --git a/preferences.ts b/preferences.ts index 1c1773a143..a7ae07dedc 100644 --- a/preferences.ts +++ b/preferences.ts @@ -12,7 +12,7 @@ import BaseUIElement from "./UI/BaseUIElement"; import Table from "./UI/Base/Table"; -const connection = new OsmConnection(false, new UIEventSource(undefined), ""); +const connection = new OsmConnection(false, false, new UIEventSource(undefined), ""); let rendered = false; diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index ccf230275e..174b1d3df0 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -56,7 +56,8 @@ export default class ScriptUtils { const urlObj = new URL(url) https.get({ host: urlObj.host, - path: urlObj.pathname, + path: urlObj.pathname + urlObj.search, + port: urlObj.port, headers: { "accept": "application/json" @@ -74,6 +75,7 @@ export default class ScriptUtils { try { resolve(JSON.parse(result)) } catch (e) { + console.error("Could not parse the following as JSON:", result) reject(e) } }); diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index f7651c4b41..733e798fc1 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -94,7 +94,8 @@ async function downloadRaw(targetdir: string, r: TileRange, overpass: Overpass)/ } ) .catch(err => { - console.log("Could not download - probably hit the rate limit; waiting a bit") + console.log(url) + console.log("Could not download - probably hit the rate limit; waiting a bit. ("+err+")") failed++; return ScriptUtils.sleep(60000).then(() => console.log("Waiting is done")) }) diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts index 492a669e98..9f957553ec 100644 --- a/scripts/generateTranslations.ts +++ b/scripts/generateTranslations.ts @@ -212,6 +212,7 @@ function generateTranslationsObjectFrom(objects: { path: string, parsed: { id: s } function MergeTranslation(source: any, target: any, language: string, context: string = "") { + for (const key in source) { if (!source.hasOwnProperty(key)) { continue @@ -220,6 +221,9 @@ function MergeTranslation(source: any, target: any, language: string, context: s const targetV = target[key] if (typeof sourceV === "string") { if(targetV === undefined){ + if(typeof target === "string"){ + throw "Trying to merge a translation into a fixed string at "+context+" for key "+key; + } target[key] = source[key]; continue; } diff --git a/test.ts b/test.ts index 5d077d3544..21ca94b74b 100644 --- a/test.ts +++ b/test.ts @@ -10,6 +10,8 @@ import {Translation} from "./UI/i18n/Translation"; import LocationInput from "./UI/Input/LocationInput"; import Loc from "./Models/Loc"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; +import LengthInput from "./UI/Input/LengthInput"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; import Combine from "./UI/Base/Combine"; import {VariableUiElement} from "./UI/Base/VariableUIElement"; @@ -152,14 +154,16 @@ function TestMiniMap() { } //*/ -const li = new LocationInput({ - preferCategory:"photo", - centerLocation: - new UIEventSource({ - lat: 51.21576, lon: 3.22001, zoom: 19 - }) +const loc = new UIEventSource({ + zoom: 24, + lat: 51.21043, + lon: 3.21389 }) - li.SetStyle("height: 20rem") +const li = new LengthInput( + AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource("map","photo")), + loc +) + li.SetStyle("height: 30rem; background: aliceblue;") .AttachTo("maindiv") new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, " "))).AttachTo("extradiv") \ No newline at end of file diff --git a/test/OsmConnection.spec.ts b/test/OsmConnection.spec.ts index ffcb4840ca..2253e56c38 100644 --- a/test/OsmConnection.spec.ts +++ b/test/OsmConnection.spec.ts @@ -15,7 +15,7 @@ export default class OsmConnectionSpec extends T { super("OsmConnectionSpec-test", [ ["login on dev", () => { - const osmConn = new OsmConnection(false, + const osmConn = new OsmConnection(false,false, new UIEventSource(undefined), "Unit test", true,