@panoramax/web-viewer 3.2.3-develop-7f50ae7f → 3.2.3-develop-17bc8da1

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.
@@ -4,7 +4,7 @@
4
4
  **Kind**: static class of <code>Panoramax.utils</code>
5
5
 
6
6
  * [.InitParameters](#Panoramax.utils.InitParameters)
7
- * [new InitParameters([componentAttrs], [urlParams])](#new_Panoramax.utils.InitParameters_new)
7
+ * [new InitParameters([componentAttrs], [urlParams], [browserStorage])](#new_Panoramax.utils.InitParameters_new)
8
8
  * [.getParentInit()](#Panoramax.utils.InitParameters+getParentInit)
9
9
  * [.getParentPostInit()](#Panoramax.utils.InitParameters+getParentPostInit)
10
10
  * [.getPSVInit()](#Panoramax.utils.InitParameters+getPSVInit)
@@ -14,7 +14,7 @@
14
14
 
15
15
  <a name="new_Panoramax.utils.InitParameters_new"></a>
16
16
 
17
- ### new InitParameters([componentAttrs], [urlParams])
17
+ ### new InitParameters([componentAttrs], [urlParams], [browserStorage])
18
18
  Merges all URL parameters and component attributes into a single set of coherent settings.
19
19
 
20
20
 
@@ -22,6 +22,7 @@ Merges all URL parameters and component attributes into a single set of coherent
22
22
  | --- | --- | --- |
23
23
  | [componentAttrs] | <code>object</code> | HTML attributes from parent component |
24
24
  | [urlParams] | <code>object</code> | Parameters extracted from URL |
25
+ | [browserStorage] | <code>object</code> | Parameters read from local/session storage |
25
26
 
26
27
  <a name="Panoramax.utils.InitParameters+getParentInit"></a>
27
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panoramax/web-viewer",
3
- "version": "3.2.3-develop-7f50ae7f",
3
+ "version": "3.2.3-develop-17bc8da1",
4
4
  "description": "Panoramax web viewer for geolocated pictures",
5
5
  "main": "build/index.js",
6
6
  "author": "Panoramax team",
@@ -3,7 +3,7 @@
3
3
  import "./Viewer.css";
4
4
  import { SYSTEM as PSSystem, DEFAULTS as PSDefaults } from "@photo-sphere-viewer/core";
5
5
  import URLHandler from "../../utils/URLHandler";
6
- import { linkMapAndPhoto } from "../../utils/map";
6
+ import { linkMapAndPhoto, saveMapParamsToLocalStorage, getMapParamsFromLocalStorage } from "../../utils/map";
7
7
  import Basic from "./Basic";
8
8
  import Photo, { PSV_DEFAULT_ZOOM, PSV_ANIM_DURATION } from "../ui/Photo";
9
9
  import MapMore from "../ui/MapMore";
@@ -139,7 +139,8 @@ export default class Viewer extends Basic {
139
139
  this.loader.setAttribute("value", 30);
140
140
  this._initParams = new InitParameters(
141
141
  InitParameters.GetComponentProperties(Viewer, this),
142
- Object.assign({}, this.urlHandler?.currentURLParams(), this.urlHandler?.currentURLParams(true))
142
+ Object.assign({}, this.urlHandler?.currentURLParams(), this.urlHandler?.currentURLParams(true)),
143
+ { map: getMapParamsFromLocalStorage() },
143
144
  );
144
145
 
145
146
  const myInitParams = this._initParams.getParentInit();
@@ -296,9 +297,14 @@ export default class Viewer extends Basic {
296
297
  * @memberof Panoramax.components.core.Viewer#
297
298
  */
298
299
  onceFirstPicLoaded() {
299
- return this.oncePSVReady().then(() => new Promise(resolve => {
300
- this.psv.addEventListener("picture-loaded", resolve, {once: true});
301
- }));
300
+ return this.oncePSVReady().then(() => {
301
+ if(this.psv.getPictureMetadata()) { return Promise.resolve(); }
302
+ else {
303
+ return new Promise(resolve => {
304
+ this.psv.addEventListener("picture-loaded", resolve, {once: true});
305
+ });
306
+ }
307
+ });
302
308
  }
303
309
 
304
310
  /** @private */
@@ -354,6 +360,7 @@ export default class Viewer extends Basic {
354
360
  async _initMap() {
355
361
  await new Promise(resolve => {
356
362
  this.map = new MapMore(this, this.mapContainer, this._initParams.getMapInit());
363
+ saveMapParamsToLocalStorage(this.map);
357
364
  this.map.once("users-changed", () => {
358
365
  this.loader.setAttribute("value", 75);
359
366
  resolve();
@@ -49,6 +49,8 @@ export default class PictureLegend extends LitElement {
49
49
  connectedCallback() {
50
50
  super.connectedCallback();
51
51
 
52
+ this._prevSearches = {};
53
+
52
54
  this._parent.onceReady().then(() => {
53
55
  this._onPicChange(this._parent.psv.getPictureMetadata());
54
56
  this._parent.psv.addEventListener("picture-loaded", () => {
@@ -63,15 +65,21 @@ export default class PictureLegend extends LitElement {
63
65
  this._caption = picMeta?.caption;
64
66
 
65
67
  if(picMeta) {
66
- this._addrTimer1 = setTimeout(() => {
67
- this._addrTimer2 = setTimeout(() => this._addr = "", 500);
68
-
69
- reverseGeocodingNominatim(picMeta.gps[1], picMeta.gps[0])
70
- .then(addr => {
71
- clearTimeout(this._addrTimer2);
72
- this._addr = addr;
73
- });
74
- }, 500);
68
+ const coordsHash = `${picMeta.gps[0]}/${picMeta.gps[1]}`;
69
+ if(this._prevSearches[coordsHash]) {
70
+ this._addr = this._prevSearches[coordsHash];
71
+ }
72
+ else {
73
+ this._addrTimer2 = setTimeout(() => this._addr = "", 250);
74
+ this._addrTimer1 = setTimeout(() => {
75
+ reverseGeocodingNominatim(picMeta.gps[1], picMeta.gps[0])
76
+ .then(addr => {
77
+ clearTimeout(this._addrTimer2);
78
+ this._addr = addr;
79
+ this._prevSearches[coordsHash] = addr;
80
+ });
81
+ }, 750);
82
+ }
75
83
  }
76
84
  else {
77
85
  this._addr = "";
@@ -246,7 +246,7 @@ export default class SearchBar extends LitElement {
246
246
  }
247
247
 
248
248
  /**
249
- * Limit search calls to every 250ms
249
+ * Limit search calls to every 500ms
250
250
  * @private
251
251
  */
252
252
  _throttledSearch() {
@@ -255,7 +255,7 @@ export default class SearchBar extends LitElement {
255
255
  delete this._throttler;
256
256
  }
257
257
 
258
- this._throttler = setTimeout(this._search.bind(this), 250);
258
+ this._throttler = setTimeout(this._search.bind(this), 500);
259
259
  }
260
260
 
261
261
  /**
@@ -277,6 +277,8 @@ export default class SearchBar extends LitElement {
277
277
  this._results = null;
278
278
 
279
279
  this.searcher(this.value).then(data => {
280
+ if(this._icon !== "loading") { return; }
281
+
280
282
  this._icon = "empty";
281
283
  if(!data || data.length == 0) {
282
284
  this._results = [];
@@ -16,9 +16,10 @@ const MAP_NONE = ["none", "null", "false", false];
16
16
  * @class Panoramax.utils.InitParameters
17
17
  * @param {object} [componentAttrs] HTML attributes from parent component
18
18
  * @param {object} [urlParams] Parameters extracted from URL
19
+ * @param {object} [browserStorage] Parameters read from local/session storage
19
20
  */
20
21
  export default class InitParameters { // eslint-disable-line import/no-unused-modules
21
- constructor(componentAttrs = {}, urlParams = {}) {
22
+ constructor(componentAttrs = {}, urlParams = {}, browserStorage = {}) {
22
23
  // Skip URL parameters if disabled by component
23
24
  if(componentAttrs["url-parameters"] === "false") { urlParams = {}; }
24
25
 
@@ -29,6 +30,8 @@ export default class InitParameters { // eslint-disable-line import/no-unused-mo
29
30
  // Sanitize Map parameters
30
31
  let componentMap = {};
31
32
  if(typeof componentAttrs?.map === "object") { componentMap = componentAttrs.map; }
33
+ let browserMap = {};
34
+ if(typeof browserStorage?.map === "object") { browserMap = browserStorage.map; }
32
35
 
33
36
  // Extract map position from URL
34
37
  let urlMap = urlParams.map && urlParams.map !== "none" ? getMapPositionFromString(urlParams.map) : null;
@@ -41,10 +44,10 @@ export default class InitParameters { // eslint-disable-line import/no-unused-mo
41
44
  let users = urlParams.users || componentAttrs.users;
42
45
  let psv_speed = urlParams.speed || componentPsv?.transitionDuration;
43
46
  let psv_nav = urlParams.nav || componentPsv?.picturesNavigation;
44
- let map_theme = urlParams.theme || componentMap.theme;
45
- let map_background = urlParams.background || componentMap.background;
46
- let map_center = urlMap?.center || componentMap.center;
47
- let map_zoom = urlMap?.zoom || componentMap.zoom;
47
+ let map_theme = urlParams.theme || browserMap?.theme || componentMap.theme;
48
+ let map_background = urlParams.background || browserMap?.background || componentMap.background;
49
+ let map_center = urlMap?.center || browserMap?.center || componentMap.center;
50
+ let map_zoom = urlMap?.zoom || browserMap?.zoom || componentMap.zoom;
48
51
  let map_pitch = urlMap?.pitch || componentMap.pitch;
49
52
  let map_bearing = urlMap?.bearing || componentMap.bearing;
50
53
 
@@ -18,6 +18,67 @@ function geocoderParamsToURLString(params) {
18
18
  return new URLSearchParams(p).toString();
19
19
  }
20
20
 
21
+ /**
22
+ * Transforms Nominatim search result into a nice-to-display address.
23
+ * @param {object} addr The Nominatim API "address" property
24
+ * @returns {string} The clean-up string for display
25
+ * @private
26
+ */
27
+ function nominatimAddressToPlaceName(addr) {
28
+ // API format @ https://nominatim.org/release-docs/develop/api/Output/#addressdetails
29
+ if(!addr || typeof addr != "object") { return ""; }
30
+
31
+ let res = "";
32
+
33
+ // House n°-like
34
+ if(addr.house_number) { res = addr.house_number; }
35
+ else if(addr.house_name) { res = addr.house_name; }
36
+ else {
37
+ const potentialNames = [
38
+ "emergency", "historic", "military", "natural", "landuse", "place", "railway", "man_made",
39
+ "aerialway", "boundary", "amenity", "aeroway", "club", "craft", "leisure", "office",
40
+ "mountain_pass", "shop", "tourism", "bridge", "tunnel", "waterway", "park"
41
+ ];
42
+ for(let pn of potentialNames) {
43
+ if(addr[pn]) {
44
+ res = addr[pn];
45
+ break;
46
+ }
47
+ }
48
+ }
49
+
50
+ // Street-like
51
+ let street;
52
+ if(addr.road && addr.road.length > 6) { street = addr.road; }
53
+ else {
54
+ const potentialNames = [
55
+ // Hamlet-like
56
+ "hamlet", "croft", "isolated_dwelling",
57
+ // Zone Indus-like
58
+ "farm", "farmyard", "industrial", "commercial", "retail", "city_block", "residential",
59
+ // Quarter-like
60
+ "neighbourhood", "allotments", "quarter"
61
+ ];
62
+ for(let pn of potentialNames) {
63
+ if(addr[pn]) {
64
+ street = addr[pn];
65
+ break;
66
+ }
67
+ }
68
+ }
69
+
70
+ if(street && res.length > 0) { res += (addr.house_number ? " " : ", ")+street; }
71
+ else if(street) { res = street; }
72
+
73
+ // City
74
+ if(addr.village || addr.town || addr.city || addr.municipality) {
75
+ if(res.length > 0) { res += ", "; }
76
+ res += addr.village || addr.town || addr.city || addr.municipality;
77
+ }
78
+
79
+ return res;
80
+ }
81
+
21
82
  /**
22
83
  * Nominatim (OSM) geocoder, ready to use for our Map
23
84
  * @private
@@ -37,13 +98,14 @@ export function forwardGeocodingNominatim(config) {
37
98
  const finalRes = { features: [] };
38
99
  const listedNames = [];
39
100
  res.features.forEach(f => {
40
- if(!listedNames.includes(f.properties.display_name)) {
101
+ const plname = nominatimAddressToPlaceName(f.properties.address) || f.properties.display_name;
102
+ if(!listedNames.includes(plname)) {
41
103
  finalRes.features.push({
42
104
  place_type: ["place"],
43
- place_name: f.properties.display_name,
105
+ place_name: plname,
44
106
  bounds: new maplibregl.LngLatBounds(f.bbox),
45
107
  });
46
- listedNames.push(f.properties.display_name);
108
+ listedNames.push(plname);
47
109
  }
48
110
  });
49
111
  return finalRes;
@@ -53,35 +115,7 @@ export function forwardGeocodingNominatim(config) {
53
115
  export function reverseGeocodingNominatim(lat, lon) {
54
116
  return fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&zoom=18&format=jsonv2`)
55
117
  .then(res => res.json())
56
- .then(res => {
57
- let addr = "";
58
-
59
- if(res?.address) {
60
- if(res.address.house_number) { addr = res.address.house_number; }
61
-
62
- // Street/place/hamlet
63
- let street;
64
- if(res.address.road && res.address.road.length > 6) {
65
- street = res.address.road;
66
- }
67
- else if(res.address.hamlet) {
68
- street = res.address.hamlet;
69
- }
70
- else if(res.address.isolated_dwelling) {
71
- street = res.address.isolated_dwelling;
72
- }
73
- if(street && addr.length > 0) { addr += " "+street; }
74
- else if(street) { addr = street; }
75
-
76
- // City
77
- if(res.address.village || res.address.town || res.address.city) {
78
- if(addr.length > 0) { addr += ", "; }
79
- addr += res.address.village || res.address.town || res.address.city;
80
- }
81
- }
82
-
83
- return addr;
84
- });
118
+ .then(res => nominatimAddressToPlaceName(res?.address));
85
119
  }
86
120
 
87
121
  /**
package/src/utils/map.js CHANGED
@@ -602,4 +602,44 @@ export function initMapKeyboardHandler(parent) {
602
602
  }
603
603
  };
604
604
  }.bind(parent.map.keyboard);
605
- }
605
+ }
606
+
607
+ const MAP_PARAMS_STORAGE = "pnx-map-parameters";
608
+
609
+ /**
610
+ * Reads map parameters from localStorage
611
+ * @private
612
+ */
613
+ export function getMapParamsFromLocalStorage() {
614
+ const params = localStorage.getItem(MAP_PARAMS_STORAGE);
615
+ if(!params) { return {}; }
616
+ try {
617
+ return JSON.parse(params);
618
+ }
619
+ catch(e) {
620
+ console.warn("Can't read map parameters stored in localStorage", e);
621
+ return {};
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Save map parameters into localStorage.
627
+ * @private
628
+ */
629
+ export function saveMapParamsToLocalStorage(map) {
630
+ // Save map state in localStorage
631
+ const save = () => localStorage.setItem(MAP_PARAMS_STORAGE, JSON.stringify({
632
+ center: Object.fromEntries(Object.entries(map.getCenter()).map(([k,v]) => ([k,v.toFixed(7)]))),
633
+ zoom: map.getZoom().toFixed(1),
634
+ background: map.getBackground(),
635
+ theme: map._mapFilters.theme
636
+ }));
637
+
638
+ // Add events to know when to rewrite info
639
+ map.on("background-changed", save);
640
+ map.on("filters-changed", save);
641
+ map.on("moveend", save);
642
+ map.on("zoomend", save);
643
+ map.on("dragend", save);
644
+ map.on("boxzoomend", save);
645
+ }
@@ -4,8 +4,7 @@ import {
4
4
  } from "../../src/utils/InitParameters";
5
5
 
6
6
  describe("InitParameters", () => {
7
- let componentAttrs;
8
- let urlParams;
7
+ let componentAttrs, urlParams, browserStorage;
9
8
 
10
9
  beforeEach(() => {
11
10
  console.warn = jest.fn();
@@ -41,12 +40,16 @@ describe("InitParameters", () => {
41
40
  camera: "cam1",
42
41
  pic_score: "high",
43
42
  };
43
+
44
+ browserStorage = {
45
+ map: { theme: "qualityscore", background: "aerial", center: [-10, -20], zoom: 19 },
46
+ };
44
47
  });
45
48
 
46
49
  afterEach(() => jest.clearAllMocks());
47
50
 
48
51
  it("should initialize with componentAttrs and urlParams", () => {
49
- const initParams = new InitParameters(componentAttrs, urlParams);
52
+ const initParams = new InitParameters(componentAttrs, urlParams, browserStorage);
50
53
 
51
54
  expect(initParams._parentInit).toEqual({
52
55
  map: true,
@@ -111,6 +114,20 @@ describe("InitParameters", () => {
111
114
  });
112
115
  });
113
116
 
117
+ it("uses browserStorage parameters if no URL parameter is available", () => {
118
+ componentAttrs.map.raster = {};
119
+ const initParams = new InitParameters(componentAttrs, undefined, browserStorage);
120
+ expect(initParams._mapAny).toEqual({
121
+ theme: "qualityscore",
122
+ background: "aerial",
123
+ center: [-10,-20],
124
+ zoom: 19,
125
+ pitch: undefined,
126
+ bearing: undefined,
127
+ users: "user1,user2",
128
+ });
129
+ });
130
+
114
131
  it("should sanitize objects correctly", () => {
115
132
  const initParams = new InitParameters(componentAttrs, urlParams);
116
133
  const obj = { a: 1, b: undefined, c: 3 };
@@ -38,7 +38,7 @@ Object {
38
38
  "lng": -1.7,
39
39
  },
40
40
  },
41
- "place_name": "Paris, Île-de-France, France métropolitaine, France",
41
+ "place_name": "Paris",
42
42
  "place_type": Array [
43
43
  "place",
44
44
  ],
@@ -0,0 +1,11 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`getMapParamsFromLocalStorage should return parsed params if they exist in localStorage 1`] = `
4
+ Array [
5
+ Array [
6
+ "pnx-map-parameters",
7
+ ],
8
+ ]
9
+ `;
10
+
11
+ exports[`saveMapParamsToLocalStorage should save map parameters to localStorage 1`] = `Array []`;
@@ -12,30 +12,30 @@ describe("getThumbGif", () => {
12
12
  });
13
13
 
14
14
  describe("isLabelLayer", () => {
15
- it("works with just text-field", () => {
16
- const layer = { type: "symbol", layout: { "text-field": "Label" } };
17
- expect(map.isLabelLayer(layer)).toBe(true);
18
- });
19
-
20
- it("works with text-field + minzoom < 15", () => {
21
- const layer = { type: "symbol", layout: { "text-field": "Label" }, minzoom: 10 };
22
- expect(map.isLabelLayer(layer)).toBe(true);
23
- });
24
-
25
- it("works with text-field + minzoom >= 15", () => {
26
- const layer = { type: "symbol", layout: { "text-field": "Label" }, minzoom: 15 };
27
- expect(map.isLabelLayer(layer)).toBe(false);
28
- });
29
-
30
- it("works with non-symbol layer", () => {
31
- const layer = { type: "fill", layout: { "text-field": "Label" } };
32
- expect(map.isLabelLayer(layer)).toBe(false);
33
- });
34
-
35
- it("works without text-field", () => {
36
- const layer = { type: "symbol", layout: {} };
37
- expect(map.isLabelLayer(layer)).toBe(false);
38
- });
15
+ it("works with just text-field", () => {
16
+ const layer = { type: "symbol", layout: { "text-field": "Label" } };
17
+ expect(map.isLabelLayer(layer)).toBe(true);
18
+ });
19
+
20
+ it("works with text-field + minzoom < 15", () => {
21
+ const layer = { type: "symbol", layout: { "text-field": "Label" }, minzoom: 10 };
22
+ expect(map.isLabelLayer(layer)).toBe(true);
23
+ });
24
+
25
+ it("works with text-field + minzoom >= 15", () => {
26
+ const layer = { type: "symbol", layout: { "text-field": "Label" }, minzoom: 15 };
27
+ expect(map.isLabelLayer(layer)).toBe(false);
28
+ });
29
+
30
+ it("works with non-symbol layer", () => {
31
+ const layer = { type: "fill", layout: { "text-field": "Label" } };
32
+ expect(map.isLabelLayer(layer)).toBe(false);
33
+ });
34
+
35
+ it("works without text-field", () => {
36
+ const layer = { type: "symbol", layout: {} };
37
+ expect(map.isLabelLayer(layer)).toBe(false);
38
+ });
39
39
  });
40
40
 
41
41
  describe("getUserLayerId", () => {
@@ -65,3 +65,62 @@ describe("switchCoefValue", () => {
65
65
  expect(res).toEqual({id: "bla", paint: { "circle-radius": ["bla", ["get", "coef_360"]]}, layout: {"circle-sort": "coef_360"}})
66
66
  });
67
67
  });
68
+
69
+
70
+ Object.defineProperty(window, "localStorage", { value: {
71
+ setItem: jest.fn(),
72
+ getItem: jest.fn(),
73
+ } });
74
+
75
+ describe("getMapParamsFromLocalStorage", () => {
76
+ it("should return an empty object if no params are stored", () => {
77
+ localStorage.getItem.mockReturnValue(null);
78
+ const result = map.getMapParamsFromLocalStorage();
79
+ expect(result).toEqual({});
80
+ });
81
+
82
+ it("should return parsed params if they exist in localStorage", () => {
83
+ const params = { center: { lat: "48.8566000", lng: "2.3522000" }, zoom: "10.0", background: "aerial", theme: "age" };
84
+ localStorage.getItem.mockReturnValue(JSON.stringify(params));
85
+ const result = map.getMapParamsFromLocalStorage();
86
+ expect(localStorage.getItem.mock.calls).toMatchSnapshot();
87
+ expect(result).toEqual(params);
88
+ });
89
+
90
+ it("should return an empty object if JSON parsing fails", () => {
91
+ console.warn = jest.fn();
92
+ localStorage.getItem.mockReturnValue("invalid-json");
93
+ const result = map.getMapParamsFromLocalStorage();
94
+ expect(result).toEqual({});
95
+ expect(console.warn.mock.calls[0][0]).toEqual("Can't read map parameters stored in localStorage");
96
+ });
97
+ });
98
+
99
+ describe("saveMapParamsToLocalStorage", () => {
100
+ const mockMap = {
101
+ getCenter: jest.fn().mockReturnValue({ lat: 48.8566, lng: 2.3522 }),
102
+ getZoom: jest.fn().mockReturnValue(10),
103
+ getBackground: jest.fn().mockReturnValue("streets"),
104
+ _mapFilters: { theme: "qualityscore" },
105
+ on: jest.fn(),
106
+ };
107
+
108
+ beforeEach(() => {
109
+ jest.clearAllMocks();
110
+ });
111
+
112
+ it("should add event listeners to the map", () => {
113
+ map.saveMapParamsToLocalStorage(mockMap);
114
+ expect(mockMap.on).toHaveBeenCalledWith("background-changed", expect.any(Function));
115
+ expect(mockMap.on).toHaveBeenCalledWith("filters-changed", expect.any(Function));
116
+ expect(mockMap.on).toHaveBeenCalledWith("moveend", expect.any(Function));
117
+ expect(mockMap.on).toHaveBeenCalledWith("zoomend", expect.any(Function));
118
+ expect(mockMap.on).toHaveBeenCalledWith("dragend", expect.any(Function));
119
+ expect(mockMap.on).toHaveBeenCalledWith("boxzoomend", expect.any(Function));
120
+ });
121
+
122
+ it("should save map parameters to localStorage", () => {
123
+ map.saveMapParamsToLocalStorage(mockMap);
124
+ expect(localStorage.setItem.mock.calls).toMatchSnapshot();
125
+ });
126
+ });