Merge branch 'develop' into feature/goejson-export

This commit is contained in:
Pieter Vander Vennet 2021-07-16 00:57:39 +02:00
commit 18e27c32be
13 changed files with 166 additions and 127 deletions

View file

@ -341,7 +341,7 @@ export class InitUiElements {
private static InitBaseMap() { private static InitBaseMap() {
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers; State.state.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(State.state.locationControl);
State.state.backgroundLayer = State.state.backgroundLayerId State.state.backgroundLayer = State.state.backgroundLayerId
.map((selectedId: string) => { .map((selectedId: string) => {

View file

@ -7,7 +7,6 @@ import {UIEventSource} from "../UIEventSource";
import {GeoOperations} from "../GeoOperations"; import {GeoOperations} from "../GeoOperations";
import {Utils} from "../../Utils"; import {Utils} from "../../Utils";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {isBoolean} from "util";
/** /**
* Calculates which layers are available at the current location * Calculates which layers are available at the current location
@ -31,42 +30,82 @@ export default class AvailableBaseLayers {
category: "osmbasedmap" category: "osmbasedmap"
} }
public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex());
public availableEditorLayers: UIEventSource<BaseLayer[]>;
constructor(location: UIEventSource<Loc>) { public static AvailableLayersAt(location: UIEventSource<Loc>): UIEventSource<BaseLayer[]> {
const self = this; const source = location.map(
this.availableEditorLayers = (currentLocation) => {
location.map(
(currentLocation) => {
if (currentLocation === undefined) { if (currentLocation === undefined) {
return AvailableBaseLayers.layerOverview; return AvailableBaseLayers.layerOverview;
} }
const currentLayers = self.availableEditorLayers?.data; const currentLayers = source?.data; // A bit unorthodox - I know
const newLayers = AvailableBaseLayers.AvailableLayersAt(currentLocation?.lon, currentLocation?.lat); const newLayers = AvailableBaseLayers.CalculateAvailableLayersAt(currentLocation?.lon, currentLocation?.lat);
if (currentLayers === undefined) { if (currentLayers === undefined) {
return newLayers;
}
if (newLayers.length !== currentLayers.length) {
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers; return newLayers;
} }
if (newLayers.length !== currentLayers.length) { }
return newLayers;
}
for (let i = 0; i < newLayers.length; i++) {
if (newLayers[i].name !== currentLayers[i].name) {
return newLayers;
}
}
return currentLayers;
});
return currentLayers;
});
return source;
} }
private static AvailableLayersAt(lon: number, lat: number): BaseLayer[] { public static SelectBestLayerAccordingTo(location: UIEventSource<Loc>, preferedCategory: UIEventSource<string | string[]>): UIEventSource<BaseLayer> {
return AvailableBaseLayers.AvailableLayersAt(location).map(available => {
// First float all 'best layers' to the top
available.sort((a, b) => {
if (a.isBest && b.isBest) {
return 0;
}
if (!a.isBest) {
return 1
}
return -1;
}
)
if (preferedCategory.data === undefined) {
return available[0]
}
let prefered: string []
if (typeof preferedCategory.data === "string") {
prefered = [preferedCategory.data]
} else {
prefered = preferedCategory.data;
}
prefered.reverse();
for (const category of prefered) {
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
available.sort((a, b) => {
if (a.category === category && b.category === category) {
return 0;
}
if (a.category !== category) {
return 1
}
return -1;
}
)
}
return available[0]
})
}
private static CalculateAvailableLayersAt(lon: number, lat: number): BaseLayer[] {
const availableLayers = [AvailableBaseLayers.osmCarto] const availableLayers = [AvailableBaseLayers.osmCarto]
const globalLayers = []; const globalLayers = [];
for (const layerOverviewItem of AvailableBaseLayers.layerOverview) { for (const layerOverviewItem of AvailableBaseLayers.layerOverview) {
@ -146,7 +185,7 @@ export default class AvailableBaseLayers {
layer: leafletLayer, layer: leafletLayer,
feature: layer, feature: layer,
isBest: props.best ?? false, isBest: props.best ?? false,
category: props.category category: props.category
}); });
} }
return layers; return layers;

View file

@ -52,7 +52,7 @@ export default class Minimap extends BaseUIElement {
return wrapper; return wrapper;
} }
private InitMap() { private InitMap() {
if (this._constructedHtmlElement === undefined) { if (this._constructedHtmlElement === undefined) {
// This element isn't initialized yet // This element isn't initialized yet

View file

@ -19,6 +19,7 @@ import {Translation} from "../i18n/Translation";
import LocationInput from "../Input/LocationInput"; import LocationInput from "../Input/LocationInput";
import {InputElement} from "../Input/InputElement"; import {InputElement} from "../Input/InputElement";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
/* /*
* The SimpleAddUI is a single panel, which can have multiple states: * The SimpleAddUI is a single panel, which can have multiple states:
@ -115,14 +116,21 @@ export default class SimpleAddUI extends Toggle {
let location = State.state.LastClickLocation; let location = State.state.LastClickLocation;
let preciseInput: InputElement<Loc> = undefined let preciseInput: InputElement<Loc> = undefined
if (preset.preciseInput !== undefined) { if (preset.preciseInput !== undefined) {
const locationSrc = new UIEventSource({
lat: location.data.lat,
lon: location.data.lon,
zoom: 19
});
let backgroundLayer = undefined;
if(preset.preciseInput.preferredBackground){
backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground))
}
preciseInput = new LocationInput({ preciseInput = new LocationInput({
preferCategory: preset.preciseInput.preferredBackground ?? State.state.backgroundLayer, mapBackground: backgroundLayer,
centerLocation: centerLocation:locationSrc
new UIEventSource({
lat: location.data.lat,
lon: location.data.lon,
zoom: 19
})
}) })
preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;")
} }

View file

@ -2,30 +2,27 @@ import {InputElement} from "./InputElement";
import Loc from "../../Models/Loc"; import Loc from "../../Models/Loc";
import {UIEventSource} from "../../Logic/UIEventSource"; import {UIEventSource} from "../../Logic/UIEventSource";
import Minimap from "../Base/Minimap"; import Minimap from "../Base/Minimap";
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
import BaseLayer from "../../Models/BaseLayer"; import BaseLayer from "../../Models/BaseLayer";
import Combine from "../Base/Combine"; import Combine from "../Base/Combine";
import Svg from "../../Svg"; import Svg from "../../Svg";
import State from "../../State";
export default class LocationInput extends InputElement<Loc> { export default class LocationInput extends InputElement<Loc> {
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false); IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private _centerLocation: UIEventSource<Loc>; private _centerLocation: UIEventSource<Loc>;
private readonly preferCategory; private readonly mapBackground : UIEventSource<BaseLayer>;
constructor(options?: { constructor(options?: {
mapBackground?: UIEventSource<BaseLayer>,
centerLocation?: UIEventSource<Loc>, centerLocation?: UIEventSource<Loc>,
preferCategory?: string | UIEventSource<string>,
}) { }) {
super(); super();
options = options ?? {} options = options ?? {}
options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) options.centerLocation = options.centerLocation ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1})
this._centerLocation = options.centerLocation; this._centerLocation = options.centerLocation;
if(typeof options.preferCategory === "string"){ this.mapBackground = options.mapBackground ?? State.state.backgroundLayer
options.preferCategory = new UIEventSource<string>(options.preferCategory);
}
this.preferCategory = options.preferCategory ?? new UIEventSource<string>(undefined)
this.SetClass("block h-full") this.SetClass("block h-full")
} }
@ -38,43 +35,10 @@ export default class LocationInput extends InputElement<Loc> {
} }
protected InnerConstructElement(): HTMLElement { protected InnerConstructElement(): HTMLElement {
const layer: UIEventSource<BaseLayer> = new AvailableBaseLayers(this._centerLocation).availableEditorLayers.map(allLayers => {
// First float all 'best layers' to the top
allLayers.sort((a, b) => {
if (a.isBest && b.isBest) {
return 0;
}
if (!a.isBest) {
return 1
}
return -1;
}
)
if (this.preferCategory) {
const self = this;
//Then sort all 'photo'-layers to the top. Stability of the sorting will force a 'best' photo layer on top
allLayers.sort((a, b) => {
const preferred = self.preferCategory.data
if (a.category === preferred && b.category === preferred) {
return 0;
}
if (a.category !== preferred) {
return 1
}
return -1;
}
)
}
return allLayers[0]
}, [this.preferCategory]
)
layer.addCallbackAndRunD(layer => console.log(layer))
const map = new Minimap( const map = new Minimap(
{ {
location: this._centerLocation, location: this._centerLocation,
background: layer background: this.mapBackground
} }
) )
map.leafletMap.addCallbackAndRunD(leaflet => { map.leafletMap.addCallbackAndRunD(leaflet => {
@ -84,7 +48,7 @@ export default class LocationInput extends InputElement<Loc> {
) )
}) })
layer.map(layer => { this.mapBackground.map(layer => {
const leaflet = map.leafletMap.data const leaflet = map.leafletMap.data
if (leaflet === undefined || layer === undefined) { if (leaflet === undefined || layer === undefined) {

View file

@ -25,9 +25,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="2.8284271" inkscape:zoom="5.6568542"
inkscape:cx="67.47399" inkscape:cx="27.044982"
inkscape:cy="29.788021" inkscape:cy="77.667126"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1" inkscape:current-layer="layer1"
showgrid="false" showgrid="false"
@ -68,40 +68,39 @@
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(0,-270.54165)"> transform="translate(0,-270.54165)">
<circle
style="fill:none;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529"
id="path815"
cx="13.16302"
cy="283.77081"
r="8.8715391" />
<path
style="fill:none;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529"
d="M 3.2841366,283.77082 H 1.0418969"
id="path817"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529"
d="M 25.405696,283.77082 H 23.286471"
id="path817-3"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529"
d="m 13.229167,295.9489 v -2.11763"
id="path817-3-6"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529"
d="m 13.229167,275.05759 v -3.44507"
id="path817-3-6-7"
inkscape:connector-curvature="0" />
<g <g
id="g824" id="g827">
transform="matrix(0.63953151,0,0,0.64343684,6.732623,276.71502)" <circle
style="fill:#ffcc33"> r="8.8715391"
cy="283.77081"
cx="13.16302"
id="path815"
style="fill:none;fill-opacity:1;stroke:#5555ec;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529" />
<path <path
id="path822" inkscape:connector-curvature="0"
d="M 16.07,8 H 15 V 5 C 15,5 15,0 10,0 5,0 5,5 5,5 V 8 H 3.93 A 1.93,1.93 0 0 0 2,9.93 v 8.15 A 1.93,1.93 0 0 0 3.93,20 H 16.07 A 1.93,1.93 0 0 0 18,18.07 V 9.93 A 1.93,1.93 0 0 0 16.07,8 Z M 10,16 a 2,2 0 1 1 2,-2 2,2 0 0 1 -2,2 z M 13,8 H 7 V 5.5 C 7,4 7,2 10,2 c 3,0 3,2 3,3.5 z" id="path817"
inkscape:connector-curvature="0" /> d="M 3.2841366,283.77082 H 1.0418969"
style="fill:none;stroke:#5555ec;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3"
d="M 25.405696,283.77082 H 23.286471"
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3-6"
d="m 13.229167,295.9489 v -2.11763"
style="fill:none;stroke:#5555ec;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
<path
inkscape:connector-curvature="0"
id="path817-3-6-7"
d="m 13.229167,275.05759 v -3.44507"
style="fill:none;stroke:#5555ec;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" />
</g> </g>
<path
style="fill:#5555ec;fill-opacity:0.98823529;stroke-width:0.6151033"
inkscape:connector-curvature="0"
d="m 16.850267,281.91543 h -0.65616 v -1.85094 c 0,0 0,-3.08489 -3.066169,-3.08489 -3.066169,0 -3.066169,3.08489 -3.066169,3.08489 v 1.85094 H 9.4056091 a 1.1835412,1.1907685 0 0 0 -1.1835412,1.19077 v 5.02838 a 1.1835412,1.1907685 0 0 0 1.1835412,1.1846 h 7.4446579 a 1.1835412,1.1907685 0 0 0 1.183541,-1.19078 v -5.0222 a 1.1835412,1.1907685 0 0 0 -1.183541,-1.19077 z m -3.722329,4.93583 a 1.2264675,1.233957 0 1 1 1.226468,-1.23395 1.2264675,1.233957 0 0 1 -1.226468,1.23395 z m 1.839702,-4.93583 h -3.679403 v -1.54245 c 0,-0.92546 0,-2.15942 1.839701,-2.15942 1.839702,0 1.839702,1.23396 1.839702,2.15942 z"
id="path822" />
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before After
Before After

View file

@ -162,6 +162,18 @@
"license": "CC0; trivial", "license": "CC0; trivial",
"sources": [] "sources": []
}, },
{
"authors": [],
"path": "crosshair-empty.svg",
"license": "CC0; trivial",
"sources": []
},
{
"authors": [],
"path": "crosshair-locked.svg",
"license": "CC0; trivial",
"sources": []
},
{ {
"authors": [ "authors": [
"Dave Gandy" "Dave Gandy"
@ -204,7 +216,7 @@
"license": "CC0", "license": "CC0",
"sources": [ "sources": [
"https://commons.wikimedia.org/wiki/File:Media-floppy.svg", "https://commons.wikimedia.org/wiki/File:Media-floppy.svg",
" http://tango.freedesktop.org/Tango_Desktop_Project" "http://tango.freedesktop.org/Tango_Desktop_Project"
] ]
}, },
{ {

View file

@ -374,7 +374,7 @@
"en": "Is there a website with more information about this artwork?", "en": "Is there a website with more information about this artwork?",
"nl": "Op welke website kan men meer informatie vinden over dit kunstwerk?", "nl": "Op welke website kan men meer informatie vinden over dit kunstwerk?",
"fr": "Sur quel site web pouvons-nous trouver plus d'informations sur cette œuvre d'art?", "fr": "Sur quel site web pouvons-nous trouver plus d'informations sur cette œuvre d'art?",
"de": "Auf welcher Website gibt es mehr Informationen über dieses Kunstwerk?", "de": "Gibt es eine Website mit weiteren Informationen über dieses Kunstwerk?",
"it": "Esiste un sito web con maggiori informazioni su questopera?", "it": "Esiste un sito web con maggiori informazioni su questopera?",
"ru": "Есть ли сайт с более подробной информацией об этой работе?", "ru": "Есть ли сайт с более подробной информацией об этой работе?",
"ja": "この作品についての詳しい情報はどのウェブサイトにありますか?", "ja": "この作品についての詳しい情報はどのウェブサイトにありますか?",

View file

@ -10,7 +10,8 @@
"ja", "ja",
"fr", "fr",
"zh_Hant", "zh_Hant",
"nb_NO" "nb_NO",
"de"
], ],
"title": { "title": {
"en": "Bicycle libraries", "en": "Bicycle libraries",
@ -20,7 +21,8 @@
"ja": "自転車ライブラリ", "ja": "自転車ライブラリ",
"fr": "Vélothèques", "fr": "Vélothèques",
"zh_Hant": "單車圖書館", "zh_Hant": "單車圖書館",
"nb_NO": "Sykkelbibliotek" "nb_NO": "Sykkelbibliotek",
"de": "Fahrradbibliothek"
}, },
"description": { "description": {
"nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.", "nl": "Een fietsbibliotheek is een plaats waar men een fiets kan lenen, vaak voor een klein bedrag per jaar. Een typisch voorbeeld zijn kinderfietsbibliotheken, waar men een fiets op maat van het kind kan lenen. Is het kind de fiets ontgroeid, dan kan het te kleine fietsje omgeruild worden voor een grotere.",

View file

@ -56,7 +56,7 @@
"tagRenderings": [ "tagRenderings": [
{ {
"render": { "render": {
"en": "The power output of this wind turbine is {canonical(generator:output:electricity)}." "en": "The power output of this wind turbine is {generator:output:electricity}."
}, },
"question": { "question": {
"en": "What is the power output of this wind turbine? (e.g. 2.3 MW)" "en": "What is the power output of this wind turbine? (e.g. 2.3 MW)"
@ -79,7 +79,7 @@
}, },
{ {
"render": { "render": {
"en": "The total height (including rotor radius) of this wind turbine is {canonical(height)}." "en": "The total height (including rotor radius) of this wind turbine is {height} metres."
}, },
"question": { "question": {
"en": "What is the total height of this wind turbine (including rotor radius), in metres?" "en": "What is the total height of this wind turbine (including rotor radius), in metres?"
@ -91,7 +91,7 @@
}, },
{ {
"render": { "render": {
"en": "The rotor diameter of this wind turbine is {canonical(rotor:diameter)}." "en": "The rotor diameter of this wind turbine is {rotor:diameter} metres."
}, },
"question": { "question": {
"en": "What is the rotor diameter of this wind turbine, in metres?" "en": "What is the rotor diameter of this wind turbine, in metres?"
@ -129,7 +129,7 @@
} }
], ],
"units": [ "units": [
{ {
"appliesToKey": [ "appliesToKey": [
"generator:output:electricity" "generator:output:electricity"
], ],
@ -183,7 +183,8 @@
}, },
{ {
"appliesToKey": [ "appliesToKey": [
"height","rotor:diameter" "height",
"rotor:diameter"
], ],
"applicableUnits": [ "applicableUnits": [
{ {

View file

@ -87,6 +87,9 @@
"shortDescription": "Eine Karte aller Sitzbänke", "shortDescription": "Eine Karte aller Sitzbänke",
"description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Einzeln stehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Account können Sie neue Bänke eintragen oder Detailinformationen existierender Bänke bearbeiten." "description": "Diese Karte zeigt alle Sitzbänke, die in OpenStreetMap eingetragen sind: Einzeln stehende Bänke und Bänke, die zu Haltestellen oder Unterständen gehören. Mit einem OpenStreetMap-Account können Sie neue Bänke eintragen oder Detailinformationen existierender Bänke bearbeiten."
}, },
"bicyclelib": {
"title": "Fahrradbibliothek"
},
"bookcases": { "bookcases": {
"title": "Öffentliche Bücherschränke Karte", "title": "Öffentliche Bücherschränke Karte",
"description": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen." "description": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen."
@ -327,8 +330,5 @@
"toilets": { "toilets": {
"title": "Offene Toilette Karte", "title": "Offene Toilette Karte",
"description": "Eine Karte der öffentlichen Toiletten" "description": "Eine Karte der öffentlichen Toiletten"
},
"bicyclelib": {
"title": "Fahrradbibliothek"
} }
} }

View file

@ -1143,6 +1143,13 @@
"human": " gigawatts" "human": " gigawatts"
} }
} }
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
} }
} }
}, },

View file

@ -919,6 +919,13 @@
"human": " gigawatt" "human": " gigawatt"
} }
} }
},
"1": {
"applicableUnits": {
"0": {
"human": " meter"
}
}
} }
} }
}, },