@panoramax/web-viewer 5.0.0-develop-d7206514 → 5.0.0-develop-d26305dd

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 (152) hide show
  1. package/build/bundle.cjs +3399 -0
  2. package/build/bundle.cjs.map +1 -0
  3. package/build/bundle_photoviewer.cjs +2510 -0
  4. package/build/bundle_photoviewer.cjs.map +1 -0
  5. package/build/cjs/index.js +1 -1
  6. package/build/cjs/index_photoviewer.js +1 -1
  7. package/build/components/core/Basic.css +56 -0
  8. package/build/components/core/Basic.js +378 -0
  9. package/build/components/core/CoverageMap.css +10 -0
  10. package/build/components/core/CoverageMap.js +169 -0
  11. package/build/components/core/Editor.css +33 -0
  12. package/build/components/core/Editor.js +398 -0
  13. package/build/components/core/PhotoViewer.css +70 -0
  14. package/build/components/core/PhotoViewer.js +650 -0
  15. package/build/components/core/Viewer.css +130 -0
  16. package/build/components/core/Viewer.js +711 -0
  17. package/build/components/core/index.js +10 -0
  18. package/build/components/index.js +11 -0
  19. package/build/components/index_photoviewer.js +6 -0
  20. package/build/components/layout/BottomDrawer.js +258 -0
  21. package/build/components/layout/CorneredGrid.js +143 -0
  22. package/build/components/layout/Mini.js +121 -0
  23. package/build/components/layout/Tabs.js +140 -0
  24. package/build/components/layout/index.js +9 -0
  25. package/build/components/menus/LocationPrecisionDoc.js +42 -0
  26. package/build/components/menus/MapBackground.js +110 -0
  27. package/build/components/menus/MapFilters.js +567 -0
  28. package/build/components/menus/MapLayers.js +238 -0
  29. package/build/components/menus/MapLegend.js +68 -0
  30. package/build/components/menus/MiniPictureLegend.js +73 -0
  31. package/build/components/menus/PictureLegend.js +379 -0
  32. package/build/components/menus/PictureMetadata.js +380 -0
  33. package/build/components/menus/PlayerOptions.js +93 -0
  34. package/build/components/menus/QualityScoreDoc.js +42 -0
  35. package/build/components/menus/ReportForm.js +132 -0
  36. package/build/components/menus/SemanticsDoc.js +38 -0
  37. package/build/components/menus/SemanticsDownload.js +33 -0
  38. package/build/components/menus/SemanticsFilters.js +153 -0
  39. package/build/components/menus/SemanticsList.js +413 -0
  40. package/build/components/menus/SemanticsMetadata.js +368 -0
  41. package/build/components/menus/Share.js +105 -0
  42. package/build/components/menus/index.js +22 -0
  43. package/build/components/menus/index_photoviewer.js +11 -0
  44. package/build/components/styles.js +557 -0
  45. package/build/components/ui/AnnotationsSwitch.js +159 -0
  46. package/build/components/ui/Button.js +77 -0
  47. package/build/components/ui/ButtonGroup.css +59 -0
  48. package/build/components/ui/ButtonGroup.js +69 -0
  49. package/build/components/ui/CopyButton.js +110 -0
  50. package/build/components/ui/Grade.js +54 -0
  51. package/build/components/ui/GradeFilter.js +122 -0
  52. package/build/components/ui/IconSwitch.js +193 -0
  53. package/build/components/ui/LinkButton.js +67 -0
  54. package/build/components/ui/ListGroup.js +66 -0
  55. package/build/components/ui/ListItem.js +90 -0
  56. package/build/components/ui/Loader.js +203 -0
  57. package/build/components/ui/Map.css +63 -0
  58. package/build/components/ui/Map.js +853 -0
  59. package/build/components/ui/MapMore.js +175 -0
  60. package/build/components/ui/Photo.css +50 -0
  61. package/build/components/ui/Photo.js +1502 -0
  62. package/build/components/ui/Popup.js +145 -0
  63. package/build/components/ui/ProgressBar.js +104 -0
  64. package/build/components/ui/QualityScore.js +147 -0
  65. package/build/components/ui/SearchBar.js +374 -0
  66. package/build/components/ui/SemanticsEditor.js +191 -0
  67. package/build/components/ui/SemanticsTable.js +88 -0
  68. package/build/components/ui/Switch.js +139 -0
  69. package/build/components/ui/TogglableGroup.js +157 -0
  70. package/build/components/ui/index.js +29 -0
  71. package/build/components/ui/index_photoviewer.js +21 -0
  72. package/build/components/ui/widgets/CopyCoordinates.js +75 -0
  73. package/build/components/ui/widgets/GeoSearch.css +21 -0
  74. package/build/components/ui/widgets/GeoSearch.js +150 -0
  75. package/build/components/ui/widgets/Legend.js +190 -0
  76. package/build/components/ui/widgets/LevelSelect.css +51 -0
  77. package/build/components/ui/widgets/LevelSelect.js +143 -0
  78. package/build/components/ui/widgets/MapFiltersButton.js +114 -0
  79. package/build/components/ui/widgets/MapLayersButton.js +79 -0
  80. package/build/components/ui/widgets/OSMEditors.js +155 -0
  81. package/build/components/ui/widgets/PictureLegendActions.js +99 -0
  82. package/build/components/ui/widgets/Player.css +7 -0
  83. package/build/components/ui/widgets/Player.js +154 -0
  84. package/build/components/ui/widgets/SemanticsFiltersButton.js +65 -0
  85. package/build/components/ui/widgets/Zoom.js +84 -0
  86. package/build/components/ui/widgets/index.js +16 -0
  87. package/build/components/ui/widgets/index_photoviewer.js +7 -0
  88. package/build/esm/components/core/Basic.js +1 -1
  89. package/build/img/arrow_360.svg +14 -0
  90. package/build/img/arrow_flat.svg +11 -0
  91. package/build/img/arrow_triangle.svg +9 -0
  92. package/build/img/arrow_turn.svg +8 -0
  93. package/build/img/bg_aerial.jpg +0 -0
  94. package/build/img/bg_streets.jpg +0 -0
  95. package/build/img/loader_base.jpg +0 -0
  96. package/build/img/logo_dead.svg +91 -0
  97. package/build/img/marker.svg +17 -0
  98. package/build/img/marker_blue.svg +20 -0
  99. package/build/img/osm.svg +49 -0
  100. package/build/img/panoramax.svg +13 -0
  101. package/build/img/switch_big.svg +54 -0
  102. package/build/img/switch_mini.svg +48 -0
  103. package/build/img/wd.svg +1 -0
  104. package/build/index_photoviewer.js +4 -0
  105. package/build/package.json +148 -0
  106. package/build/servers.js +14 -0
  107. package/build/translations/ar.json +1 -0
  108. package/build/translations/be.json +257 -0
  109. package/build/translations/br.json +81 -0
  110. package/build/translations/cy.json +117 -0
  111. package/build/translations/da.json +300 -0
  112. package/build/translations/de.json +309 -0
  113. package/build/translations/en.json +294 -0
  114. package/build/translations/eo.json +235 -0
  115. package/build/translations/es.json +292 -0
  116. package/build/translations/fi.json +1 -0
  117. package/build/translations/fr.json +294 -0
  118. package/build/translations/hr.json +294 -0
  119. package/build/translations/hu.json +294 -0
  120. package/build/translations/it.json +306 -0
  121. package/build/translations/ja.json +182 -0
  122. package/build/translations/ko.json +1 -0
  123. package/build/translations/nl.json +305 -0
  124. package/build/translations/nn.json +1 -0
  125. package/build/translations/pl.json +169 -0
  126. package/build/translations/pt.json +296 -0
  127. package/build/translations/pt_BR.json +304 -0
  128. package/build/translations/sv.json +182 -0
  129. package/build/translations/ti.json +9 -0
  130. package/build/translations/tr.json +297 -0
  131. package/build/translations/uk.json +268 -0
  132. package/build/translations/zh_Hant.json +309 -0
  133. package/build/utils/API.js +928 -0
  134. package/build/utils/InitParameters.js +521 -0
  135. package/build/utils/MapStyleComposer.js +889 -0
  136. package/build/utils/PanoraMapProtocol.js +49 -0
  137. package/build/utils/PhotoAdapter.js +49 -0
  138. package/build/utils/PresetsManager.js +148 -0
  139. package/build/utils/SemanticsMapProtocol.js +144 -0
  140. package/build/utils/URLHandler.js +426 -0
  141. package/build/utils/geocoder.js +203 -0
  142. package/build/utils/i18n.js +128 -0
  143. package/build/utils/index.js +17 -0
  144. package/build/utils/index_photoviewer.js +14 -0
  145. package/build/utils/indoor.js +200 -0
  146. package/build/utils/map.js +788 -0
  147. package/build/utils/picture.js +507 -0
  148. package/build/utils/semantics.js +321 -0
  149. package/build/utils/services.js +148 -0
  150. package/build/utils/utils.js +433 -0
  151. package/build/utils/widgets.js +110 -0
  152. package/package.json +1 -1
@@ -0,0 +1,380 @@
1
+ import { LitElement, css, nothing } from "lit";
2
+ import { html, unsafeStatic } from "lit/static-html.js";
3
+ import { fa, onceParentAvailable, createWebComp } from "../../utils/widgets.js";
4
+ import {
5
+ faCamera, faImage, faImages, faInfoCircle, faLocationDot,
6
+ faMedal, faQuestion, faScroll, faTags,
7
+ } from "@fortawesome/free-solid-svg-icons";
8
+ import { dataBlocks, faSvg, hidden, textarea, titles } from "../styles.js";
9
+ import { getGPSPrecision } from "../../utils/picture.js";
10
+ import { PanoramaxMetaCatalogURL } from "../../utils/services.js";
11
+ import {
12
+ QUALITYSCORE_GPS_VALUES, QUALITYSCORE_POND_GPS, QUALITYSCORE_POND_RES,
13
+ QUALITYSCORE_RES_360_VALUES, QUALITYSCORE_RES_FLAT_VALUES, getGrade
14
+ } from "../../utils/utils.js";
15
+
16
+ const missing = () => fa(faQuestion, {styles: {height: "16px"}});
17
+
18
+ /**
19
+ * Picture metadata displays detailed info about a single picture (ID, capture context, EXIF attributes...).
20
+ * @class Panoramax.components.menus.PictureMetadata
21
+ * @element pnx-picture-metadata
22
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
23
+ * @example
24
+ * ```html
25
+ * <pnx-picture-metadata ._parent=${viewer} />
26
+ * ```
27
+ */
28
+ export default class PictureMetadata extends LitElement {
29
+ /** @private */
30
+ static styles = [ faSvg, titles, textarea, hidden, dataBlocks, css`
31
+ div[slot="content"] {
32
+ padding: 5px 10px;
33
+ background-color: #ededed;
34
+ }
35
+
36
+ .data-block {
37
+ min-width: 50%;
38
+ }
39
+
40
+ pnx-semantics-table {
41
+ overflow-x: auto;
42
+ display: block;
43
+ }
44
+ ` ];
45
+
46
+ /**
47
+ * Component properties.
48
+ * @memberof Panoramax.components.menus.PictureMetadata#
49
+ * @type {Object}
50
+ * @property {boolean} [block-editor=false] Set to true to avoid going beyond semantics editor
51
+ */
52
+ static properties = {
53
+ _meta: {state: true},
54
+ "block-editor": { type: Boolean },
55
+ };
56
+
57
+ /** @private */
58
+ connectedCallback() {
59
+ super.connectedCallback();
60
+
61
+ const gototags = () => {
62
+ const tabs = this.shadowRoot.querySelector("pnx-tabs");
63
+ if(tabs) { tabs.setAttribute("activeTabIndex", 4); }
64
+ };
65
+
66
+ window.navigation?.addEventListener("navigate", () => {
67
+ this.requestUpdate();
68
+ });
69
+
70
+ onceParentAvailable(this).then(() => {
71
+ this._meta = this._parent?.psv?.getPictureMetadata();
72
+ this._parent?.oncePSVReady?.().then(() => {
73
+ this._parent.psv.addEventListener("picture-loaded", () => {
74
+ this._meta = this._parent.psv.getPictureMetadata();
75
+ });
76
+ if(!this._parent._initParams.getParentPostInit().skipTagsMenuOpening) {
77
+ this._parent.psv.addEventListener("annotation-focused", gototags);
78
+ this._parent.psv.addEventListener("annotations-toggled", e => {
79
+ if(e.detail.visible) {
80
+ gototags();
81
+ }
82
+ });
83
+ if(this._parent.psv.getSelectedAnnotations().length > 0) {
84
+ gototags();
85
+ }
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ /** @private */
92
+ _onQualityScoreClick() {
93
+ const qsTab = this.shadowRoot.querySelector("pnx-tabs");
94
+ if(qsTab) {
95
+ qsTab.setAttribute("activeTabIndex", 3);
96
+ }
97
+ }
98
+
99
+ /** @private */
100
+ _onCaptureDateClick() {
101
+ const qsTab = this.shadowRoot.querySelector("pnx-tabs");
102
+ if(qsTab) {
103
+ qsTab.setAttribute("activeTabIndex", 1);
104
+ }
105
+ }
106
+
107
+ /** @private */
108
+ _toTab(title, data) {
109
+ return html`
110
+ <h4 slot="title">${title}</h4>
111
+ <div slot="content" class="data-blocks">
112
+ ${data.filter(b => b).map(b => html`<div class="data-block" style=${b.style}>
113
+ <h5>${b.title}</h5>
114
+ <div style=${b.content_style} @click=${b.click}>${b.content}</div>
115
+ </div>`)}
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ /** @private */
121
+ render() {
122
+ if(!this._meta) { return nothing; }
123
+
124
+ const lang = this._parent?.lang || window.navigator.language;
125
+
126
+ // Generic information
127
+ const persOrient = this._meta?.properties?.["pers:interior_orientation"];
128
+ const makeModel = [persOrient.camera_manufacturer, persOrient.camera_model].filter(v => v).join(" ");
129
+ const focal = persOrient?.focal_length ? `${persOrient?.focal_length} mm` : missing();
130
+ let resmp = persOrient?.["sensor_array_dimensions"];
131
+ if(resmp) { resmp = `${resmp[0]} x ${resmp[1]} px (${Math.floor(resmp[0] * resmp[1] / 1000000)} Mpx)`;}
132
+ let pictype = this._parent?._t.pnx.picture_flat;
133
+ let pictypelong = this._parent?._t.pnx.picture_flat_long;
134
+ let picFov = persOrient?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up
135
+ if(picFov !== null && picFov !== undefined) {
136
+ if(picFov === 360) {
137
+ pictype = this._parent?._t.pnx.picture_360;
138
+ pictypelong = this._parent?._t.pnx.picture_360_long;
139
+ }
140
+ else { pictype += ` (${picFov}°)`; }
141
+ }
142
+
143
+ // Camera tab
144
+ const cameraData = [
145
+ { title: this._parent?._t.pnx.metadata_camera_make, content: persOrient?.camera_manufacturer || missing() },
146
+ { title: this._parent?._t.pnx.metadata_camera_model, content: persOrient?.camera_model || missing() },
147
+ { title: this._parent?._t.pnx.metadata_camera_type, content: pictype },
148
+ { title: this._parent?._t.pnx.metadata_camera_resolution, content: resmp || missing() },
149
+ { title: this._parent?._t.pnx.metadata_camera_focal_length, content: focal },
150
+ // Capture date
151
+ this._meta?.caption?.date && {
152
+ title: this._parent?._t.pnx.metadata_general_date,
153
+ content: html`
154
+ <strong>${new Intl.DateTimeFormat(lang, {
155
+ timeZone: this._meta.caption.tz,
156
+ dateStyle: "short"
157
+ }).format(this._meta.caption.date)}</strong>
158
+ <br />${new Intl.DateTimeFormat(lang, {
159
+ timeZone: this._meta.caption.tz,
160
+ hour: "numeric",
161
+ minute: "numeric",
162
+ second: "numeric",
163
+ fractionalSecondDigits: 3,
164
+ timeZoneName: "longOffset"
165
+ }).format(this._meta.caption.date)}
166
+ `
167
+ }
168
+ ];
169
+
170
+ // Location tab
171
+ const orientation = this._meta?.properties?.["view:azimuth"] !== undefined && `${this._meta.properties["view:azimuth"]}°`;
172
+ const locationData = [
173
+ {
174
+ title: this._parent?._t.pnx.metadata_location_coordinates,
175
+ content: html`
176
+ ${this._meta.gps[0].toFixed(5)}, ${this._meta.gps[1].toFixed(5)}
177
+ <pnx-copy-coordinates
178
+ ._parent=${this._parent}
179
+ .gps=${this._meta.gps}
180
+ style="margin-left: 10px"
181
+ ></pnx-copy-coordinates>
182
+ `,
183
+ style: "width: 100%"
184
+ },
185
+ { title: this._parent?._t.pnx.metadata_location_orientation, content: orientation || missing() },
186
+ { title: this._parent?._t.pnx.metadata_location_precision, content: getGPSPrecision(this._meta) || missing() },
187
+ ];
188
+
189
+ // Quality tab
190
+ const hasQualityScore = (
191
+ this._parent?.map?._hasQualityScore?.()
192
+ || this._meta?.properties?.["quality:horizontal_accuracy"]
193
+ || this._meta?.properties?.["panoramax:horizontal_pixel_density"]
194
+ );
195
+ let qualityData, generalGrade;
196
+ if(hasQualityScore) {
197
+ const gpsGrade = getGrade(QUALITYSCORE_GPS_VALUES, this._meta?.properties?.["quality:horizontal_accuracy"]);
198
+ const resGrade = getGrade(
199
+ this._meta?.horizontalFov === 360 ? QUALITYSCORE_RES_360_VALUES : QUALITYSCORE_RES_FLAT_VALUES,
200
+ this._meta?.properties?.["panoramax:horizontal_pixel_density"]
201
+ );
202
+
203
+ // Note: score is also calculated in utils/map code
204
+ generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS);
205
+
206
+ qualityData = [
207
+ {
208
+ title: html`
209
+ ${this._parent?._t.pnx.metadata_quality_score}
210
+ <pnx-button
211
+ title="${this._parent?._t.pnx.metadata_quality_help}"
212
+ kind="superinline"
213
+ @click=${() => this._parent?._showQualityScoreDoc()}
214
+ >
215
+ ${fa(faInfoCircle)}
216
+ </pnx-button>
217
+ `,
218
+ content: html`<pnx-quality-score grade=${generalGrade} style="font-size: 16px"></pnx-quality-score>`,
219
+ style: "width: 100%"
220
+ },
221
+ {
222
+ title: this._parent?._t.pnx.metadata_quality_gps_score,
223
+ content: createWebComp("pnx-grade", { stars: gpsGrade, _t: this._parent?._t })
224
+ },
225
+ {
226
+ title: this._parent?._t.pnx.metadata_quality_resolution_score,
227
+ content: createWebComp("pnx-grade", { stars: resGrade, _t: this._parent?._t })
228
+ },
229
+ ];
230
+ }
231
+
232
+ // EXIF data
233
+ const exifData = Object.entries(this._meta.properties.exif)
234
+ .sort()
235
+ .filter(([, value]) => value)
236
+ .map(([key, value]) => {
237
+ if(JSON.stringify(value).includes("\\u")) {
238
+ value = JSON.stringify(value).replace(/\\u[0-9A-Fa-f]{4}/g, unicode => (
239
+ " 0x" + parseInt(unicode.slice(2), 16).toString(16).toUpperCase().padStart(4, "0")
240
+ )).slice(1, -1).trim();
241
+ }
242
+ return {
243
+ title: key,
244
+ content: value.length > 30 ? html`<textarea readonly .value=${value}></textarea>`: value,
245
+ style: value.length > 30 ? "width: 100%" : undefined
246
+ };
247
+ });
248
+ exifData.unshift({
249
+ content: html`
250
+ <pnx-link-button
251
+ size="sm"
252
+ target="_blank"
253
+ href="https://exiv2.org/metadata.html"
254
+ title=${this._parent?._t.pnx.metadata_exif_doc_title}
255
+ >${fa(faInfoCircle)} ${this._parent?._t.pnx.metadata_exif_doc}</pnx-link-button>
256
+ `,
257
+ style: "width: 100%"
258
+ });
259
+
260
+ // General metadata
261
+ const overview = [
262
+ // Producer
263
+ this._meta?.caption?.producer?.length > 0 && {
264
+ title: this._parent?._t.pnx.metadata_general_author,
265
+ content: html`
266
+ <strong>${this._meta.caption.producer[this._meta.caption.producer.length - 1]}</strong>
267
+ ${this._meta.caption.producer.length > 1 ? html`<br />${this._meta.caption.producer.slice(0, -1).join(", ")}` : nothing}
268
+ `
269
+ },
270
+ // Capture date
271
+ this._meta?.caption?.date && {
272
+ title: this._parent?._t.pnx.metadata_general_date,
273
+ click: this._onCaptureDateClick,
274
+ content_style: "cursor: pointer",
275
+ content: html`
276
+ <strong>${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, dateStyle: "long"}).format(this._meta.caption.date)}</strong>
277
+ <br />${new Intl.DateTimeFormat(lang, {timeZone: this._meta.caption.tz, hour: "numeric",minute:"numeric", second: "numeric"}).format(this._meta.caption.date)}
278
+ `
279
+ },
280
+ // Camera
281
+ persOrient && {
282
+ title: this._parent?._t.pnx.metadata_camera,
283
+ content: html`
284
+ <strong>${makeModel.length > 0 ? makeModel : missing()}</strong>
285
+ <br />${pictypelong}
286
+ `
287
+ },
288
+ // License
289
+ this._meta?.caption?.license && {
290
+ title: this._parent?._t.pnx.metadata_general_license,
291
+ content: html`${unsafeStatic(this._meta.caption.license)}`
292
+ },
293
+ // Quality score
294
+ hasQualityScore && {
295
+ title: this._parent?._t.pnx.metadata_quality,
296
+ click: this._onQualityScoreClick,
297
+ content: html`<pnx-quality-score
298
+ grade=${generalGrade}
299
+ style="font-size: 14px; cursor: pointer"
300
+ />`
301
+ },
302
+ // Copy ID
303
+ {
304
+ title: this._parent?._t.pnx.metadata_general_copy_id,
305
+ content_style: "display: flex; gap: 5px;",
306
+ content: html`
307
+ <pnx-copy-button
308
+ kind="outline"
309
+ size="sm"
310
+ ._t=${this._parent?._t}
311
+ text=${this._meta.id}
312
+ title=${this._meta.id}
313
+ style="flex: 1"
314
+ >
315
+ ${fa(faImage)} ${this._parent?._t.pnx.metadata_general_picid}
316
+ </pnx-copy-button>
317
+ <pnx-copy-button
318
+ kind="outline"
319
+ size="sm"
320
+ ._t=${this._parent?._t}
321
+ text=${this._meta.sequence.id}
322
+ title=${this._meta.sequence.id}
323
+ style="flex: 1"
324
+ >
325
+ ${fa(faImages)} ${this._parent?._t.pnx.metadata_general_seqid}
326
+ </pnx-copy-button>
327
+ `
328
+ },
329
+ this._meta?.origInstance && {
330
+ title: this._parent?._t.pnx.metadata_general_instance,
331
+ content: html`<strong>
332
+ <a href=${this._meta.origInstance.href+window.location.search} target="_blank" style="text-decoration: none">
333
+ <img
334
+ src=${PanoramaxMetaCatalogURL()+"/api/instances/"+this._meta.origInstance.instance_name+"/logo"}
335
+ style="height: 30px; max-width: 150px; vertical-align: middle"
336
+ alt=""
337
+ />
338
+ <span style="text-decoration: underline">
339
+ ${this._meta.origInstance.instance_name || this._meta.origInstance.href.replace(/^http.?:\/\//, "")}
340
+ </span>
341
+ </a>
342
+ </strong>`
343
+ }
344
+ ];
345
+
346
+ return html`<pnx-tabs .hideNav=${this["block-editor"]}>
347
+ ${this._toTab( // General
348
+ html`${fa(faImage)} ${this._parent?._t.pnx.metadata_summary}`,
349
+ overview
350
+ )}
351
+
352
+ ${this._toTab( // Camera
353
+ html`${fa(faCamera)} ${this._parent?._t.pnx.metadata_camera}`,
354
+ cameraData
355
+ )}
356
+
357
+ ${this._toTab( // Position
358
+ html`${fa(faLocationDot)} ${this._parent?._t.pnx.metadata_location}`,
359
+ locationData
360
+ )}
361
+
362
+ ${hasQualityScore ? this._toTab( // Quality
363
+ html`${fa(faMedal)} ${this._parent?._t.pnx.metadata_quality}`,
364
+ qualityData
365
+ ) : nothing}
366
+
367
+ <h4 slot="title">${fa(faTags)} ${this._parent?._t.pnx.semantics_title}</h4>
368
+ <div slot="content" class="data-blocks">
369
+ <pnx-semantics-metadata ._parent=${this._parent}></pnx-semantics-metadata>
370
+ </div>
371
+
372
+ ${this._meta.properties?.exif ? this._toTab( // EXIF
373
+ html`${fa(faScroll)} ${this._parent?._t.pnx.metadata_exif}`,
374
+ exifData
375
+ ) : nothing}
376
+ </pnx-tabs>`;
377
+ }
378
+ }
379
+
380
+ customElements.define("pnx-picture-metadata", PictureMetadata);
@@ -0,0 +1,93 @@
1
+ import { LitElement, css, html } from "lit";
2
+ import { faSvg } from "../styles.js";
3
+ import { faLightbulb, faPersonBiking, faRocket } from "@fortawesome/free-solid-svg-icons";
4
+ import { PIC_MAX_STAY_DURATION } from "../ui/Photo.js";
5
+ import { fa, onceParentAvailable } from "../../utils/widgets.js";
6
+
7
+ /**
8
+ * Player Options menu displays player speed and contrast settings.
9
+ * @class Panoramax.components.menus.PlayerOptions
10
+ * @element pnx-player-options
11
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
12
+ * @example
13
+ * ```html
14
+ * <pnx-player-options ._parent=${viewer} />
15
+ * ```
16
+ */
17
+ export default class PlayerOptions extends LitElement {
18
+ /** @private */
19
+ static styles = [ faSvg, css`
20
+ :host {
21
+ display: flex;
22
+ flex-direction: row;
23
+ align-items: center;
24
+ justify-content: center;
25
+ gap: 15px;
26
+ padding: 5px 15px;
27
+ }
28
+
29
+ .pnx-input-range {
30
+ display: flex;
31
+ justify-content: space-between;
32
+ gap: 10px;
33
+ align-items: center;
34
+ width: 100%;
35
+ }
36
+ ` ];
37
+
38
+ /** @private */
39
+ connectedCallback() {
40
+ super.connectedCallback();
41
+ onceParentAvailable(this).then(() => this._parent?.psv?.addEventListener("transition-duration-changed", e => {
42
+ this.renderRoot.querySelector("#pnx-player-speed").value = PIC_MAX_STAY_DURATION - e.detail.value;
43
+ }));
44
+ }
45
+
46
+ /** @private */
47
+ _onSpeedChange(e) {
48
+ const newSpeed = PIC_MAX_STAY_DURATION - e.target.value;
49
+ this._parent?.psv?.setTransitionDuration(newSpeed);
50
+ }
51
+
52
+ /** @private */
53
+ _onContrastClick() {
54
+ const btn = this.renderRoot.querySelector("#pnx-player-contrast");
55
+ if(btn.getAttribute("active") == "true") {
56
+ btn.removeAttribute("active");
57
+ this._parent?.psv?.setHigherContrast(false);
58
+ }
59
+ else {
60
+ btn.setAttribute("active", "true");
61
+ this._parent?.psv?.setHigherContrast(true);
62
+ }
63
+ }
64
+
65
+ /** @private */
66
+ render() {
67
+ return html`
68
+ <div class="pnx-input-range" title="${this._parent?._t.pnx.sequence_speed}">
69
+ ${fa(faPersonBiking)}
70
+ <input
71
+ id="pnx-player-speed"
72
+ type="range" name="speed"
73
+ min="0" max="${PIC_MAX_STAY_DURATION - 100}"
74
+ value="${PIC_MAX_STAY_DURATION - this._parent?.psv.getTransitionDuration()}"
75
+ title="${this._parent?._t.pnx.sequence_speed}"
76
+ style="width: 100%;"
77
+ @change=${this._onSpeedChange}
78
+ />
79
+ ${fa(faRocket)}
80
+ </div>
81
+ <pnx-button
82
+ id="pnx-player-contrast"
83
+ title="${this._parent?._t.pnx.contrast}"
84
+ kind="outline"
85
+ @click=${this._onContrastClick}
86
+ >
87
+ ${fa(faLightbulb)}
88
+ </pnx-button>
89
+ `;
90
+ }
91
+ }
92
+
93
+ customElements.define("pnx-player-options", PlayerOptions);
@@ -0,0 +1,42 @@
1
+ import { LitElement, html } from "lit";
2
+ import { docPopup, faSvg, titles } from "../styles.js";
3
+ import { fa } from "../../utils/widgets.js";
4
+ import { faExternalLinkAlt, faMedal } from "@fortawesome/free-solid-svg-icons";
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, docPopup ];
18
+
19
+ render() {
20
+ return html`
21
+ <h4>${fa(faMedal)} ${this._t?.pnx.filter_qualityscore}</h4>
22
+ <p>${this._t?.pnx.qualityscore_doc_1}</p>
23
+ <p>${this._t?.pnx.qualityscore_doc_2}</p>
24
+ <pnx-quality-score grade=""></pnx-quality-score>
25
+ <p>${this._t?.pnx.qualityscore_doc_3}</p>
26
+ <p>${this._t?.pnx.qualityscore_doc_4}</p>
27
+ <ul>
28
+ <li>${this._t?.pnx.qualityscore_doc_41}</li>
29
+ <li>${this._t?.pnx.qualityscore_doc_42}</li>
30
+ </ul>
31
+ <p>${this._t?.pnx.qualityscore_doc_5}</p>
32
+ <pnx-link-button
33
+ href="https://docs.panoramax.fr/pictures-metadata/quality_score/"
34
+ target="_blank"
35
+ kind="outline"
36
+ style="width: 100%"
37
+ >${fa(faExternalLinkAlt)} ${this._t?.pnx.popup_details_link}</pnx-link-button>
38
+ `;
39
+ }
40
+ }
41
+
42
+ customElements.define("pnx-quality-score-doc", QualityScoreDoc);
@@ -0,0 +1,132 @@
1
+ import { LitElement, css, html, nothing } from "lit";
2
+ import { faSvg, input, select, textarea, titles } from "../styles.js";
3
+ import { fa } from "../../utils/widgets.js";
4
+ import {
5
+ faAt, faCircleInfo, faCommentDots,
6
+ faPaperPlane, faTriangleExclamation,
7
+ } from "@fortawesome/free-solid-svg-icons";
8
+ import { getUserAccount } from "../../utils/utils.js";
9
+
10
+ const REPORT_NATURE = [
11
+ "", "blur_missing", "blur_excess", "inappropriate", "privacy",
12
+ "picture_low_quality", "mislocated", "copyright", "other"
13
+ ];
14
+
15
+ /**
16
+ * Report Form displays a form to send issues about pictures.
17
+ * @class Panoramax.components.menus.ReportForm
18
+ * @element pnx-report-form
19
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
20
+ * @example
21
+ * ```html
22
+ * <pnx-report-form ._parent=${viewer} />
23
+ * ```
24
+ */
25
+ export default class ReportForm extends LitElement {
26
+ /** @private */
27
+ static styles = [ titles, faSvg, select, textarea, input, css`
28
+ .group {
29
+ margin-bottom: 10px;
30
+ }
31
+
32
+ .group:not(.group-inline) label {
33
+ margin-bottom: 5px;
34
+ display: inline-block;
35
+ }
36
+ ` ];
37
+
38
+ /** @private */
39
+ static properties = {
40
+ mode: {state: true},
41
+ failure: {state: true},
42
+ };
43
+
44
+ /** @private */
45
+ _onSubmit(e) {
46
+ e.preventDefault();
47
+
48
+ const picMeta = this._parent?.psv?.getPictureMetadata();
49
+ const data = Object.fromEntries(new FormData(e.target));
50
+ const params = {
51
+ issue: data.issue,
52
+ picture_id: data["whole-sequence"] ? null : picMeta?.id,
53
+ reporter_comments: data.comments,
54
+ reporter_email: data.email,
55
+ sequence_id: picMeta?.sequence?.id
56
+ };
57
+
58
+ this.mode = "wait";
59
+
60
+ // Call API
61
+ this._parent?.getAPI().sendReport(params).then(() => {
62
+ this.mode = "success";
63
+ }).catch(e => {
64
+ console.error(e);
65
+ this.mode = "failure";
66
+ this.failure = e;
67
+ });
68
+ }
69
+
70
+ /** @private */
71
+ render() {
72
+ const userAccount = getUserAccount();
73
+
74
+ if(this.mode) {
75
+ return html`
76
+ <h4>${fa(faTriangleExclamation)} ${this._parent?._t.pnx.report}</h4>
77
+ <p>
78
+ ${this.mode === "wait" ? this._parent?._t.pnx.report_wait : nothing}
79
+ ${this.mode === "success" ? this._parent?._t.pnx.report_success : nothing}
80
+ ${this.mode === "failure" ? this._parent?._t.pnx.report_failure.replace("{e}", this.failure) : nothing}
81
+ </p>
82
+ `;
83
+ }
84
+
85
+ return html`
86
+ <h4>${fa(faTriangleExclamation)} ${this._parent?._t.pnx.report}</h4>
87
+
88
+ ${userAccount && this._parent?._t ? html`
89
+ <p>${this._parent?._t.pnx.report_auth.replace("{a}", userAccount.name)}</p>
90
+ ` : nothing}
91
+
92
+ <form @submit=${this._onSubmit.bind(this)}>
93
+ <div class="group">
94
+ <label for="issue">${fa(faCircleInfo)} ${this._parent?._t.pnx.report_nature_label}</label>
95
+ <select name="issue" id="issue" required class="pnx-100w">
96
+ ${REPORT_NATURE.map(nature => html`
97
+ <option
98
+ value=${nature}
99
+ ${nature === "" ? html`disabled selected hidden` : nothing}
100
+ >
101
+ ${this._parent?._t.pnx.report_nature[nature]}
102
+ </option>
103
+ `)}
104
+ </select>
105
+ </div>
106
+
107
+ <div class="group group-inline">
108
+ <input name="whole-sequence" id="whole-sequence" type="checkbox" />
109
+ <label for="whole-sequence">${this._parent?._t.pnx.report_whole_sequence}</label>
110
+ </div>
111
+
112
+ <div class="group">
113
+ <label for="comments">${fa(faCommentDots)} ${this._parent?._t.pnx.report_details}</label>
114
+ <textarea name="comments" id="comments" placeholder=${this._parent?._t.pnx.report_details_placeholder}></textarea>
115
+ </div>
116
+
117
+ ${!userAccount ? html`
118
+ <div class="group">
119
+ <label for="email">${fa(faAt)} ${this._parent?._t.pnx.report_email}</label>
120
+ <input type="email" name="email" id="email" placeholder=${this._parent?._t.pnx.report_email_placeholder} class="pnx-100w" />
121
+ </div>
122
+ `: nothing}
123
+
124
+ <pnx-button type="submit" style="width: 100%">
125
+ ${fa(faPaperPlane)} ${this._parent?._t.pnx.report_submit}
126
+ </pnx-button>
127
+ </form>
128
+ `;
129
+ }
130
+ }
131
+
132
+ customElements.define("pnx-report-form", ReportForm);
@@ -0,0 +1,38 @@
1
+ import { LitElement, html } from "lit";
2
+ import { docPopup, faSvg, titles } from "../styles.js";
3
+ import { fa } from "../../utils/widgets.js";
4
+ import { faExternalLinkAlt, faTags } from "@fortawesome/free-solid-svg-icons";
5
+
6
+ /**
7
+ * Semantics Doc displays a synthetic summary of what are semantics and how to use it.
8
+ * @class Panoramax.components.menus.SemanticsDoc
9
+ * @element pnx-semantics-doc
10
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
11
+ * @example
12
+ * ```html
13
+ * <pnx-semantics-doc ._t=${viewer._t} />
14
+ * ```
15
+ */
16
+ export default class SemanticsDoc extends LitElement {
17
+ static styles = [ titles, faSvg, docPopup ];
18
+
19
+ render() {
20
+ return html`
21
+ <h4>${fa(faTags)} ${this._t?.pnx.semantics_title}</h4>
22
+ <p>${this._t?.pnx.semantics_doc_info1}</p>
23
+ <ul style="padding-left: 1.5em;">
24
+ <li>${this._t?.pnx.semantics_doc_info1u1}</li>
25
+ <li>${this._t?.pnx.semantics_doc_info2}</li>
26
+ </ul>
27
+ <p>${this._t?.pnx.semantics_doc_info3}</p>
28
+ <pnx-link-button
29
+ href="https://docs.panoramax.fr/tags/"
30
+ target="_blank"
31
+ kind="outline"
32
+ style="width: 100%"
33
+ >${fa(faExternalLinkAlt)} ${this._t?.pnx.popup_details_link}</pnx-link-button>
34
+ `;
35
+ }
36
+ }
37
+
38
+ customElements.define("pnx-semantics-doc", SemanticsDoc);