@panoramax/web-viewer 3.2.3-develop-04898f19 → 3.2.3-develop-dfee2adc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/viewer.html CHANGED
@@ -1,12 +1,12 @@
1
1
  <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><title>Panoramax Web Viewer</title><style>#viewer{position:relative;width:95%;margin:2.5%;height:400px}#viewer.fullpage{position:fixed;top:0;bottom:0;left:0;right:0;height:unset;width:unset;margin:0}</style><script defer="defer" src="index.js"></script><link href="index.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><pnx-viewer id="viewer" class="fullpage" map='{
2
- "raster": {
3
- "type": "raster",
4
- "tiles": [
2
+ raster: {
3
+ type: "raster",
4
+ tiles: [
5
5
  "https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&STYLE=normal&FORMAT=image/jpeg&TILEMATRIXSET=PM_0_21&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}"
6
6
  ],
7
- "minzoom": 0,
8
- "maxzoom": 21,
9
- "attribution": "&copy; IGN",
10
- "tileSize": 256
7
+ minzoom: 0,
8
+ maxzoom: 21,
9
+ attribution: "&copy; IGN",
10
+ tileSize: 256
11
11
  }
12
12
  }'/><script>var servers={meta:"https://api.panoramax.xyz/api",ign:"https://panoramax.ign.fr/api",osm:"https://panoramax.openstreetmap.fr/api",local:"http://localhost:5000/api",dev:"https://panoramax.codeureusesenliberte.fr/api"},urlParams=new URLSearchParams(window.location.search),server=servers[urlParams.get("server")||"meta"];window.onload=()=>{document.getElementById("viewer").setAttribute("endpoint",server)}</script></body></html>
package/docs/index.md CHANGED
@@ -83,6 +83,15 @@ Once ready, you can create for example a viewer. We use web components to do so,
83
83
 
84
84
  [Many options are available to configure it finely](./reference/components/core/Viewer.md).
85
85
 
86
+ You may also add some CSS to make sure your component has proper dimensions:
87
+
88
+ ```css
89
+ pnx-viewer {
90
+ width: 300px;
91
+ height: 250px;
92
+ }
93
+ ```
94
+
86
95
  Beyond classic viewer, other widgets are available and [can be tested online](https://viewer.geovisio.fr/).
87
96
 
88
97
  __Coverage map__
@@ -35,11 +35,14 @@
35
35
  ### new CoverageMap()
36
36
  Coverage Map is a basic map showing Panoramax coverage.
37
37
 
38
+ Make sure to set width/height through CSS for proper display.
39
+
38
40
  **Example**
39
41
  ```html
40
42
  <pnx-coverage-map
41
43
  endpoint="https://panoramax.openstreetmap.fr/"
42
44
  map='{"bounds": [[-73.9876, 40.7661], [-73.9397, 40.8002]]}'
45
+ style="width: 300px; height: 250px"
43
46
  />
44
47
  ```
45
48
  <a name="Panoramax.components.core.CoverageMap+properties"></a>
@@ -38,10 +38,13 @@
38
38
  Editor allows to focus on a single sequence, and preview what you edits would look like.
39
39
  It shows both picture and map.
40
40
 
41
+ Make sure to set width/height through CSS for proper display.
42
+
41
43
  **Example**
42
44
  ```html
43
45
  <pnx-editor
44
46
  endpoint="https://panoramax.openstreetmap.fr/"
47
+ style="width: 300px; height: 250px"
45
48
  />
46
49
  ```
47
50
  <a name="Panoramax.components.core.Editor+properties"></a>
@@ -56,16 +56,20 @@ This component has a [CorneredGrid](../layout/CorneredGrid.md/#Panoramax.compone
56
56
 
57
57
  If you need a viewer with map, checkout [Viewer component](Viewer.md/#Panoramax.components.core.Viewer).
58
58
 
59
+ Make sure to set width/height through CSS for proper display.
60
+
59
61
  **Example**
60
62
  ```html
61
63
  <!-- Basic example -->
62
64
  <pnx-photo-viewer
63
65
  endpoint="https://panoramax.openstreetmap.fr/"
66
+ style="width: 300px; height: 250px"
64
67
  />
65
68
 
66
69
  <!-- With slotted widgets -->
67
70
  <pnx-photo-viewer
68
71
  endpoint="https://panoramax.openstreetmap.fr/"
72
+ style="width: 300px; height: 250px"
69
73
  >
70
74
  <p slot="top-right">My custom text</p>
71
75
  </pnx-photo-viewer>
@@ -73,6 +77,7 @@ If you need a viewer with map, checkout [Viewer component](Viewer.md/#Panoramax.
73
77
  <!-- With only your custom widgets -->
74
78
  <pnx-photo-viewer
75
79
  endpoint="https://panoramax.openstreetmap.fr/"
80
+ style="width: 300px; height: 250px"
76
81
  widgets="false"
77
82
  >
78
83
  <p slot="top-right">My custom text</p>
@@ -61,16 +61,20 @@ This component has a [CorneredGrid](../layout/CorneredGrid.md/#Panoramax.compone
61
61
 
62
62
  If you need a viewer without map, checkout [Photo Viewer component](PhotoViewer.md/#Panoramax.components.core.PhotoViewer).
63
63
 
64
+ Make sure to set width/height through CSS for proper display.
65
+
64
66
  **Example**
65
67
  ```html
66
68
  <!-- Basic example -->
67
69
  <pnx-viewer
68
70
  endpoint="https://panoramax.openstreetmap.fr/"
71
+ style="width: 300px; height: 250px"
69
72
  />
70
73
 
71
74
  <!-- With slotted widgets -->
72
75
  <pnx-viewer
73
76
  endpoint="https://panoramax.openstreetmap.fr/"
77
+ style="width: 300px; height: 250px"
74
78
  >
75
79
  <p slot="top-right">My custom text</p>
76
80
  </pnx-viewer>
@@ -78,6 +82,7 @@ If you need a viewer without map, checkout [Photo Viewer component](PhotoViewer.
78
82
  <!-- With only your custom widgets -->
79
83
  <pnx-viewer
80
84
  endpoint="https://panoramax.openstreetmap.fr/"
85
+ style="width: 300px; height: 250px"
81
86
  widgets="false"
82
87
  >
83
88
  <p slot="top-right">My custom text</p>
@@ -86,6 +91,7 @@ If you need a viewer without map, checkout [Photo Viewer component](PhotoViewer.
86
91
  <!-- With map options -->
87
92
  <pnx-viewer
88
93
  endpoint="https://panoramax.openstreetmap.fr/"
94
+ style="width: 300px; height: 250px"
89
95
  map="{'maxZoom': 15, 'background': 'aerial', 'raster': '...'}"
90
96
  />
91
97
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panoramax/web-viewer",
3
- "version": "3.2.3-develop-04898f19",
3
+ "version": "3.2.3-develop-dfee2adc",
4
4
  "description": "Panoramax web viewer for geolocated pictures",
5
5
  "main": "build/index.js",
6
6
  "author": "Panoramax team",
@@ -98,6 +98,7 @@
98
98
  "@photo-sphere-viewer/gallery-plugin": "5.12.1",
99
99
  "@photo-sphere-viewer/markers-plugin": "5.12.1",
100
100
  "@photo-sphere-viewer/virtual-tour-plugin": "5.12.1",
101
+ "json5": "^2.2.3",
101
102
  "lit": "^3.2.1",
102
103
  "maplibre-gl": "^5.2.0",
103
104
  "pmtiles": "^4.3.0",
@@ -33,14 +33,14 @@
33
33
  picture="329af5c6-4761-4a6d-9c1e-674fd6daa8b6"
34
34
  background="aerial"
35
35
  raster='{
36
- "type": "raster",
37
- "tiles": [
36
+ type: "raster",
37
+ tiles: [
38
38
  "https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&STYLE=normal&FORMAT=image/jpeg&TILEMATRIXSET=PM_0_21&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}"
39
39
  ],
40
- "minzoom": 0,
41
- "maxzoom": 21,
42
- "attribution": "&copy; IGN",
43
- "tileSize": 256
40
+ minzoom: 0,
41
+ maxzoom: 21,
42
+ attribution: "&copy; IGN",
43
+ tileSize: 256
44
44
  }'
45
45
  ></pnx-editor>
46
46
 
@@ -32,15 +32,15 @@
32
32
  id="viewer"
33
33
  class="fullpage"
34
34
  map='{
35
- "raster": {
36
- "type": "raster",
37
- "tiles": [
35
+ raster: {
36
+ type: "raster",
37
+ tiles: [
38
38
  "https://data.geopf.fr/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&STYLE=normal&FORMAT=image/jpeg&TILEMATRIXSET=PM_0_21&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}"
39
39
  ],
40
- "minzoom": 0,
41
- "maxzoom": 21,
42
- "attribution": "&copy; IGN",
43
- "tileSize": 256
40
+ minzoom: 0,
41
+ maxzoom: 21,
42
+ attribution: "&copy; IGN",
43
+ tileSize: 256
44
44
  }
45
45
  }'
46
46
  />
@@ -4,6 +4,7 @@ import { getTranslations } from "../../utils/i18n";
4
4
  import { DEFAULT_TILES } from "../../utils/map";
5
5
  import { createWebComp } from "../../utils/widgets";
6
6
  import { isInIframe, isInternetFast } from "../../utils/utils";
7
+ import JSON5 from "json5";
7
8
  import PACKAGE_JSON from "../../../package.json";
8
9
  import "@fontsource/atkinson-hyperlegible-next";
9
10
  import "./Basic.css";
@@ -44,7 +45,7 @@ export default class Basic extends LitElement {
44
45
  static properties = {
45
46
  picture: {type: String, reflect: true},
46
47
  sequence: {type: String, reflect: true},
47
- fetchOptions: {type: Object, attribute: false},
48
+ fetchOptions: {converter: Basic.GetJSONConverter()},
48
49
  users: {type: Array, reflect: true},
49
50
  mapstyle: {type: String},
50
51
  lang: {type: String},
@@ -58,7 +59,7 @@ export default class Basic extends LitElement {
58
59
  this.users = ["geovisio"];
59
60
  this.mapstyle = this.getAttribute("mapstyle") || DEFAULT_TILES;
60
61
  this.lang = this.getAttribute("lang") || null;
61
- this.endpoint = this.getAttribute("endpoint") || null; // No default
62
+ this.endpoint = null; // No default
62
63
  this.picture = null;
63
64
  this.sequence = null;
64
65
 
@@ -82,7 +83,6 @@ export default class Basic extends LitElement {
82
83
 
83
84
  connectedCallback() {
84
85
  super.connectedCallback();
85
- if(this.endpoint) { this._setupAPI(); }
86
86
  }
87
87
 
88
88
  /**
@@ -98,6 +98,9 @@ export default class Basic extends LitElement {
98
98
  return;
99
99
  }
100
100
 
101
+ this._loadsAPI = this.endpoint;
102
+ let myLoadAPI = this.endpoint;
103
+
101
104
  // Check if mapstyle is not a unparsed JSON
102
105
  try {
103
106
  this.mapstyle = JSON.parse(this.mapstyle);
@@ -112,6 +115,8 @@ export default class Basic extends LitElement {
112
115
  });
113
116
  this.api.onceReady()
114
117
  .then(() => {
118
+ if(myLoadAPI != this._loadsAPI || !this.api) { return; }
119
+
115
120
  let unavailable = this.api.getUnavailableFeatures();
116
121
  let available = this.api.getAvailableFeatures();
117
122
  available = unavailable.length === 0 ? "✅ All features available" : "✅ Available features: "+available.join(", ");
@@ -122,9 +127,11 @@ export default class Basic extends LitElement {
122
127
  ${unavailable}
123
128
  `.trim());
124
129
  })
125
- .catch(e => this.loader.dismiss(e, this._t.pnx.error_api));
130
+ .catch(e => this.loader.dismiss(e, this._t.pnx.error_api))
131
+ .finally(() => delete this._loadsAPI);
126
132
  }
127
133
  catch(e) {
134
+ delete this._loadsAPI;
128
135
  if(this.loader?.dismiss) {
129
136
  this.loader.dismiss(e, this._t.pnx.error_api);
130
137
  }
@@ -173,7 +180,17 @@ export default class Basic extends LitElement {
173
180
  super.attributeChangedCallback(name, _old, value);
174
181
 
175
182
  if(name === "endpoint") {
176
- if(!this.api || this.api._endpoint !== value) { this._setupAPI(); }
183
+ if(
184
+ !(this._loadsAPI && value && this._loadsAPI === value)
185
+ && !(this.api && this.api._endpoint === value)
186
+ && value
187
+ ) {
188
+ if(this._loadsAPI || this.api) {
189
+ delete this.api;
190
+ delete this._loadsAPI;
191
+ }
192
+ this._setupAPI();
193
+ }
177
194
  }
178
195
  if(["picture", "sequence"].includes(name)) {
179
196
  let seqId, picId, prevSeqId, prevPicId;
@@ -319,4 +336,12 @@ export default class Basic extends LitElement {
319
336
  super.addEventListener(type, listener, options);
320
337
  }
321
338
  }
339
+
340
+ /** @private */
341
+ static GetJSONConverter() {
342
+ return {
343
+ fromAttribute: (value) => JSON5.parse(value),
344
+ toAttribute: (value) => JSON.stringify(value)
345
+ };
346
+ }
322
347
  }
@@ -8,6 +8,8 @@ import { default as InitParameters, alterMapState } from "../../utils/InitParame
8
8
 
9
9
  /**
10
10
  * Coverage Map is a basic map showing Panoramax coverage.
11
+ *
12
+ * Make sure to set width/height through CSS for proper display.
11
13
  * @class Panoramax.components.core.CoverageMap
12
14
  * @element pnx-coverage-map
13
15
  * @extends Panoramax.components.core.Basic
@@ -22,6 +24,7 @@ import { default as InitParameters, alterMapState } from "../../utils/InitParame
22
24
  * <pnx-coverage-map
23
25
  * endpoint="https://panoramax.openstreetmap.fr/"
24
26
  * map='{"bounds": [[-73.9876, 40.7661], [-73.9397, 40.8002]]}'
27
+ * style="width: 300px; height: 250px"
25
28
  * />
26
29
  * ```
27
30
  */
@@ -41,7 +44,7 @@ export default class CoverageMap extends Basic {
41
44
  * @property {object} [map] [Any map option available in Map class](#Panoramax.components.ui.Map).<br />Example: `map='{"bounds": [[-73.9876, 40.7661], [-73.9397, 40.8002]]}'`
42
45
  */
43
46
  static properties = {
44
- map: {type: Object},
47
+ map: {converter: Basic.GetJSONConverter()},
45
48
  ...Basic.properties
46
49
  };
47
50
 
@@ -54,9 +57,6 @@ export default class CoverageMap extends Basic {
54
57
  this._initParams = new InitParameters(InitParameters.GetComponentProperties(CoverageMap, this));
55
58
  this._initMap();
56
59
  });
57
-
58
- // Events handlers
59
- this.addEventListener("select", this._onSelect.bind(this));
60
60
  }
61
61
 
62
62
  getClassName() {
@@ -103,13 +103,16 @@ export default class CoverageMap extends Basic {
103
103
  this.map = new MyMap(this, this._mapContainer, Object.assign({}, this._initParams.getMapInit(), { hash: true }));
104
104
  this.map.addControl(new NavigationControl({ showCompass: false }));
105
105
  this.loader.setAttribute("value", 70);
106
+
107
+ this.addEventListener("select", this._onSelect.bind(this));
108
+ this.map.on("picture-click", e => this.select(e.seqId, e.picId));
109
+ this.map.on("sequence-click", e => this.select(e.seqId));
110
+
106
111
  this.map.waitForEnoughMapLoaded().then(() => {
107
112
  alterMapState(this.map, this._initParams.getMapPostInit());
108
113
  this.map.reloadLayersStyles();
109
114
  this.loader.dismiss();
110
115
  });
111
- this.map.on("picture-click", e => this.select(e.seqId, e.picId));
112
- this.map.on("sequence-click", e => this.select(e.seqId));
113
116
  }
114
117
 
115
118
  /**
@@ -17,6 +17,8 @@ const LAYER_HEADING_ID = "sequence-headings";
17
17
  /**
18
18
  * Editor allows to focus on a single sequence, and preview what you edits would look like.
19
19
  * It shows both picture and map.
20
+ *
21
+ * Make sure to set width/height through CSS for proper display.
20
22
  * @class Panoramax.components.core.Editor
21
23
  * @element pnx-editor
22
24
  * @extends Panoramax.components.core.Basic
@@ -31,6 +33,7 @@ const LAYER_HEADING_ID = "sequence-headings";
31
33
  * ```html
32
34
  * <pnx-editor
33
35
  * endpoint="https://panoramax.openstreetmap.fr/"
36
+ * style="width: 300px; height: 250px"
34
37
  * />
35
38
  * ```
36
39
  */
@@ -51,7 +54,7 @@ export default class Editor extends Basic {
51
54
  * @property {string} [background=streets] Choose default map background to display (streets or aerial, if raster aerial background available). Defaults to street.
52
55
  */
53
56
  static properties = {
54
- raster: {type: Object},
57
+ raster: {converter: Basic.GetJSONConverter()},
55
58
  background: {type: String},
56
59
  ...Basic.properties
57
60
  };
@@ -20,6 +20,8 @@ const PSV_MOVE_DELTA = Math.PI / 6;
20
20
  * This component has a [CorneredGrid](#Panoramax.components.layout.CorneredGrid) layout, you can use directly any slot element to pass custom widgets.
21
21
  *
22
22
  * If you need a viewer with map, checkout [Viewer component](#Panoramax.components.core.Viewer).
23
+ *
24
+ * Make sure to set width/height through CSS for proper display.
23
25
  * @class Panoramax.components.core.PhotoViewer
24
26
  * @element pnx-photo-viewer
25
27
  * @extends Panoramax.components.core.Basic
@@ -43,11 +45,13 @@ const PSV_MOVE_DELTA = Math.PI / 6;
43
45
  * <!-- Basic example -->
44
46
  * <pnx-photo-viewer
45
47
  * endpoint="https://panoramax.openstreetmap.fr/"
48
+ * style="width: 300px; height: 250px"
46
49
  * />
47
50
  *
48
51
  * <!-- With slotted widgets -->
49
52
  * <pnx-photo-viewer
50
53
  * endpoint="https://panoramax.openstreetmap.fr/"
54
+ * style="width: 300px; height: 250px"
51
55
  * >
52
56
  * <p slot="top-right">My custom text</p>
53
57
  * </pnx-photo-viewer>
@@ -55,6 +59,7 @@ const PSV_MOVE_DELTA = Math.PI / 6;
55
59
  * <!-- With only your custom widgets -->
56
60
  * <pnx-photo-viewer
57
61
  * endpoint="https://panoramax.openstreetmap.fr/"
62
+ * style="width: 300px; height: 250px"
58
63
  * widgets="false"
59
64
  * >
60
65
  * <p slot="top-right">My custom text</p>
@@ -77,7 +82,7 @@ export default class PhotoViewer extends Basic {
77
82
  * @property {string} [url-parameters=true] Should the component add and update URL query parameters to save viewer state ?
78
83
  */
79
84
  static properties = {
80
- psv: {type: Object},
85
+ psv: {converter: Basic.GetJSONConverter()},
81
86
  widgets: {type: String},
82
87
  "url-parameters": {type: String},
83
88
  ...Basic.properties
@@ -199,7 +204,7 @@ export default class PhotoViewer extends Basic {
199
204
  let waiter;
200
205
  return new Promise(resolve => {
201
206
  waiter = setInterval(() => {
202
- if(typeof this.psv === "object") {
207
+ if(this.psv && typeof this.psv === "object") {
203
208
  if(this.psv.container) {
204
209
  clearInterval(waiter);
205
210
  resolve();
@@ -26,6 +26,8 @@ const MAP_MOVE_DELTA = 100;
26
26
  * This component has a [CorneredGrid](#Panoramax.components.layout.CorneredGrid) layout, you can use directly any slot element to pass custom widgets.
27
27
  *
28
28
  * If you need a viewer without map, checkout [Photo Viewer component](#Panoramax.components.core.PhotoViewer).
29
+ *
30
+ * Make sure to set width/height through CSS for proper display.
29
31
  * @class Panoramax.components.core.Viewer
30
32
  * @element pnx-viewer
31
33
  * @extends Panoramax.components.core.PhotoViewer
@@ -52,11 +54,13 @@ const MAP_MOVE_DELTA = 100;
52
54
  * <!-- Basic example -->
53
55
  * <pnx-viewer
54
56
  * endpoint="https://panoramax.openstreetmap.fr/"
57
+ * style="width: 300px; height: 250px"
55
58
  * />
56
59
  *
57
60
  * <!-- With slotted widgets -->
58
61
  * <pnx-viewer
59
62
  * endpoint="https://panoramax.openstreetmap.fr/"
63
+ * style="width: 300px; height: 250px"
60
64
  * >
61
65
  * <p slot="top-right">My custom text</p>
62
66
  * </pnx-viewer>
@@ -64,6 +68,7 @@ const MAP_MOVE_DELTA = 100;
64
68
  * <!-- With only your custom widgets -->
65
69
  * <pnx-viewer
66
70
  * endpoint="https://panoramax.openstreetmap.fr/"
71
+ * style="width: 300px; height: 250px"
67
72
  * widgets="false"
68
73
  * >
69
74
  * <p slot="top-right">My custom text</p>
@@ -72,6 +77,7 @@ const MAP_MOVE_DELTA = 100;
72
77
  * <!-- With map options -->
73
78
  * <pnx-viewer
74
79
  * endpoint="https://panoramax.openstreetmap.fr/"
80
+ * style="width: 300px; height: 250px"
75
81
  * map="{'maxZoom': 15, 'background': 'aerial', 'raster': '...'}"
76
82
  * />
77
83
  * ```
@@ -97,7 +103,7 @@ export default class Viewer extends PhotoViewer {
97
103
  * @property {string} [lang] To override language used for labels. Defaults to using user's preferred languages.
98
104
  */
99
105
  static properties = {
100
- map: {type: Object},
106
+ map: {converter: PhotoViewer.GetJSONConverter()},
101
107
  focus: {type: String, reflect: true},
102
108
  geocoder: {type: String},
103
109
  ...PhotoViewer.properties
package/src/utils/map.js CHANGED
@@ -136,6 +136,9 @@ export function combineStyles(parent, options) {
136
136
  style.layers = style.layers.concat(options.supplementaryStyle.layers || []);
137
137
  }
138
138
 
139
+ // Fix for empty layers causing sorting issues
140
+ style.layers = style.layers.filter(l => l.id);
141
+
139
142
  // Aerial imagery background
140
143
  if(options.raster) {
141
144
  style.sources["pnx-aerial"] = options.raster;
@@ -35,7 +35,7 @@ describe("constructor", () => {
35
35
 
36
36
  it("should set up API when endpoint is provided", async () => {
37
37
  basic.endpoint = "https://api.example.com";
38
- basic.connectedCallback();
38
+ basic.attributeChangedCallback("endpoint", null, "https://api.example.com");
39
39
  expect(API).toHaveBeenCalledWith("https://api.example.com", expect.any(Object));
40
40
  });
41
41
  });
@@ -17,5 +17,8 @@ jest.mock("../../../src/components/core/Basic", () => (
17
17
  onceAPIReady() {
18
18
  return Promise.resolve();
19
19
  }
20
+ static GetJSONConverter() {
21
+ return {};
22
+ }
20
23
  }
21
24
  ));
@@ -22,6 +22,9 @@ jest.mock("../../../src/components/core/PhotoViewer", () => (
22
22
  onceAPIReady() {
23
23
  return Promise.resolve();
24
24
  }
25
+ static GetJSONConverter() {
26
+ return {};
27
+ }
25
28
  }
26
29
  ));
27
30