@panoramax/web-viewer 3.0.2-develop-a8ea8e60

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.
Files changed (125) hide show
  1. package/.dockerignore +6 -0
  2. package/.gitlab-ci.yml +71 -0
  3. package/CHANGELOG.md +428 -0
  4. package/CODE_OF_CONDUCT.md +134 -0
  5. package/Dockerfile +14 -0
  6. package/LICENSE +21 -0
  7. package/README.md +39 -0
  8. package/build/editor.html +1 -0
  9. package/build/index.css +36 -0
  10. package/build/index.css.map +1 -0
  11. package/build/index.html +1 -0
  12. package/build/index.js +25 -0
  13. package/build/index.js.map +1 -0
  14. package/build/map.html +1 -0
  15. package/build/viewer.html +1 -0
  16. package/config/env.js +104 -0
  17. package/config/getHttpsConfig.js +66 -0
  18. package/config/getPackageJson.js +25 -0
  19. package/config/jest/babelTransform.js +29 -0
  20. package/config/jest/cssTransform.js +14 -0
  21. package/config/jest/fileTransform.js +40 -0
  22. package/config/modules.js +134 -0
  23. package/config/paths.js +72 -0
  24. package/config/pnpTs.js +35 -0
  25. package/config/webpack/persistentCache/createEnvironmentHash.js +9 -0
  26. package/config/webpack.config.js +885 -0
  27. package/config/webpackDevServer.config.js +127 -0
  28. package/docs/01_Start.md +149 -0
  29. package/docs/02_Usage.md +828 -0
  30. package/docs/03_URL_settings.md +140 -0
  31. package/docs/04_Advanced_examples.md +214 -0
  32. package/docs/05_Compatibility.md +85 -0
  33. package/docs/09_Develop.md +62 -0
  34. package/docs/90_Releases.md +27 -0
  35. package/docs/images/class_diagram.drawio +129 -0
  36. package/docs/images/class_diagram.jpg +0 -0
  37. package/docs/images/screenshot.jpg +0 -0
  38. package/mkdocs.yml +45 -0
  39. package/package.json +254 -0
  40. package/public/editor.html +54 -0
  41. package/public/favicon.ico +0 -0
  42. package/public/index.html +59 -0
  43. package/public/map.html +53 -0
  44. package/public/viewer.html +67 -0
  45. package/scripts/build.js +217 -0
  46. package/scripts/start.js +176 -0
  47. package/scripts/test.js +52 -0
  48. package/src/Editor.css +37 -0
  49. package/src/Editor.js +359 -0
  50. package/src/StandaloneMap.js +114 -0
  51. package/src/Viewer.css +203 -0
  52. package/src/Viewer.js +1186 -0
  53. package/src/components/CoreView.css +64 -0
  54. package/src/components/CoreView.js +159 -0
  55. package/src/components/Loader.css +56 -0
  56. package/src/components/Loader.js +111 -0
  57. package/src/components/Map.css +65 -0
  58. package/src/components/Map.js +841 -0
  59. package/src/components/Photo.css +36 -0
  60. package/src/components/Photo.js +687 -0
  61. package/src/img/arrow_360.svg +14 -0
  62. package/src/img/arrow_flat.svg +11 -0
  63. package/src/img/arrow_triangle.svg +10 -0
  64. package/src/img/arrow_turn.svg +9 -0
  65. package/src/img/bg_aerial.jpg +0 -0
  66. package/src/img/bg_streets.jpg +0 -0
  67. package/src/img/loader_base.jpg +0 -0
  68. package/src/img/loader_hd.jpg +0 -0
  69. package/src/img/logo_dead.svg +91 -0
  70. package/src/img/marker.svg +17 -0
  71. package/src/img/marker_blue.svg +20 -0
  72. package/src/img/switch_big.svg +44 -0
  73. package/src/img/switch_mini.svg +48 -0
  74. package/src/index.js +10 -0
  75. package/src/translations/de.json +163 -0
  76. package/src/translations/en.json +164 -0
  77. package/src/translations/eo.json +6 -0
  78. package/src/translations/es.json +164 -0
  79. package/src/translations/fi.json +1 -0
  80. package/src/translations/fr.json +164 -0
  81. package/src/translations/hu.json +133 -0
  82. package/src/translations/nl.json +1 -0
  83. package/src/translations/zh_Hant.json +136 -0
  84. package/src/utils/API.js +709 -0
  85. package/src/utils/Exif.js +198 -0
  86. package/src/utils/I18n.js +75 -0
  87. package/src/utils/Map.js +382 -0
  88. package/src/utils/PhotoAdapter.js +45 -0
  89. package/src/utils/Utils.js +568 -0
  90. package/src/utils/Widgets.js +477 -0
  91. package/src/viewer/URLHash.js +334 -0
  92. package/src/viewer/Widgets.css +711 -0
  93. package/src/viewer/Widgets.js +1196 -0
  94. package/tests/Editor.test.js +125 -0
  95. package/tests/StandaloneMap.test.js +44 -0
  96. package/tests/Viewer.test.js +363 -0
  97. package/tests/__snapshots__/Editor.test.js.snap +300 -0
  98. package/tests/__snapshots__/StandaloneMap.test.js.snap +30 -0
  99. package/tests/__snapshots__/Viewer.test.js.snap +195 -0
  100. package/tests/components/CoreView.test.js +91 -0
  101. package/tests/components/Loader.test.js +38 -0
  102. package/tests/components/Map.test.js +230 -0
  103. package/tests/components/Photo.test.js +335 -0
  104. package/tests/components/__snapshots__/Loader.test.js.snap +15 -0
  105. package/tests/components/__snapshots__/Map.test.js.snap +767 -0
  106. package/tests/components/__snapshots__/Photo.test.js.snap +205 -0
  107. package/tests/data/Map_geocoder_ban.json +36 -0
  108. package/tests/data/Map_geocoder_nominatim.json +56 -0
  109. package/tests/data/Viewer_pictures_1.json +148 -0
  110. package/tests/setupTests.js +5 -0
  111. package/tests/utils/API.test.js +906 -0
  112. package/tests/utils/Exif.test.js +124 -0
  113. package/tests/utils/I18n.test.js +28 -0
  114. package/tests/utils/Map.test.js +105 -0
  115. package/tests/utils/Utils.test.js +300 -0
  116. package/tests/utils/Widgets.test.js +107 -0
  117. package/tests/utils/__snapshots__/API.test.js.snap +132 -0
  118. package/tests/utils/__snapshots__/Exif.test.js.snap +43 -0
  119. package/tests/utils/__snapshots__/Map.test.js.snap +48 -0
  120. package/tests/utils/__snapshots__/Utils.test.js.snap +41 -0
  121. package/tests/utils/__snapshots__/Widgets.test.js.snap +44 -0
  122. package/tests/viewer/URLHash.test.js +537 -0
  123. package/tests/viewer/Widgets.test.js +127 -0
  124. package/tests/viewer/__snapshots__/URLHash.test.js.snap +98 -0
  125. package/tests/viewer/__snapshots__/Widgets.test.js.snap +393 -0
@@ -0,0 +1,64 @@
1
+ /* Main container */
2
+ .gvs {
3
+ container-type: inline-size;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ .gvs * { box-sizing: border-box; }
8
+
9
+ /* Colors */
10
+ :root {
11
+ --white: #ffffff;
12
+ --black: #181818;
13
+ --black-pale: #1b1a17;
14
+ --red: #f70000;
15
+ --red-pale: #ff726f;
16
+ --grey: #f5f5f5;
17
+ --grey-pale: #cfd2cf;
18
+ --grey-semi-dark: #808080;
19
+ --grey-dark: #3e3e3e;
20
+ --blue: #2954e9;
21
+ --blue-dark: #0a1f69;
22
+ --blue-semi: #d7dffc;
23
+ --blue-pale: #f2f5ff;
24
+ --blue-geovisio: #34495e;
25
+ --beige: #f5f3ec;
26
+ --yellow: #fec868;
27
+ --orange: #ff6f00;
28
+ --orange-pale: #fffafa;
29
+ --green: #7ec636;
30
+ --green-pale: #f0ffee;
31
+ }
32
+
33
+ /* Titles */
34
+ .gvs h3 {
35
+ font-size: 1.1em;
36
+ line-height: 1.1em;
37
+ font-weight: 500;
38
+ margin: 10px 0 10px 0;
39
+ }
40
+
41
+ .gvs h4 {
42
+ font-size: 1.0em;
43
+ line-height: 1.0em;
44
+ font-weight: 500;
45
+ margin: 15px 0;
46
+ }
47
+
48
+ .gvs h4:first-of-type { margin-top: 0; }
49
+
50
+ .gvs h4 svg {
51
+ height: 18px;
52
+ vertical-align: sub;
53
+ margin-right: 2px;
54
+ }
55
+
56
+ /* Hidden elements on mobile */
57
+ @container (max-width: 576px) {
58
+ .gvs-mobile-hidden { display: none !important; }
59
+ }
60
+
61
+ /* Hidden elements on print */
62
+ @media print {
63
+ .gvs-print-hidden { display: none !important; }
64
+ }
@@ -0,0 +1,159 @@
1
+ import "./CoreView.css";
2
+ import API from "../utils/API";
3
+ import { getTranslations } from "../utils/I18n";
4
+ import { DEFAULT_TILES } from "../utils/Map";
5
+ import { isInIframe, isInternetFast } from "../utils/Utils";
6
+ import PACKAGE_JSON from "../../package.json";
7
+ import Loader from "./Loader";
8
+
9
+
10
+ /**
11
+ * Core view is an abstract class used for setting up any of the main Panoramax JS view components.
12
+ *
13
+ * It is used to prepare API, internationalization, options checks... for Viewer, StandaloneMap and Editor classes.
14
+ *
15
+ * @param {string|Element} container The DOM element to create viewer into
16
+ * @param {string} endpoint URL to API to use (must be a [STAC API](https://github.com/radiantearth/stac-api-spec/blob/main/overview.md))
17
+ * @param {object} [options] View options.
18
+ * @param {string} [options.selectedSequence] The ID of sequence to highlight on load (defaults to none)
19
+ * @param {string} [options.selectedPicture] The ID of picture to highlight on load (defaults to none)
20
+ * @param {object} [options.fetchOptions=null] Set custom options for fetch calls made against API ([same syntax as fetch options parameter](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters))
21
+ * @param {string|string[]} [options.users] List of user IDs to default use for display. Defaults to all users.
22
+ * @param {string|object} [options.style] The map's MapLibre style. This can be an a JSON object conforming to the schema described in the [MapLibre Style Specification](https://maplibre.org/maplibre-gl-js-docs/style-spec/), or a URL string pointing to one. Defaults to OSMFR vector tiles.
23
+ *
24
+ * @property {object} _t The translations labels
25
+ * @property {string} _selectedSeqId The selected sequence ID
26
+ * @property {string} _selectedPicId The selected picture ID
27
+ * @property {API} _api The API handler
28
+ * @property {Loader} _loader The initial loader message
29
+ * @property {object} _options The stored options
30
+ * @property {Element} container The DOM container
31
+ */
32
+ export default class CoreView extends EventTarget {
33
+ constructor(container, endpoint, options = {}) {
34
+ super();
35
+
36
+ this._options = options;
37
+ if(this._options == null) { this._options = {}; }
38
+ if(!this._options.users) { this._options.users = ["geovisio"]; }
39
+ if(typeof this._options.users === "string") { this._options.users = [this._options.users]; }
40
+ if(!this._options.style) { this._options.style = DEFAULT_TILES; }
41
+
42
+ if(!this._options.testing) {
43
+ // Display version in logs
44
+ console.info(`📷 Panoramax ${this.getClassName()} - Version ${PACKAGE_JSON.version} (${__COMMIT_HASH__})
45
+
46
+ 🆘 Issues can be reported at ${PACKAGE_JSON.repository.url}`);
47
+ }
48
+
49
+ // Translations
50
+ this._t = getTranslations(this._options.lang);
51
+
52
+ // Internet speed check
53
+ this._isInternetFast = null;
54
+ isInternetFast().then(isFast => this._isInternetFast = isFast);
55
+
56
+ // Selected IDs
57
+ this._selectedSeqId = this._options.selectedSequence || null;
58
+ this._selectedPicId = this._options.selectedPicture || null;
59
+
60
+ // Container init
61
+ this.container = typeof container === "string" ? document.getElementById(container) : container;
62
+ if(!(this.container instanceof Element)) { throw new Error("Container is not a valid HTML element, does it exist in your page ?"); }
63
+ this.container.classList.add("gvs", `gvs-${this.getClassName().toLocaleLowerCase()}`);
64
+ if(isInIframe()) { this.container.classList.add("gvs-iframed"); }
65
+
66
+ // Loader init
67
+ this.loaderContainer = document.createElement("div");
68
+ this.container.appendChild(this.loaderContainer);
69
+ this._loader = new Loader(this, this.loaderContainer);
70
+
71
+ // API init
72
+ endpoint = endpoint.replace("/api/search", "/api");
73
+ this._api = new API(endpoint, {
74
+ users: this._options.users,
75
+ fetch: this._options?.fetchOptions,
76
+ style: this._options.style,
77
+ });
78
+ this._api.onceReady()
79
+ .then(() => console.info(`🌐 Connected to API "${this._api._metadata.name}" (${this._api._endpoint})\nℹ️ API runs STAC ${this._api._metadata.stac_version} & GeoVisio ${this._api._metadata.geovisio_version}`))
80
+ .catch(e => this._loader.dismiss(e, this._t.gvs.error_api));
81
+ }
82
+
83
+ /**
84
+ * This allows to retrieve an always correct class name.
85
+ * This is crap, but avoids issues with Webpack & so on.
86
+ *
87
+ * Each inheriting class must override this method.
88
+ */
89
+ getClassName() {
90
+ return "CoreView";
91
+ }
92
+
93
+ /**
94
+ * Ends all form of life in this object.
95
+ *
96
+ * This is useful for Single Page Applications (SPA), to remove various event listeners.
97
+ */
98
+ destroy() {
99
+ delete this._options;
100
+ delete this._t;
101
+ delete this._api;
102
+ delete this._loader;
103
+ this.loaderContainer.remove();
104
+ delete this.loaderContainer;
105
+ }
106
+
107
+ /**
108
+ * Is the view running in a small container (small embed or smartphone)
109
+ * @returns {boolean} True if container is small
110
+ */
111
+ isWidthSmall() {
112
+ return this.container?.offsetWidth < 576;
113
+ }
114
+
115
+ /**
116
+ * Is the view running in a small-height container (small embed or smartphone)
117
+ * @returns {boolean} True if container height is small
118
+ */
119
+ isHeightSmall() {
120
+ return this.container?.offsetHeight < 400;
121
+ }
122
+
123
+ /**
124
+ * Change the currently picture and/or sequence.
125
+ * Calling the method without parameters unselects.
126
+ * @param {string} [seqId] The sequence UUID
127
+ * @param {string} [picId] The picture UUID
128
+ * @param {boolean} [force=false] Force select even if already selected
129
+ */
130
+ select(seqId = null, picId = null, force = false) {
131
+ const prevSeqId = this._selectedSeqId || null;
132
+ const prevPicId = this._selectedPicId || null;
133
+ if(!force && prevPicId == picId) { return; } // Avoid running if already selected
134
+
135
+ this._selectedSeqId = seqId;
136
+ this._selectedPicId = picId;
137
+
138
+ /**
139
+ * Event for sequence/picture selection
140
+ *
141
+ * @event select
142
+ * @memberof CoreView
143
+ * @type {object}
144
+ * @property {object} detail Event information
145
+ * @property {string} detail.seqId The selected sequence ID
146
+ * @property {string} detail.picId The selected picture ID (or null if not a precise picture clicked)
147
+ * @property {string} [detail.prevSeqId] The previously selected sequence ID (or null if none)
148
+ * @property {string} [detail.prevPicId] The previously selected picture ID (or null if none)
149
+ */
150
+ this.dispatchEvent(new CustomEvent("select", {
151
+ detail: {
152
+ seqId,
153
+ picId,
154
+ prevSeqId,
155
+ prevPicId,
156
+ }
157
+ }));
158
+ }
159
+ }
@@ -0,0 +1,56 @@
1
+ .gvs .gvs-loader {
2
+ position: relative;
3
+ width: 100%;
4
+ height: 100%;
5
+ z-index: 0;
6
+ }
7
+
8
+ .gvs .gvs-loader.gvs-loader-visible {
9
+ visibility: visible;
10
+ opacity: 1;
11
+ }
12
+
13
+ /* Loader overlay */
14
+ .gvs .gvs-loader {
15
+ visibility: hidden;
16
+ position: absolute;
17
+ top: 0;
18
+ right: 0;
19
+ left: 0;
20
+ bottom: 0;
21
+ opacity: 0;
22
+ display: flex;
23
+ flex-direction: column;
24
+ justify-content: center;
25
+ gap: 40px;
26
+ align-items: center;
27
+ background: #37474F;
28
+ z-index: 9000;
29
+ transition: all 0.5s;
30
+ font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
31
+ font-weight: 550;
32
+ color: white;
33
+ font-size: 1.4em;
34
+ text-align: center;
35
+ }
36
+
37
+ .gvs .gvs-loader a {
38
+ color: white !important;
39
+ }
40
+
41
+ /* Flashing text */
42
+ .gvs .gvs-loader > div > span {
43
+ animation: blinker 2s linear infinite;
44
+ }
45
+ @keyframes blinker { 50% { opacity: 0.3; } }
46
+
47
+ /* Rotating logo */
48
+ .gvs .gvs-loader .gvs-loader-img {
49
+ width: 150px;
50
+ animation: rotating 1.4s linear infinite;
51
+ }
52
+
53
+ @keyframes rotating {
54
+ from { transform: rotate(0deg); }
55
+ to { transform: rotate(360deg); }
56
+ }
@@ -0,0 +1,111 @@
1
+ import "./Loader.css";
2
+ import LogoDead from "../img/logo_dead.svg";
3
+ import LoaderImg from "../img/marker.svg";
4
+
5
+ /**
6
+ * Loader is a full-screen loading message.
7
+ * @private
8
+ *
9
+ * @param {CoreView} parent The parent view
10
+ * @param {Element} container The DOM element to create loader into
11
+ */
12
+ export default class Loader {
13
+ constructor(parent, container) {
14
+ this._parent = parent;
15
+ this.container = container;
16
+ this.container.classList.add("gvs-loader", "gvs-loader-visible");
17
+
18
+ // Logo
19
+ const logo = document.createElement("img");
20
+ logo.src = LoaderImg;
21
+ logo.title = this._parent._t.map.loading;
22
+ logo.classList.add("gvs-loader-img");
23
+ this.container.appendChild(logo);
24
+
25
+ // Label (1 serious, then fun ones)
26
+ const labelWrapper = document.createElement("div");
27
+ const label = document.createElement("span");
28
+ const nextLabelFun = () => (
29
+ this._parent._t.gvs.loading_labels_fun[
30
+ Math.floor(Math.random() * this._parent._t.gvs.loading_labels_fun.length)
31
+ ]
32
+ );
33
+ label.innerHTML = this._parent._t.gvs.loading_labels_serious[
34
+ Math.floor(Math.random() * this._parent._t.gvs.loading_labels_serious.length)
35
+ ];
36
+ const nextLabelFct = () => setTimeout(() => {
37
+ label.innerHTML = nextLabelFun();
38
+ this._loaderLabelChanger = nextLabelFct();
39
+ }, 500 + Math.random() * 1000);
40
+ this._loaderLabelChanger = nextLabelFct();
41
+ labelWrapper.appendChild(label);
42
+
43
+ this.container.appendChild(labelWrapper);
44
+ }
45
+
46
+ /**
47
+ * Is the loader visible ?
48
+ * @returns {boolean} True if visible
49
+ */
50
+ isVisible() {
51
+ return this.container.classList.contains("gvs-loader-visible");
52
+ }
53
+
54
+ /**
55
+ * Dismiss loader, or show error
56
+ * @param {object} [err] Optional error object to show in browser console
57
+ * @param {str} [errMeaningful] Optional error message to show to user
58
+ * @param {fct} [next] Optional function to run after loader dismiss
59
+ */
60
+ dismiss(err = null, errMeaningful = null, next = null) {
61
+ clearTimeout(this._loaderLabelChanger);
62
+
63
+ if(!err) {
64
+ this.container.classList.remove("gvs-loader-visible");
65
+ setTimeout(() => this.container.remove(), 2000);
66
+
67
+ /**
68
+ * Event for viewer being ready to use (API loaded)
69
+ *
70
+ * @event ready
71
+ * @memberof CoreView
72
+ */
73
+ const readyEvt = new CustomEvent("ready");
74
+ this._parent.dispatchEvent(readyEvt);
75
+
76
+ if(next) { next(); }
77
+ }
78
+ else {
79
+ if(err !== true) { console.error(err); }
80
+
81
+ // Change content
82
+ this.container.children[0].src = LogoDead;
83
+ this.container.children[0].style.width = "200px";
84
+ this.container.children[0].style.animation = "unset";
85
+ let errHtml = `<small>${next ? this._parent._t.gvs.error_click : this._parent._t.gvs.error_retry}</small>`;
86
+ if(errMeaningful) { errHtml = errMeaningful + "<br />" + errHtml; }
87
+ this.container.children[1].innerHTML = `${this._parent._t.gvs.error}<br />${errHtml}`;
88
+ if(next) {
89
+ this.container.addEventListener("click", next);
90
+ }
91
+ const errLabel = errMeaningful || "Panoramax JS had a blocking exception";
92
+
93
+ /**
94
+ * Event for viewer failing to initially load
95
+ *
96
+ * @event broken
97
+ * @memberof CoreView
98
+ * @type {object}
99
+ * @property {object} detail Event information
100
+ * @property {string} detail.error The user-friendly error message to display
101
+ */
102
+ const brokenEvt = new CustomEvent("broken", {
103
+ detail: { error: errLabel }
104
+ });
105
+ this._parent.dispatchEvent(brokenEvt);
106
+
107
+ // Throw error
108
+ throw new Error(errLabel);
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,65 @@
1
+ /* Force maplibre to fill all available space */
2
+ .gvs-map.maplibregl-map {
3
+ width: 100%;
4
+ height: 100%;
5
+ background-color: white;
6
+ }
7
+
8
+ /* Picture thumbnail on map */
9
+ .maplibregl-popup-content {
10
+ padding: 5px !important;
11
+ position: relative;
12
+ }
13
+
14
+ .gvs-map-thumb {
15
+ display: inline-block;
16
+ text-align: center;
17
+ vertical-align: middle;
18
+ max-width: 130px;
19
+ max-height: 130px;
20
+ }
21
+
22
+ @keyframes rotating {
23
+ from { transform: rotate(0deg); }
24
+ to { transform: rotate(360deg); }
25
+ }
26
+
27
+ .gvs-map-thumb-loader {
28
+ background-color: rgb(230,230,230);
29
+ border-radius: 65px;
30
+ max-height: 60px;
31
+ margin: 5px 35px;
32
+ animation: rotating 2s linear infinite;
33
+ }
34
+
35
+ .gvs-map-thumb-legend {
36
+ display: block;
37
+ position: absolute;
38
+ bottom: 5px;
39
+ right: 5px;
40
+ left: 5px;
41
+ background-color: rgba(0,0,0,0.8);
42
+ color: white;
43
+ text-align: center;
44
+ font-style: italic;
45
+ font-size: 0.8em;
46
+ margin: 0;
47
+ padding: 0;
48
+ }
49
+
50
+ /* Max size for geocoder search bar */
51
+ .maplibregl-ctrl-geocoder {
52
+ max-width: 60%;
53
+ }
54
+
55
+ .maplibregl-marker {
56
+ width: 60px;
57
+ }
58
+
59
+ /* Resize canvas for print */
60
+ @media print {
61
+ .gvs-map.maplibregl-map canvas {
62
+ width: 100% !important;
63
+ height: 100% !important;
64
+ }
65
+ }