@panoramax/web-viewer 3.2.3 → 4.0.0-develop-39167b4d

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 (255) hide show
  1. package/.gitlab-ci.yml +13 -6
  2. package/CHANGELOG.md +53 -1
  3. package/CODE_OF_CONDUCT.md +1 -1
  4. package/README.md +1 -1
  5. package/build/editor.html +10 -1
  6. package/build/index.css +12 -12
  7. package/build/index.css.map +1 -1
  8. package/build/index.html +1 -1
  9. package/build/index.js +2126 -14
  10. package/build/index.js.map +1 -1
  11. package/build/map.html +1 -1
  12. package/build/photo.html +1 -0
  13. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff +0 -0
  14. package/build/static/media/atkinson-hyperlegible-next-latin-400-normal..woff2 +0 -0
  15. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff +0 -0
  16. package/build/static/media/atkinson-hyperlegible-next-latin-ext-400-normal..woff2 +0 -0
  17. package/build/viewer.html +12 -1
  18. package/build/widgets.html +1 -0
  19. package/config/jest/mocks.js +201 -0
  20. package/config/paths.js +2 -0
  21. package/config/webpack.config.js +52 -0
  22. package/docs/03_URL_settings.md +14 -16
  23. package/docs/05_Compatibility.md +59 -76
  24. package/docs/09_Develop.md +46 -11
  25. package/docs/90_Releases.md +2 -2
  26. package/docs/images/class_diagram.drawio +60 -45
  27. package/docs/images/class_diagram.jpg +0 -0
  28. package/docs/images/screenshot.jpg +0 -0
  29. package/docs/index.md +135 -0
  30. package/docs/reference/components/core/Basic.md +196 -0
  31. package/docs/reference/components/core/CoverageMap.md +210 -0
  32. package/docs/reference/components/core/Editor.md +224 -0
  33. package/docs/reference/components/core/PhotoViewer.md +307 -0
  34. package/docs/reference/components/core/Viewer.md +350 -0
  35. package/docs/reference/components/layout/BottomDrawer.md +35 -0
  36. package/docs/reference/components/layout/CorneredGrid.md +29 -0
  37. package/docs/reference/components/layout/Mini.md +45 -0
  38. package/docs/reference/components/layout/Tabs.md +45 -0
  39. package/docs/reference/components/menus/MapBackground.md +32 -0
  40. package/docs/reference/components/menus/MapFilters.md +15 -0
  41. package/docs/reference/components/menus/MapLayers.md +15 -0
  42. package/docs/reference/components/menus/MapLegend.md +15 -0
  43. package/docs/reference/components/menus/PictureLegend.md +16 -0
  44. package/docs/reference/components/menus/PictureMetadata.md +15 -0
  45. package/docs/reference/components/menus/PlayerOptions.md +15 -0
  46. package/docs/reference/components/menus/QualityScoreDoc.md +15 -0
  47. package/docs/reference/components/menus/ReportForm.md +15 -0
  48. package/docs/reference/components/menus/ShareMenu.md +15 -0
  49. package/docs/reference/components/ui/Button.md +40 -0
  50. package/docs/reference/components/ui/ButtonGroup.md +36 -0
  51. package/docs/reference/components/ui/CopyButton.md +38 -0
  52. package/docs/reference/components/ui/Grade.md +32 -0
  53. package/docs/reference/components/ui/LinkButton.md +45 -0
  54. package/docs/reference/components/ui/ListGroup.md +22 -0
  55. package/docs/reference/components/ui/Loader.md +56 -0
  56. package/docs/reference/components/ui/Map.md +239 -0
  57. package/docs/reference/components/ui/MapMore.md +256 -0
  58. package/docs/reference/components/ui/Photo.md +385 -0
  59. package/docs/reference/components/ui/Popup.md +56 -0
  60. package/docs/reference/components/ui/ProgressBar.md +32 -0
  61. package/docs/reference/components/ui/QualityScore.md +45 -0
  62. package/docs/reference/components/ui/SearchBar.md +63 -0
  63. package/docs/reference/components/ui/TogglableGroup.md +39 -0
  64. package/docs/reference/components/ui/widgets/GeoSearch.md +32 -0
  65. package/docs/reference/components/ui/widgets/Legend.md +49 -0
  66. package/docs/reference/components/ui/widgets/MapFiltersButton.md +33 -0
  67. package/docs/reference/components/ui/widgets/MapLayersButton.md +15 -0
  68. package/docs/reference/components/ui/widgets/OSMEditors.md +15 -0
  69. package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
  70. package/docs/reference/components/ui/widgets/Player.md +33 -0
  71. package/docs/reference/components/ui/widgets/Zoom.md +15 -0
  72. package/docs/reference/utils/API.md +334 -0
  73. package/docs/reference/utils/InitParameters.md +68 -0
  74. package/docs/reference/utils/URLHandler.md +107 -0
  75. package/docs/reference.md +79 -0
  76. package/docs/shortcuts.md +11 -0
  77. package/docs/tutorials/aerial_imagery.md +19 -0
  78. package/docs/tutorials/authentication.md +10 -0
  79. package/docs/tutorials/custom_widgets.md +59 -0
  80. package/docs/tutorials/map_style.md +39 -0
  81. package/docs/tutorials/migrate_v4.md +153 -0
  82. package/docs/tutorials/synced_coverage.md +43 -0
  83. package/mkdocs.yml +66 -5
  84. package/package.json +22 -17
  85. package/public/editor.html +21 -29
  86. package/public/index.html +17 -12
  87. package/public/map.html +19 -18
  88. package/public/photo.html +55 -0
  89. package/public/viewer.html +22 -26
  90. package/public/widgets.html +306 -0
  91. package/scripts/doc.js +79 -0
  92. package/src/components/core/Basic.css +48 -0
  93. package/src/components/core/Basic.js +349 -0
  94. package/src/components/core/CoverageMap.css +9 -0
  95. package/src/components/core/CoverageMap.js +139 -0
  96. package/src/components/core/Editor.css +23 -0
  97. package/src/components/core/Editor.js +390 -0
  98. package/src/components/core/PhotoViewer.css +48 -0
  99. package/src/components/core/PhotoViewer.js +499 -0
  100. package/src/components/core/Viewer.css +98 -0
  101. package/src/components/core/Viewer.js +564 -0
  102. package/src/components/core/index.js +12 -0
  103. package/src/components/index.js +13 -0
  104. package/src/components/layout/BottomDrawer.js +257 -0
  105. package/src/components/layout/CorneredGrid.js +112 -0
  106. package/src/components/layout/Mini.js +117 -0
  107. package/src/components/layout/Tabs.js +133 -0
  108. package/src/components/layout/index.js +9 -0
  109. package/src/components/menus/MapBackground.js +106 -0
  110. package/src/components/menus/MapFilters.js +400 -0
  111. package/src/components/menus/MapLayers.js +143 -0
  112. package/src/components/menus/MapLegend.js +34 -0
  113. package/src/components/menus/PictureLegend.js +257 -0
  114. package/src/components/menus/PictureMetadata.js +317 -0
  115. package/src/components/menus/PlayerOptions.js +95 -0
  116. package/src/components/menus/QualityScoreDoc.js +36 -0
  117. package/src/components/menus/ReportForm.js +133 -0
  118. package/src/components/menus/Share.js +100 -0
  119. package/src/components/menus/index.js +15 -0
  120. package/src/components/styles.js +383 -0
  121. package/src/components/ui/Button.js +77 -0
  122. package/src/components/ui/ButtonGroup.css +57 -0
  123. package/src/components/ui/ButtonGroup.js +68 -0
  124. package/src/components/ui/CopyButton.js +106 -0
  125. package/src/components/ui/Grade.js +54 -0
  126. package/src/components/ui/LinkButton.js +67 -0
  127. package/src/components/ui/ListGroup.js +66 -0
  128. package/src/components/ui/Loader.js +203 -0
  129. package/src/components/{Map.css → ui/Map.css} +5 -17
  130. package/src/components/{Map.js → ui/Map.js} +148 -156
  131. package/src/components/ui/MapMore.js +324 -0
  132. package/src/components/{Photo.css → ui/Photo.css} +6 -6
  133. package/src/components/{Photo.js → ui/Photo.js} +313 -101
  134. package/src/components/ui/Popup.js +145 -0
  135. package/src/components/ui/ProgressBar.js +104 -0
  136. package/src/components/ui/QualityScore.js +147 -0
  137. package/src/components/ui/SearchBar.js +367 -0
  138. package/src/components/ui/TogglableGroup.js +157 -0
  139. package/src/components/ui/index.js +22 -0
  140. package/src/components/ui/widgets/GeoSearch.css +21 -0
  141. package/src/components/ui/widgets/GeoSearch.js +139 -0
  142. package/src/components/ui/widgets/Legend.js +113 -0
  143. package/src/components/ui/widgets/MapFiltersButton.js +104 -0
  144. package/src/components/ui/widgets/MapLayersButton.js +79 -0
  145. package/src/components/ui/widgets/OSMEditors.js +155 -0
  146. package/src/components/ui/widgets/PictureLegendActions.js +117 -0
  147. package/src/components/ui/widgets/Player.css +7 -0
  148. package/src/components/ui/widgets/Player.js +151 -0
  149. package/src/components/ui/widgets/Zoom.js +82 -0
  150. package/src/components/ui/widgets/index.js +13 -0
  151. package/src/img/loader_base.jpg +0 -0
  152. package/src/img/panoramax.svg +13 -0
  153. package/src/img/switch_big.svg +20 -10
  154. package/src/index.js +7 -9
  155. package/src/translations/br.json +1 -0
  156. package/src/translations/da.json +38 -15
  157. package/src/translations/de.json +5 -3
  158. package/src/translations/en.json +35 -15
  159. package/src/translations/eo.json +38 -15
  160. package/src/translations/es.json +1 -1
  161. package/src/translations/fr.json +36 -16
  162. package/src/translations/hu.json +1 -1
  163. package/src/translations/it.json +39 -16
  164. package/src/translations/ja.json +182 -1
  165. package/src/translations/nl.json +106 -6
  166. package/src/translations/pl.json +1 -1
  167. package/src/translations/sv.json +182 -0
  168. package/src/translations/zh_Hant.json +35 -14
  169. package/src/utils/API.js +109 -49
  170. package/src/utils/InitParameters.js +388 -0
  171. package/src/utils/PhotoAdapter.js +1 -0
  172. package/src/utils/URLHandler.js +362 -0
  173. package/src/utils/geocoder.js +152 -0
  174. package/src/utils/{I18n.js → i18n.js} +7 -3
  175. package/src/utils/index.js +11 -0
  176. package/src/utils/{Map.js → map.js} +256 -77
  177. package/src/utils/picture.js +442 -0
  178. package/src/utils/utils.js +324 -0
  179. package/src/utils/widgets.js +55 -0
  180. package/tests/components/core/Basic.test.js +121 -0
  181. package/tests/components/core/BasicMock.js +25 -0
  182. package/tests/components/core/CoverageMap.test.js +20 -0
  183. package/tests/components/core/Editor.test.js +20 -0
  184. package/tests/components/core/PhotoViewer.test.js +57 -0
  185. package/tests/components/core/Viewer.test.js +84 -0
  186. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +73 -0
  187. package/tests/components/core/__snapshots__/Viewer.test.js.snap +145 -0
  188. package/tests/components/ui/CopyButton.test.js +52 -0
  189. package/tests/components/ui/Loader.test.js +55 -0
  190. package/tests/components/{Map.test.js → ui/Map.test.js} +73 -61
  191. package/tests/components/{Photo.test.js → ui/Photo.test.js} +97 -63
  192. package/tests/components/ui/Popup.test.js +26 -0
  193. package/tests/components/ui/QualityScore.test.js +18 -0
  194. package/tests/components/ui/SearchBar.test.js +110 -0
  195. package/tests/components/ui/__snapshots__/CopyButton.test.js.snap +33 -0
  196. package/tests/components/ui/__snapshots__/Loader.test.js.snap +56 -0
  197. package/tests/components/{__snapshots__ → ui/__snapshots__}/Map.test.js.snap +11 -38
  198. package/tests/components/{__snapshots__ → ui/__snapshots__}/Photo.test.js.snap +70 -6
  199. package/tests/components/ui/__snapshots__/Popup.test.js.snap +29 -0
  200. package/tests/components/ui/__snapshots__/QualityScore.test.js.snap +11 -0
  201. package/tests/components/ui/__snapshots__/SearchBar.test.js.snap +65 -0
  202. package/tests/utils/API.test.js +83 -83
  203. package/tests/utils/InitParameters.test.js +499 -0
  204. package/tests/utils/URLHandler.test.js +401 -0
  205. package/tests/utils/__snapshots__/API.test.js.snap +10 -0
  206. package/tests/utils/__snapshots__/URLHandler.test.js.snap +21 -0
  207. package/tests/utils/__snapshots__/{Map.test.js.snap → geocoder.test.js.snap} +1 -1
  208. package/tests/utils/__snapshots__/map.test.js.snap +11 -0
  209. package/tests/utils/__snapshots__/picture.test.js.snap +327 -0
  210. package/tests/utils/__snapshots__/widgets.test.js.snap +19 -0
  211. package/tests/utils/geocoder.test.js +37 -0
  212. package/tests/utils/{I18n.test.js → i18n.test.js} +8 -8
  213. package/tests/utils/map.test.js +126 -0
  214. package/tests/utils/picture.test.js +745 -0
  215. package/tests/utils/utils.test.js +288 -0
  216. package/tests/utils/widgets.test.js +31 -0
  217. package/docs/01_Start.md +0 -149
  218. package/docs/02_Usage.md +0 -831
  219. package/docs/04_Advanced_examples.md +0 -216
  220. package/src/Editor.css +0 -37
  221. package/src/Editor.js +0 -361
  222. package/src/StandaloneMap.js +0 -114
  223. package/src/Viewer.css +0 -203
  224. package/src/Viewer.js +0 -1246
  225. package/src/components/CoreView.css +0 -70
  226. package/src/components/CoreView.js +0 -175
  227. package/src/components/Loader.css +0 -74
  228. package/src/components/Loader.js +0 -120
  229. package/src/img/loader_hd.jpg +0 -0
  230. package/src/utils/Exif.js +0 -193
  231. package/src/utils/Utils.js +0 -631
  232. package/src/utils/Widgets.js +0 -562
  233. package/src/viewer/URLHash.js +0 -469
  234. package/src/viewer/Widgets.css +0 -880
  235. package/src/viewer/Widgets.js +0 -1470
  236. package/tests/Editor.test.js +0 -126
  237. package/tests/StandaloneMap.test.js +0 -45
  238. package/tests/Viewer.test.js +0 -366
  239. package/tests/__snapshots__/Editor.test.js.snap +0 -298
  240. package/tests/__snapshots__/StandaloneMap.test.js.snap +0 -30
  241. package/tests/__snapshots__/Viewer.test.js.snap +0 -195
  242. package/tests/components/CoreView.test.js +0 -92
  243. package/tests/components/Loader.test.js +0 -38
  244. package/tests/components/__snapshots__/Loader.test.js.snap +0 -15
  245. package/tests/utils/Exif.test.js +0 -124
  246. package/tests/utils/Map.test.js +0 -113
  247. package/tests/utils/Utils.test.js +0 -300
  248. package/tests/utils/Widgets.test.js +0 -107
  249. package/tests/utils/__snapshots__/Exif.test.js.snap +0 -43
  250. package/tests/utils/__snapshots__/Utils.test.js.snap +0 -41
  251. package/tests/utils/__snapshots__/Widgets.test.js.snap +0 -44
  252. package/tests/viewer/URLHash.test.js +0 -559
  253. package/tests/viewer/Widgets.test.js +0 -127
  254. package/tests/viewer/__snapshots__/URLHash.test.js.snap +0 -108
  255. package/tests/viewer/__snapshots__/Widgets.test.js.snap +0 -403
@@ -0,0 +1,257 @@
1
+ import { LitElement, html, nothing, css } from "lit";
2
+ import { classMap } from "lit/directives/class-map.js";
3
+ import { fa } from "../../utils/widgets";
4
+ import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft";
5
+ import { faChevronUp } from "@fortawesome/free-solid-svg-icons/faChevronUp";
6
+ import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
7
+ import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
8
+ import { faCalendarAlt } from "@fortawesome/free-solid-svg-icons/faCalendarAlt";
9
+ import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation";
10
+ import { faShareNodes } from "@fortawesome/free-solid-svg-icons/faShareNodes";
11
+ import { placeholder, panel } from "../styles";
12
+ import { reverseGeocodingNominatim } from "../../utils/geocoder";
13
+
14
+ /**
15
+ * Picture legend shows info about picture author, capture date, address, and access to metadata popup.
16
+ * @class Panoramax.components.menus.PictureLegend
17
+ * @element pnx-picture-legend
18
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
19
+ * @slot `editors` External links to map editors, or any tool that may be helpful. Defaults to OSM tools (iD & JOSM).
20
+ * @example
21
+ * ```html
22
+ * <pnx-picture-legend ._parent=${viewer} />
23
+ * ```
24
+ */
25
+ export default class PictureLegend extends LitElement {
26
+ /** @private */
27
+ static styles = [placeholder, panel, css`
28
+ :host {
29
+ overflow-y: auto;
30
+ overflow-x: hidden;
31
+ display: block;
32
+ margin: 0;
33
+ font-family: var(--font-family);
34
+ }
35
+
36
+ @media screen and (min-width: 576px) {
37
+ :host { max-height: 70vh; }
38
+ pnx-picture-metadata { width: 30vw; }
39
+ }
40
+
41
+ .pnx-hidden { display: none !important; }
42
+
43
+ /* Top bar */
44
+ .headline {
45
+ display: flex;
46
+ gap: 10px;
47
+ align-items: center;
48
+ margin: 10px 10px 5px 10px;
49
+ justify-content: space-between;
50
+ }
51
+
52
+ /* Address line */
53
+ #pic-legend-addr {
54
+ line-height: 1.2em;
55
+ font-size: 1em;
56
+ margin-bottom: 2px;
57
+ flex-grow: 5;
58
+ font-weight: 800;
59
+ }
60
+
61
+ #pic-legend-addr span {
62
+ display: inline-block;
63
+ height: 100%;
64
+ width: 100%;
65
+ }
66
+
67
+ /* Minimal info block */
68
+ #pic-legend-info {
69
+ margin: 10px;
70
+ display: flex;
71
+ gap: 10px;
72
+ justify-content: space-around;
73
+ }
74
+ .info-block {
75
+ display: flex;
76
+ flex-shrink: 1;
77
+ gap: 5px;
78
+ font-weight: 600;
79
+ font-size: 0.85em;
80
+ align-items: center;
81
+ }
82
+ .info-block svg { height: 18px; }
83
+
84
+ /* Expand button */
85
+ #pic-legend-expand {
86
+ display: block;
87
+ margin-top: 5px;
88
+ max-width: 100%;
89
+ }
90
+ #pic-legend-expand::part(btn) {
91
+ border-radius: 10px;
92
+ border-top-right-radius: 0;
93
+ border-top-left-radius: 0;
94
+ }
95
+
96
+ /* Details block */
97
+ pnx-picture-metadata {
98
+ margin: 5px 10px 10px;
99
+ display: block;
100
+ max-width: 450px;
101
+ box-sizing: border-box;
102
+ }
103
+
104
+ /* Details actions */
105
+ #pic-legend-cta {
106
+ display: flex;
107
+ margin: 5px 10px;
108
+ border-bottom-left-radius: 10px;
109
+ border-bottom-right-radius: 10px;
110
+ gap: 5px;
111
+ flex-wrap: wrap;
112
+ }
113
+
114
+ /* More options menu */
115
+ #pnx-legend-opts { min-width: unset; }
116
+
117
+ /* Editors */
118
+ #pic-legend-editors { margin: 0 10px; }
119
+ `];
120
+
121
+ /** @private */
122
+ static properties = {
123
+ _caption: { state: true },
124
+ _addr: { state: true },
125
+ _expanded: { state: true },
126
+ collapsable: { type: Boolean },
127
+ };
128
+
129
+ /** @private */
130
+ constructor() {
131
+ super();
132
+ this._expanded = true;
133
+ this.collapsable = false;
134
+ }
135
+
136
+ /** @private */
137
+ connectedCallback() {
138
+ super.connectedCallback();
139
+
140
+ this._expanded = !this.collapsable;
141
+ this._prevSearches = {};
142
+
143
+ this._parent.onceReady().then(() => {
144
+ this._onPicChange(this._parent.psv.getPictureMetadata());
145
+ this._parent.psv.addEventListener("picture-loaded", () => {
146
+ this._onPicChange(this._parent.psv.getPictureMetadata());
147
+ });
148
+ });
149
+ }
150
+
151
+ /** @private */
152
+ _onPicChange(picMeta) {
153
+ clearTimeout(this._addrTimer1);
154
+ this._caption = picMeta?.caption;
155
+
156
+ if(picMeta) {
157
+ const coordsHash = `${picMeta.gps[0]}/${picMeta.gps[1]}`;
158
+ if(this._prevSearches[coordsHash]) {
159
+ this._addr = this._prevSearches[coordsHash];
160
+ }
161
+ else {
162
+ this._addrTimer2 = setTimeout(() => this._addr = "", 250);
163
+ this._addrTimer1 = setTimeout(() => {
164
+ reverseGeocodingNominatim(picMeta.gps[1], picMeta.gps[0])
165
+ .then(addr => {
166
+ clearTimeout(this._addrTimer2);
167
+ this._addr = addr;
168
+ this._prevSearches[coordsHash] = addr;
169
+ });
170
+ }, 750);
171
+ }
172
+ }
173
+ else {
174
+ this._addr = "";
175
+ }
176
+ }
177
+
178
+ /** @private */
179
+ _onBackClick() {
180
+ if(this._parent.isWidthSmall() && this._parent.focus === "map") {
181
+ this._parent.select();
182
+ }
183
+ else {
184
+ this._parent._setFocus?.("map");
185
+ }
186
+ }
187
+
188
+ /** @private */
189
+ render() {
190
+ if(!this._caption) { return nothing; }
191
+
192
+ const hiddenExpanded = classMap({"pnx-hidden": this._expanded});
193
+ const shownExpanded = classMap({"pnx-hidden": !this._expanded});
194
+
195
+ return html`
196
+ <div class="headline">
197
+ ${this._parent._setFocus ? html`
198
+ <pnx-button
199
+ kind="superinline"
200
+ @click=${this._onBackClick}
201
+ >
202
+ ${fa(faArrowLeft)}
203
+ </pnx-button>
204
+ ` : nothing}
205
+
206
+ <div id="pic-legend-addr">
207
+ ${this._addr?.length > 0 ? this._addr : html`<span class="pnx-placeholder-loading">&nbsp;</span>`}
208
+ </div>
209
+
210
+ <pnx-picture-legend-actions
211
+ @click=${e => e.stopPropagation()}
212
+ ._parent=${this._parent}
213
+ ?full=${this._expanded}
214
+ ></pnx-picture-legend-actions>
215
+ </div>
216
+
217
+ <div id="pic-legend-info" class=${hiddenExpanded}>
218
+ ${this._caption.producer?.length > 0 ? html`<div class="info-block">
219
+ ${fa(faUser)}
220
+ ${this._caption.producer[this._caption.producer.length-1]}
221
+ </div>` : nothing}
222
+
223
+ ${this._caption.date ? html`<div class="info-block">
224
+ ${fa(faCalendarAlt)}
225
+ ${this._caption.date.toLocaleDateString(undefined, { year: "numeric", month: "long" })}
226
+ </div>` : nothing}
227
+ </div>
228
+
229
+ <div id="pic-legend-cta" class=${shownExpanded}>
230
+ ${this._parent.api._endpoints.report ? html`
231
+ <pnx-button size="sm" @click=${() => this._parent._showReportForm()}>
232
+ ${fa(faTriangleExclamation)} ${this._parent?._t.pnx.report}
233
+ </pnx-button>
234
+ ` : nothing}
235
+
236
+ <pnx-button size="sm" @click=${() => this._parent._showShareOptions()}>
237
+ ${fa(faShareNodes)} ${this._parent?._t.pnx.share}
238
+ </pnx-button>
239
+
240
+ <slot name="editors">
241
+ <pnx-widget-osmeditors ._parent=${this._parent} />
242
+ </slot>
243
+ </div>
244
+
245
+ <pnx-picture-metadata class=${shownExpanded} ._parent=${this._parent}></pnx-picture-metadata>
246
+
247
+ ${this.collapsable ? html`<pnx-button
248
+ kind="inline"
249
+ size="sm"
250
+ id="pic-legend-expand"
251
+ @click=${() => this._expanded = !this._expanded}
252
+ >${this._expanded ? fa(faChevronUp) : fa(faChevronDown)}</pnx-button>` : nothing}
253
+ `;
254
+ }
255
+ }
256
+
257
+ customElements.define("pnx-picture-legend", PictureLegend);
@@ -0,0 +1,317 @@
1
+ import { LitElement, nothing, css } from "lit";
2
+ import { html, unsafeStatic } from "lit/static-html.js";
3
+ import { fa } from "../../utils/widgets";
4
+ import { faLocationDot } from "@fortawesome/free-solid-svg-icons/faLocationDot";
5
+ import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal";
6
+ import { faCamera } from "@fortawesome/free-solid-svg-icons/faCamera";
7
+ import { faImage } from "@fortawesome/free-solid-svg-icons/faImage";
8
+ import { faImages } from "@fortawesome/free-solid-svg-icons/faImages";
9
+ import { faScroll } from "@fortawesome/free-solid-svg-icons/faScroll";
10
+ import { faQuestion } from "@fortawesome/free-solid-svg-icons/faQuestion";
11
+ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
12
+ import { titles, textarea } from "../styles";
13
+ import { createWebComp } from "../../utils/widgets";
14
+ import { getGPSPrecision } from "../../utils/picture";
15
+ import {
16
+ getGrade, QUALITYSCORE_GPS_VALUES, QUALITYSCORE_RES_360_VALUES,
17
+ QUALITYSCORE_RES_FLAT_VALUES, QUALITYSCORE_POND_GPS, QUALITYSCORE_POND_RES
18
+ } from "../../utils/utils";
19
+
20
+ const missing = () => fa(faQuestion, {styles: {height: "16px"}});
21
+
22
+ /**
23
+ * Picture metadata displays detailed info about a single picture (ID, capture context, EXIF attributes...).
24
+ * @class Panoramax.components.menus.PictureMetadata
25
+ * @element pnx-picture-metadata
26
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
27
+ * @example
28
+ * ```html
29
+ * <pnx-picture-metadata ._parent=${viewer} />
30
+ * ```
31
+ */
32
+ export default class PictureMetadata extends LitElement {
33
+ /** @private */
34
+ static styles = [ titles, textarea, css`
35
+ div[slot="content"] {
36
+ padding: 5px 10px;
37
+ background-color: #ededed;
38
+ }
39
+
40
+ /* Small data blocks */
41
+ .data-block {
42
+ display: inline-block;
43
+ min-width: 50%;
44
+ margin: 8px 0;
45
+ box-sizing: border-box;
46
+ vertical-align: top;
47
+ }
48
+ .data-block h5 {
49
+ font-size: 0.8em;
50
+ font-weight: 400;
51
+ color: var(--blue-dark);
52
+ margin: 0 0 5px 0;
53
+ }
54
+ .data-block h5 pnx-button { vertical-align: middle; }
55
+ .data-block h5 svg.svg-inline--fa { height: 14px; }
56
+ .data-block div {
57
+ font-size: 0.8em;
58
+ }
59
+ ` ];
60
+
61
+ /** @private */
62
+ static properties = {
63
+ _meta: {state: true},
64
+ };
65
+
66
+ /** @private */
67
+ connectedCallback() {
68
+ super.connectedCallback();
69
+
70
+ this._meta = this._parent?.psv?.getPictureMetadata();
71
+ this._parent?.psv?.addEventListener("picture-loaded", () => {
72
+ this._meta = this._parent?.psv?.getPictureMetadata();
73
+ });
74
+ }
75
+
76
+ /** @private */
77
+ _onQualityScoreClick() {
78
+ const qsTab = this.shadowRoot.querySelector("pnx-tabs");
79
+ if(qsTab) {
80
+ qsTab.setAttribute("activeTabIndex", 3);
81
+ }
82
+ }
83
+
84
+ /** @private */
85
+ _toTab(title, data) {
86
+ return html`
87
+ <h4 slot="title">${title}</h4>
88
+ <div slot="content" class="data-blocks">
89
+ ${data.filter(b => b).map(b => html`<div class="data-block" style=${b.style}>
90
+ <h5>${b.title}</h5>
91
+ <div style=${b.content_style}>${b.content}</div>
92
+ </div>`)}
93
+ </div>
94
+ `;
95
+ }
96
+
97
+ /** @private */
98
+ render() {
99
+ /* eslint-disable indent */
100
+ if(!this._meta) { return nothing; }
101
+
102
+ // Generic information
103
+ const persOrient = this._meta?.properties?.["pers:interior_orientation"];
104
+ const makeModel = [persOrient.camera_manufacturer, persOrient.camera_model].filter(v => v).join(" ");
105
+ const focal = persOrient?.focal_length ? `${persOrient?.focal_length} mm` : missing();
106
+ let resmp = persOrient?.["sensor_array_dimensions"];
107
+ if(resmp) { resmp = `${resmp[0]} x ${resmp[1]} px (${Math.floor(resmp[0] * resmp[1] / 1000000)} Mpx)`;}
108
+ let pictype = this._parent?._t.pnx.picture_flat;
109
+ let pictypelong = this._parent?._t.pnx.picture_flat_long;
110
+ let picFov = persOrient?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up
111
+ if(picFov !== null && picFov !== undefined) {
112
+ if(picFov === 360) {
113
+ pictype = this._parent?._t.pnx.picture_360;
114
+ pictypelong = this._parent?._t.pnx.picture_360_long;
115
+ }
116
+ else { pictype += ` (${picFov}°)`; }
117
+ }
118
+
119
+ // Camera tab
120
+ const cameraData = [
121
+ { title: this._parent?._t.pnx.metadata_camera_make, content: persOrient?.camera_manufacturer || missing() },
122
+ { title: this._parent?._t.pnx.metadata_camera_model, content: persOrient?.camera_model || missing() },
123
+ { title: this._parent?._t.pnx.metadata_camera_type, content: pictype },
124
+ { title: this._parent?._t.pnx.metadata_camera_resolution, content: resmp || missing() },
125
+ { title: this._parent?._t.pnx.metadata_camera_focal_length, content: focal },
126
+ // Capture date
127
+ this._meta?.caption?.date && {
128
+ title: this._parent?._t.pnx.metadata_general_date,
129
+ content: html`
130
+ <strong>${new Intl.DateTimeFormat(undefined, {
131
+ timeZone: this._meta.caption.tz,
132
+ dateStyle: "short"
133
+ }).format(this._meta.caption.date)}</strong>
134
+ <br />${new Intl.DateTimeFormat(undefined, {
135
+ timeZone: this._meta.caption.tz,
136
+ hour: "numeric",
137
+ minute: "numeric",
138
+ second: "numeric",
139
+ fractionalSecondDigits: 3,
140
+ timeZoneName: "longOffset"
141
+ }).format(this._meta.caption.date)}
142
+ `
143
+ }
144
+ ];
145
+
146
+ // Location tab
147
+ const orientation = this._meta?.properties?.["view:azimuth"] !== undefined && `${this._meta.properties["view:azimuth"]}°`;
148
+ const locationData = [
149
+ { title: this._parent?._t.pnx.metadata_location_longitude, content: this._meta.gps[0] },
150
+ { title: this._parent?._t.pnx.metadata_location_latitude, content: this._meta.gps[1] },
151
+ { title: this._parent?._t.pnx.metadata_location_orientation, content: orientation || missing() },
152
+ { title: this._parent?._t.pnx.metadata_location_precision, content: getGPSPrecision(this._meta) || missing() },
153
+ ];
154
+
155
+ // Quality tab
156
+ const hasQualityScore = (
157
+ this._parent?.map?._hasQualityScore?.()
158
+ || this._meta?.properties?.["quality:horizontal_accuracy"]
159
+ || this._meta?.properties?.["panoramax:horizontal_pixel_density"]
160
+ );
161
+ let qualityData, generalGrade;
162
+ if(hasQualityScore) {
163
+ const gpsGrade = getGrade(QUALITYSCORE_GPS_VALUES, this._meta?.properties?.["quality:horizontal_accuracy"]);
164
+ const resGrade = getGrade(
165
+ this._meta?.horizontalFov === 360 ? QUALITYSCORE_RES_360_VALUES : QUALITYSCORE_RES_FLAT_VALUES,
166
+ this._meta?.properties?.["panoramax:horizontal_pixel_density"]
167
+ );
168
+
169
+ // Note: score is also calculated in utils/map code
170
+ generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS);
171
+
172
+ qualityData = [
173
+ {
174
+ title: html`
175
+ ${this._parent?._t.pnx.metadata_quality_score}
176
+ <pnx-button
177
+ title="${this._parent?._t.pnx.metadata_quality_help}"
178
+ kind="superinline"
179
+ @click=${() => this._parent?._showQualityScoreDoc()}
180
+ >
181
+ ${fa(faInfoCircle)}
182
+ </pnx-button>
183
+ `,
184
+ content: html`<pnx-quality-score grade=${generalGrade} style="font-size: 16px"></pnx-quality-score>`,
185
+ style: "width: 100%"
186
+ },
187
+ {
188
+ title: this._parent?._t.pnx.metadata_quality_gps_score,
189
+ content: createWebComp("pnx-grade", { stars: gpsGrade, _t: this._parent?._t })
190
+ },
191
+ {
192
+ title: this._parent?._t.pnx.metadata_quality_resolution_score,
193
+ content: createWebComp("pnx-grade", { stars: resGrade, _t: this._parent?._t })
194
+ },
195
+ ];
196
+ }
197
+
198
+ // EXIF data
199
+ const exifData = Object.entries(this._meta.properties.exif)
200
+ .sort()
201
+ .filter(([, value]) => value)
202
+ .map(([key, value]) => {
203
+ if(JSON.stringify(value).includes("\\u")) {
204
+ value = JSON.stringify(value).replace(/\\u[0-9A-Fa-f]{4}/g, unicode => (
205
+ " 0x" + parseInt(unicode.slice(2), 16).toString(16).toUpperCase().padStart(4, "0")
206
+ )).slice(1, -1).trim();
207
+ }
208
+ return {
209
+ title: key,
210
+ content: value.length > 30 ? html`<textarea readonly>${value}</textarea>`: value,
211
+ style: value.length > 30 ? "width: 100%" : undefined
212
+ };
213
+ });
214
+
215
+ // General metadata
216
+ const overview = [
217
+ // Producer
218
+ this._meta?.caption?.producer?.length > 0 && {
219
+ title: this._parent?._t.pnx.metadata_general_author,
220
+ content: html`
221
+ <strong>${this._meta.caption.producer[this._meta.caption.producer.length - 1]}</strong>
222
+ ${this._meta.caption.producer.length > 1 ? html`<br />${this._meta.caption.producer.slice(0, -1).join(", ")}` : nothing}
223
+ `
224
+ },
225
+ // Capture date
226
+ this._meta?.caption?.date && {
227
+ title: this._parent?._t.pnx.metadata_general_date,
228
+ content: html`
229
+ <strong>${new Intl.DateTimeFormat(undefined, {timeZone: this._meta.caption.tz, dateStyle: "long"}).format(this._meta.caption.date)}</strong>
230
+ <br />${new Intl.DateTimeFormat(undefined, {timeZone: this._meta.caption.tz, hour: "numeric",minute:"numeric"}).format(this._meta.caption.date)}
231
+ `
232
+ },
233
+ // Camera
234
+ persOrient && {
235
+ title: this._parent?._t.pnx.metadata_camera,
236
+ content: html`
237
+ <strong>${makeModel.length > 0 ? makeModel : missing()}</strong>
238
+ <br />${pictypelong}
239
+ `
240
+ },
241
+ // License
242
+ this._meta?.caption?.license && {
243
+ title: this._parent?._t.pnx.metadata_general_license,
244
+ content: html`${unsafeStatic(this._meta.caption.license)}`
245
+ },
246
+ // Quality score
247
+ hasQualityScore && {
248
+ title: this._parent?._t.pnx.metadata_quality,
249
+ content: html`<pnx-quality-score
250
+ grade=${generalGrade}
251
+ style="font-size: 14px; cursor: pointer"
252
+ @click=${this._onQualityScoreClick}
253
+ />`
254
+ },
255
+ // Copy ID
256
+ {
257
+ title: this._parent?._t.pnx.metadata_general_copy_id,
258
+ content_style: "display: flex; gap: 5px;",
259
+ content: html`
260
+ <pnx-copy-button
261
+ kind="outline"
262
+ size="sm"
263
+ ._t=${this._parent?._t}
264
+ text=${this._meta.id}
265
+ style="flex: 1"
266
+ >
267
+ ${fa(faImage)} ${this._parent?._t.pnx.metadata_general_picid}
268
+ </pnx-copy-button>
269
+ <pnx-copy-button
270
+ kind="outline"
271
+ size="sm"
272
+ ._t=${this._parent?._t}
273
+ text=${this._meta.sequence.id}
274
+ style="flex: 1"
275
+ >
276
+ ${fa(faImages)} ${this._parent?._t.pnx.metadata_general_seqid}
277
+ </pnx-copy-button>
278
+ `
279
+ },
280
+ this._meta?.origInstance && {
281
+ title: this._parent?._t.pnx.metadata_general_instance,
282
+ content: html`<strong><a href=${this._meta.origInstance.href+window.location.search} target="_blank">
283
+ ${this._meta.origInstance.instance_name || this._meta.origInstance.href.replace(/^http.?:\/\//, "")}
284
+ </a></strong>`
285
+ }
286
+ ];
287
+
288
+ return html`<pnx-tabs>
289
+ ${this._toTab( // General
290
+ html`${fa(faImage)} ${this._parent?._t.pnx.metadata_summary}`,
291
+ overview
292
+ )}
293
+
294
+ ${this._toTab( // Camera
295
+ html`${fa(faCamera)} ${this._parent?._t.pnx.metadata_camera}`,
296
+ cameraData
297
+ )}
298
+
299
+ ${this._toTab( // Position
300
+ html`${fa(faLocationDot)} ${this._parent?._t.pnx.metadata_location}`,
301
+ locationData
302
+ )}
303
+
304
+ ${hasQualityScore ? this._toTab( // Quality
305
+ html`${fa(faMedal)} ${this._parent?._t.pnx.metadata_quality}`,
306
+ qualityData
307
+ ) : nothing}
308
+
309
+ ${this._meta.properties?.exif ? this._toTab( // EXIF
310
+ html`${fa(faScroll)} ${this._parent?._t.pnx.metadata_exif}`,
311
+ exifData
312
+ ) : nothing}
313
+ </pnx-tabs>`;
314
+ }
315
+ }
316
+
317
+ customElements.define("pnx-picture-metadata", PictureMetadata);
@@ -0,0 +1,95 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { faSvg } from "../styles";
3
+ import { faRocket } from "@fortawesome/free-solid-svg-icons/faRocket";
4
+ import { faLightbulb } from "@fortawesome/free-solid-svg-icons/faLightbulb";
5
+ import { faPersonBiking } from "@fortawesome/free-solid-svg-icons/faPersonBiking";
6
+ import { PIC_MAX_STAY_DURATION } from "../ui/Photo";
7
+ import { fa } from "../../utils/widgets";
8
+
9
+ /**
10
+ * Player Options menu displays player speed and contrast settings.
11
+ * @class Panoramax.components.menus.PlayerOptions
12
+ * @element pnx-player-options
13
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
14
+ * @example
15
+ * ```html
16
+ * <pnx-player-options ._parent=${viewer} />
17
+ * ```
18
+ */
19
+ export default class PlayerOptions extends LitElement {
20
+ /** @private */
21
+ static styles = [ faSvg, css`
22
+ :host {
23
+ display: flex;
24
+ flex-direction: row;
25
+ align-items: center;
26
+ justify-content: center;
27
+ gap: 15px;
28
+ padding: 5px 15px;
29
+ }
30
+
31
+ .pnx-input-range {
32
+ display: flex;
33
+ justify-content: space-between;
34
+ gap: 10px;
35
+ align-items: center;
36
+ width: 100%;
37
+ }
38
+ ` ];
39
+
40
+ /** @private */
41
+ connectedCallback() {
42
+ super.connectedCallback();
43
+ this._parent?.psv?.addEventListener("transition-duration-changed", e => {
44
+ this.renderRoot.querySelector("#pnx-player-speed").value = PIC_MAX_STAY_DURATION - e.detail.value;
45
+ });
46
+ }
47
+
48
+ /** @private */
49
+ _onSpeedChange(e) {
50
+ const newSpeed = PIC_MAX_STAY_DURATION - e.target.value;
51
+ this._parent?.psv?.setTransitionDuration(newSpeed);
52
+ }
53
+
54
+ /** @private */
55
+ _onContrastClick() {
56
+ const btn = this.renderRoot.querySelector("#pnx-player-contrast");
57
+ if(btn.getAttribute("active") == "true") {
58
+ btn.removeAttribute("active");
59
+ this._parent?.psv?.setHigherContrast(false);
60
+ }
61
+ else {
62
+ btn.setAttribute("active", "true");
63
+ this._parent?.psv?.setHigherContrast(true);
64
+ }
65
+ }
66
+
67
+ /** @private */
68
+ render() {
69
+ return html`
70
+ <div class="pnx-input-range" title="${this._parent?._t.pnx.sequence_speed}">
71
+ ${fa(faPersonBiking)}
72
+ <input
73
+ id="pnx-player-speed"
74
+ type="range" name="speed"
75
+ min="0" max="${PIC_MAX_STAY_DURATION - 100}"
76
+ value="${PIC_MAX_STAY_DURATION - this._parent?.psv.getTransitionDuration()}"
77
+ title="${this._parent?._t.pnx.sequence_speed}"
78
+ style="width: 100%;"
79
+ @change=${this._onSpeedChange}
80
+ />
81
+ ${fa(faRocket)}
82
+ </div>
83
+ <pnx-button
84
+ id="pnx-player-contrast"
85
+ title="${this._parent?._t.pnx.contrast}"
86
+ kind="outline"
87
+ @click=${this._onContrastClick}
88
+ >
89
+ ${fa(faLightbulb)}
90
+ </pnx-button>
91
+ `;
92
+ }
93
+ }
94
+
95
+ customElements.define("pnx-player-options", PlayerOptions);
@@ -0,0 +1,36 @@
1
+ import { LitElement, html } from "lit";
2
+ import { faSvg, titles } from "../styles";
3
+ import { fa } from "../../utils/widgets";
4
+ import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal";
5
+
6
+ /**
7
+ * Quality Score Doc displays a synthetic summary of how Quality Score is computed.
8
+ * @class Panoramax.components.menus.QualityScoreDoc
9
+ * @element pnx-quality-score-doc
10
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
11
+ * @example
12
+ * ```html
13
+ * <pnx-quality-score-doc ._t=${viewer._t} />
14
+ * ```
15
+ */
16
+ export default class QualityScoreDoc extends LitElement {
17
+ static styles = [ titles, faSvg ];
18
+
19
+ render() {
20
+ return html`
21
+ <h4>${fa(faMedal)} ${this._t?.pnx.qualityscore_title}</h4>
22
+ <p>${this._t?.pnx.qualityscore_doc_1}</p>
23
+ <p>${this._t?.pnx.qualityscore_doc_2}</p>
24
+ <pnx-quality-score grade="5"></pnx-quality-score>
25
+ <p>${this._t?.pnx.qualityscore_doc_3}</p>
26
+ <pnx-link-button
27
+ href="https://docs.panoramax.fr/pictures-metadata/quality_score/"
28
+ target="_blank"
29
+ kind="outline"
30
+ style="width: 100%"
31
+ >${this._t?.pnx.qualityscore_doc_link}</pnx-link-button>
32
+ `;
33
+ }
34
+ }
35
+
36
+ customElements.define("pnx-quality-score-doc", QualityScoreDoc);