@panoramax/web-viewer 3.2.3-develop-6e69906d → 3.2.3-develop-8b82a4e5

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 (61) hide show
  1. package/build/index.css +2 -2
  2. package/build/index.css.map +1 -1
  3. package/build/index.js +535 -216
  4. package/build/index.js.map +1 -1
  5. package/build/widgets.html +1 -1
  6. package/docs/reference/components/core/PhotoViewer.md +2 -0
  7. package/docs/reference/components/core/Viewer.md +2 -0
  8. package/docs/reference/components/layout/BottomDrawer.md +35 -0
  9. package/docs/reference/components/layout/Tabs.md +45 -0
  10. package/docs/reference/components/menus/PictureLegend.md +1 -0
  11. package/docs/reference/components/ui/Button.md +3 -2
  12. package/docs/reference/components/ui/CopyButton.md +7 -4
  13. package/docs/reference/components/ui/LinkButton.md +1 -0
  14. package/docs/reference/components/ui/ListGroup.md +22 -0
  15. package/docs/reference/components/ui/widgets/Legend.md +11 -0
  16. package/docs/reference/components/ui/widgets/OSMEditors.md +15 -0
  17. package/docs/reference/components/ui/widgets/PictureLegendActions.md +32 -0
  18. package/docs/reference.md +6 -2
  19. package/mkdocs.yml +5 -1
  20. package/package.json +1 -1
  21. package/public/widgets.html +45 -9
  22. package/src/components/core/Basic.css +1 -0
  23. package/src/components/core/PhotoViewer.css +0 -23
  24. package/src/components/core/PhotoViewer.js +41 -22
  25. package/src/components/core/Viewer.css +6 -31
  26. package/src/components/core/Viewer.js +40 -11
  27. package/src/components/layout/BottomDrawer.js +204 -0
  28. package/src/components/layout/CorneredGrid.js +3 -0
  29. package/src/components/layout/Tabs.js +133 -0
  30. package/src/components/layout/index.js +2 -0
  31. package/src/components/menus/PictureLegend.js +162 -23
  32. package/src/components/menus/PictureMetadata.js +220 -110
  33. package/src/components/menus/Share.js +2 -142
  34. package/src/components/styles.js +47 -47
  35. package/src/components/ui/Button.js +4 -2
  36. package/src/components/ui/CopyButton.js +34 -5
  37. package/src/components/ui/LinkButton.js +6 -7
  38. package/src/components/ui/ListGroup.js +66 -0
  39. package/src/components/ui/Map.js +4 -1
  40. package/src/components/ui/QualityScore.js +19 -24
  41. package/src/components/ui/TogglableGroup.js +47 -53
  42. package/src/components/ui/index.js +1 -0
  43. package/src/components/ui/widgets/Legend.js +29 -6
  44. package/src/components/ui/widgets/OSMEditors.js +153 -0
  45. package/src/components/ui/widgets/PictureLegendActions.js +131 -0
  46. package/src/components/ui/widgets/index.js +5 -4
  47. package/src/translations/en.json +14 -8
  48. package/src/translations/fr.json +14 -8
  49. package/src/utils/InitParameters.js +2 -1
  50. package/src/utils/geocoder.js +3 -1
  51. package/src/utils/picture.js +1 -2
  52. package/src/utils/widgets.js +5 -43
  53. package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +12 -32
  54. package/tests/components/core/__snapshots__/Viewer.test.js.snap +5 -25
  55. package/tests/components/ui/__snapshots__/Photo.test.js.snap +6 -2
  56. package/tests/utils/InitParameters.test.js +7 -9
  57. package/tests/utils/__snapshots__/picture.test.js.snap +13 -4
  58. package/tests/utils/picture.test.js +2 -2
  59. package/tests/utils/widgets.test.js +0 -59
  60. package/docs/reference/components/ui/widgets/Share.md +0 -15
  61. package/src/components/ui/widgets/Share.js +0 -30
@@ -1,7 +1,13 @@
1
- import {LitElement, html, nothing, css} from "lit";
2
- import {fa} from "../../utils/widgets";
3
- import { faCircleInfo } from "@fortawesome/free-solid-svg-icons/faCircleInfo";
4
- import { placeholder } from "../styles";
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 { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown";
6
+ import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
7
+ import { faClock } from "@fortawesome/free-solid-svg-icons/faClock";
8
+ import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation";
9
+ import { faShareNodes } from "@fortawesome/free-solid-svg-icons/faShareNodes";
10
+ import { placeholder, panel } from "../styles";
5
11
  import { reverseGeocodingNominatim } from "../../utils/geocoder";
6
12
 
7
13
  /**
@@ -9,6 +15,7 @@ import { reverseGeocodingNominatim } from "../../utils/geocoder";
9
15
  * @class Panoramax.components.menus.PictureLegend
10
16
  * @element pnx-picture-legend
11
17
  * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
18
+ * @slot `editors` External links to map editors, or any tool that may be helpful. Defaults to OSM tools (iD & JOSM).
12
19
  * @example
13
20
  * ```html
14
21
  * <pnx-picture-legend ._parent=${viewer} />
@@ -16,39 +23,119 @@ import { reverseGeocodingNominatim } from "../../utils/geocoder";
16
23
  */
17
24
  export default class PictureLegend extends LitElement {
18
25
  /** @private */
19
- static styles = [placeholder, css`
20
- .addr {
26
+ static styles = [placeholder, panel, css`
27
+ :host {
28
+ overflow-y: auto;
29
+ overflow-x: hidden;
30
+ display: block;
31
+ margin: 0;
32
+ font-family: var(--font-family);
33
+ }
34
+
35
+ @media screen and (min-width: 576px) {
36
+ :host { max-height: 70vh; }
37
+ pnx-picture-metadata { width: 30vw; }
38
+ }
39
+
40
+ .pnx-hidden { display: none !important; }
41
+
42
+ /* Top bar */
43
+ .headline {
44
+ display: flex;
45
+ gap: 10px;
46
+ align-items: center;
47
+ margin: 10px 10px 5px 10px;
48
+ justify-content: space-between;
49
+ }
50
+
51
+ /* Address line */
52
+ #pic-legend-addr {
21
53
  line-height: 1.2em;
22
54
  font-size: 1em;
23
55
  margin-bottom: 2px;
56
+ flex-grow: 5;
57
+ font-weight: 800;
24
58
  }
25
59
 
26
- .addr span {
60
+ #pic-legend-addr span {
27
61
  display: inline-block;
28
62
  height: 100%;
29
63
  width: 100%;
30
64
  }
31
65
 
32
- .context {
33
- font-size: 0.9em;
66
+ /* Minimal info block */
67
+ #pic-legend-info {
68
+ margin: 10px;
34
69
  display: flex;
35
- align-items: center;
36
- justify-content: space-between;
70
+ gap: 10px;
71
+ justify-content: space-around;
72
+ }
73
+ .info-block {
74
+ display: flex;
75
+ flex-shrink: 1;
76
+ gap: 5px;
77
+ font-weight: 600;
78
+ font-size: 0.85em;
79
+ }
80
+ .info-block svg { height: 18px; }
81
+
82
+ /* Expand button */
83
+ #pic-legend-expand {
84
+ display: block;
85
+ margin-top: 5px;
86
+ max-width: 100%;
87
+ }
88
+ #pic-legend-expand::part(btn) {
89
+ border-radius: 10px;
90
+ border-top-right-radius: 0;
91
+ border-top-left-radius: 0;
92
+ }
93
+
94
+ /* Details block */
95
+ pnx-picture-metadata {
96
+ margin: 5px 10px 10px;
97
+ display: block;
98
+ max-width: 450px;
99
+ box-sizing: border-box;
100
+ }
101
+
102
+ /* Details actions */
103
+ #pic-legend-cta {
104
+ display: flex;
105
+ margin: 5px 10px;
106
+ border-bottom-left-radius: 10px;
107
+ border-bottom-right-radius: 10px;
108
+ gap: 5px;
109
+ flex-wrap: wrap;
37
110
  }
38
111
 
39
- pnx-button { float: right; vertical-align: sub; }
112
+ /* More options menu */
113
+ #pnx-legend-opts { min-width: unset; }
114
+
115
+ /* Editors */
116
+ #pic-legend-editors { margin: 0 10px; }
40
117
  `];
41
118
 
42
119
  /** @private */
43
120
  static properties = {
44
121
  _caption: { state: true },
45
122
  _addr: { state: true },
123
+ _expanded: { state: true },
124
+ collapsable: { type: Boolean },
46
125
  };
47
126
 
127
+ /** @private */
128
+ constructor() {
129
+ super();
130
+ this._expanded = true;
131
+ this.collapsable = false;
132
+ }
133
+
48
134
  /** @private */
49
135
  connectedCallback() {
50
136
  super.connectedCallback();
51
137
 
138
+ this._expanded = !this.collapsable;
52
139
  this._prevSearches = {};
53
140
 
54
141
  this._parent.onceReady().then(() => {
@@ -63,6 +150,7 @@ export default class PictureLegend extends LitElement {
63
150
  _onPicChange(picMeta) {
64
151
  clearTimeout(this._addrTimer1);
65
152
  this._caption = picMeta?.caption;
153
+ this._expanded = !this.collapsable;
66
154
 
67
155
  if(picMeta) {
68
156
  const coordsHash = `${picMeta.gps[0]}/${picMeta.gps[1]}`;
@@ -86,24 +174,75 @@ export default class PictureLegend extends LitElement {
86
174
  }
87
175
  }
88
176
 
177
+ /** @private */
178
+ _onBackClick() {
179
+ if(this._expanded && this.collapsable) { this._expanded = false; }
180
+ else { this._parent.select(); }
181
+ }
182
+
89
183
  /** @private */
90
184
  render() {
91
185
  if(!this._caption) { return nothing; }
92
186
 
187
+ const hiddenExpanded = classMap({"pnx-hidden": this._expanded});
188
+ const shownExpanded = classMap({"pnx-hidden": !this._expanded});
189
+
93
190
  return html`
94
- <div class="addr">
95
- ${this._addr?.length > 0 ? this._addr : html`<span class="pnx-placeholder-loading">&nbsp;</span>`}
96
- </div>
97
- <div class="context">
98
- ${this._caption.producer ? this._caption.producer : nothing}
99
- ${this._caption.producer && this._caption.date ? html`-` : nothing}
100
- ${this._caption.date ? this._caption.date.toLocaleDateString(undefined, { year: "numeric", month: "long", day: "numeric" }) : nothing}
191
+ <div class="headline">
101
192
  <pnx-button
102
- kind="outline"
103
- title=${this._parent?._t.pnx.legend_title}
104
- @click=${() => this._parent?._showPictureMetadata()}
105
- >${fa(faCircleInfo, { styles: { height: "12px" }})}</pnx-button>
193
+ kind="superinline"
194
+ @click=${this._onBackClick}
195
+ >
196
+ ${fa(faArrowLeft)}
197
+ </pnx-button>
198
+
199
+ <div id="pic-legend-addr">
200
+ ${this._addr?.length > 0 ? this._addr : html`<span class="pnx-placeholder-loading">&nbsp;</span>`}
201
+ </div>
202
+
203
+ <pnx-picture-legend-actions
204
+ @click=${e => e.stopPropagation()}
205
+ ._parent=${this._parent}
206
+ ?full=${this._expanded}
207
+ ></pnx-picture-legend-actions>
208
+ </div>
209
+
210
+ <div id="pic-legend-info" class=${hiddenExpanded}>
211
+ ${this._caption.producer?.length > 0 ? html`<div class="info-block">
212
+ ${fa(faUser)}
213
+ ${this._caption.producer[this._caption.producer.length-1]}
214
+ </div>` : nothing}
215
+
216
+ ${this._caption.date ? html`<div class="info-block">
217
+ ${fa(faClock)}
218
+ ${this._caption.date.toLocaleDateString(undefined, { year: "numeric", month: "long" })}
219
+ </div>` : nothing}
220
+ </div>
221
+
222
+ <div id="pic-legend-cta" class=${shownExpanded}>
223
+ ${this._parent.api._endpoints.report ? html`
224
+ <pnx-button size="sm" @click=${() => this._parent._showReportForm()}>
225
+ ${fa(faTriangleExclamation)} ${this._parent?._t.pnx.report}
226
+ </pnx-button>
227
+ ` : nothing}
228
+
229
+ <pnx-button size="sm" @click=${() => this._parent._showShareOptions()}>
230
+ ${fa(faShareNodes)} ${this._parent?._t.pnx.share}
231
+ </pnx-button>
232
+
233
+ <slot name="editors">
234
+ <pnx-widget-osmeditors ._parent=${this._parent} />
235
+ </slot>
106
236
  </div>
237
+
238
+ ${this.collapsable ? html`<pnx-button
239
+ kind="full"
240
+ id="pic-legend-expand"
241
+ class=${hiddenExpanded}
242
+ @click=${() => this._expanded = true}
243
+ >${fa(faChevronDown)}</pnx-button>` : nothing}
244
+
245
+ <pnx-picture-metadata class=${shownExpanded} ._parent=${this._parent}></pnx-picture-metadata>
107
246
  `;
108
247
  }
109
248
  }
@@ -1,20 +1,24 @@
1
- import { LitElement, html, nothing } from "lit";
1
+ import { LitElement, nothing, css } from "lit";
2
+ import { html, unsafeStatic } from "lit/static-html.js";
2
3
  import { fa } from "../../utils/widgets";
3
- import { faCircleInfo } from "@fortawesome/free-solid-svg-icons/faCircleInfo";
4
- import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons/faTriangleExclamation";
5
4
  import { faLocationDot } from "@fortawesome/free-solid-svg-icons/faLocationDot";
6
5
  import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal";
7
- import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
8
6
  import { faCamera } from "@fortawesome/free-solid-svg-icons/faCamera";
9
- import { faGear } from "@fortawesome/free-solid-svg-icons/faGear";
10
- import { titles, tables, expandable } from "../styles";
11
- import { createTable, createLinkCell, createWebComp } from "../../utils/widgets";
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";
12
14
  import { getGPSPrecision } from "../../utils/picture";
13
15
  import {
14
16
  getGrade, QUALITYSCORE_GPS_VALUES, QUALITYSCORE_RES_360_VALUES,
15
17
  QUALITYSCORE_RES_FLAT_VALUES, QUALITYSCORE_POND_GPS, QUALITYSCORE_POND_RES
16
18
  } from "../../utils/utils";
17
19
 
20
+ const missing = () => fa(faQuestion, {styles: {height: "16px"}});
21
+
18
22
  /**
19
23
  * Picture metadata displays detailed info about a single picture (ID, capture context, EXIF attributes...).
20
24
  * @class Panoramax.components.menus.PictureMetadata
@@ -27,7 +31,41 @@ import {
27
31
  */
28
32
  export default class PictureMetadata extends LitElement {
29
33
  /** @private */
30
- static styles = [ titles, tables, expandable ];
34
+ static styles = [ titles, textarea, css`
35
+ h4[slot="title"] {
36
+ margin: 0;
37
+ justify-content: center;
38
+ font-size: 0.8em;
39
+ line-height: 2em;
40
+ padding: 0.5em 0;
41
+ }
42
+ h4[slot="title"] svg.svg-inline--fa {
43
+ height: 14px;
44
+ }
45
+ div[slot="content"] {
46
+ padding: 5px 10px;
47
+ background-color: #ededed;
48
+ }
49
+
50
+ /* Small data blocks */
51
+ /* .data-blocks { display: flex; } */
52
+ .data-block {
53
+ display: inline-block;
54
+ min-width: 50%;
55
+ margin: 5px 0;
56
+ box-sizing: border-box;
57
+ vertical-align: top;
58
+ }
59
+ .data-block h5 {
60
+ font-size: 0.8em;
61
+ font-weight: 400;
62
+ color: var(--blue-dark);
63
+ margin: 0 0 5px 0;
64
+ }
65
+ .data-block div {
66
+ font-size: 1em;
67
+ }
68
+ ` ];
31
69
 
32
70
  /** @private */
33
71
  static properties = {
@@ -44,77 +82,74 @@ export default class PictureMetadata extends LitElement {
44
82
  });
45
83
  }
46
84
 
85
+ /** @private */
86
+ _toTab(title, data) {
87
+ return html`
88
+ <h4 slot="title">${title}</h4>
89
+ <div slot="content" class="data-blocks">
90
+ ${data.filter(b => b).map(b => html`<div class="data-block" style=${b.style}>
91
+ <h5>${b.title}</h5>
92
+ <div style=${b.content_style}>${b.content}</div>
93
+ </div>`)}
94
+ </div>
95
+ `;
96
+ }
97
+
47
98
  /** @private */
48
99
  render() {
49
100
  /* eslint-disable indent */
50
101
  if(!this._meta) { return nothing; }
51
102
 
52
- // General metadata
53
- const generalData = [
54
- {
55
- section: this._parent?._t.pnx.metadata_general_picid,
56
- classes: ["pnx-td-with-id"],
57
- values: createLinkCell(
58
- this._meta.id,
59
- this._parent.api.getPictureMetadataUrl(this._meta.id, this._meta?.sequence?.id),
60
- this._parent?._t.pnx.metadata_general_picid_link,
61
- this._parent?._t
62
- )
63
- },
64
- {
65
- section: this._parent?._t.pnx.metadata_general_seqid,
66
- classes: ["pnx-td-with-id"],
67
- values: createLinkCell(
68
- this._meta?.sequence?.id,
69
- this._parent.api.getSequenceMetadataUrl(this._meta?.sequence?.id),
70
- this._parent?._t.pnx.metadata_general_seqid_link,
71
- this._parent?._t
72
- )
73
- },
74
- { section: this._parent?._t.pnx.metadata_general_author, value: this._meta?.caption?.producer },
75
- { section: this._parent?._t.pnx.metadata_general_license, value: this._meta?.caption?.license },
76
- {
77
- section: this._parent?._t.pnx.metadata_general_date,
78
- value: this._meta?.caption?.date?.toLocaleDateString(undefined, {
79
- year: "numeric", month: "long", day: "numeric",
80
- hour: "numeric", minute: "numeric", second: "numeric",
81
- fractionalSecondDigits: 3, timeZoneName: "short"
82
- })
83
- },
84
- ];
85
-
86
- // Camera details
87
- const focal = this._meta?.properties?.["pers:interior_orientation"]?.focal_length ? `${this._meta?.properties?.["pers:interior_orientation"]?.focal_length} mm` : "❓";
88
- let resmp = this._meta?.properties?.["pers:interior_orientation"]?.["sensor_array_dimensions"];
103
+ // Generic information
104
+ const persOrient = this._meta?.properties?.["pers:interior_orientation"];
105
+ const makeModel = [persOrient.camera_manufacturer, persOrient.camera_model].filter(v => v).join(" ");
106
+ const focal = persOrient?.focal_length ? `${persOrient?.focal_length} mm` : missing();
107
+ let resmp = persOrient?.["sensor_array_dimensions"];
89
108
  if(resmp) { resmp = `${resmp[0]} x ${resmp[1]} px (${Math.floor(resmp[0] * resmp[1] / 1000000)} Mpx)`;}
90
109
  let pictype = this._parent?._t.pnx.picture_flat;
91
- let picFov = this._meta?.properties?.["pers:interior_orientation"]?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up
110
+ let pictypelong = this._parent?._t.pnx.picture_flat_long;
111
+ let picFov = persOrient?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up
92
112
  if(picFov !== null && picFov !== undefined) {
93
- if(picFov === 360) { pictype = this._parent?._t.pnx.picture_360; }
113
+ if(picFov === 360) {
114
+ pictype = this._parent?._t.pnx.picture_360;
115
+ pictypelong = this._parent?._t.pnx.picture_360_long;
116
+ }
94
117
  else { pictype += ` (${picFov}°)`; }
95
118
  }
96
119
 
120
+ // Camera tab
97
121
  const cameraData = [
98
- { section: this._parent?._t.pnx.metadata_camera_make, value: this._meta?.properties?.["pers:interior_orientation"]?.camera_manufacturer || "❓" },
99
- { section: this._parent?._t.pnx.metadata_camera_model, value: this._meta?.properties?.["pers:interior_orientation"]?.camera_model || "❓" },
100
- { section: this._parent?._t.pnx.metadata_camera_type, value: pictype },
101
- { section: this._parent?._t.pnx.metadata_camera_resolution, value: resmp || "❓" },
102
- { section: this._parent?._t.pnx.metadata_camera_focal_length, value: focal },
122
+ { title: this._parent?._t.pnx.metadata_camera_make, content: persOrient?.camera_manufacturer || missing() },
123
+ { title: this._parent?._t.pnx.metadata_camera_model, content: persOrient?.camera_model || missing() },
124
+ { title: this._parent?._t.pnx.metadata_camera_type, content: pictype },
125
+ { title: this._parent?._t.pnx.metadata_camera_resolution, content: resmp || missing() },
126
+ { title: this._parent?._t.pnx.metadata_camera_focal_length, content: focal },
127
+ // Capture date
128
+ this._meta?.caption?.date && {
129
+ title: this._parent?._t.pnx.metadata_general_date,
130
+ content: html`
131
+ <strong>${new Intl.DateTimeFormat(undefined, {dateStyle: "short"}).format(this._meta.caption.date)}</strong>
132
+ <br />${new Intl.DateTimeFormat(undefined, {hour: "numeric", minute: "numeric", second: "numeric", fractionalSecondDigits: 3, timeZoneName: "longOffset"}).format(this._meta.caption.date)}
133
+ `
134
+ }
103
135
  ];
104
136
 
105
- // Location details
106
- const orientation = this._meta?.properties?.["view:azimuth"] !== undefined ? `${this._meta.properties["view:azimuth"]}°` : "❓";
107
- const gpsPrecisionLabel = getGPSPrecision(this._meta);
137
+ // Location tab
138
+ const orientation = this._meta?.properties?.["view:azimuth"] !== undefined && `${this._meta.properties["view:azimuth"]}°`;
108
139
  const locationData = [
109
- { section: this._parent?._t.pnx.metadata_location_longitude, value: this._meta.gps[0] },
110
- { section: this._parent?._t.pnx.metadata_location_latitude, value: this._meta.gps[1] },
111
- { section: this._parent?._t.pnx.metadata_location_orientation, value: orientation },
112
- { section: this._parent?._t.pnx.metadata_location_precision, value: gpsPrecisionLabel },
140
+ { title: this._parent?._t.pnx.metadata_location_longitude, content: this._meta.gps[0] },
141
+ { title: this._parent?._t.pnx.metadata_location_latitude, content: this._meta.gps[1] },
142
+ { title: this._parent?._t.pnx.metadata_location_orientation, content: orientation || missing() },
143
+ { title: this._parent?._t.pnx.metadata_location_precision, content: getGPSPrecision(this._meta) || missing() },
113
144
  ];
114
145
 
115
- // Picture quality level
116
- const hasQualityScore = this._parent?.map?._hasQualityScore?.();
117
- let qualityData;
146
+ // Quality tab
147
+ const hasQualityScore = (
148
+ this._parent?.map?._hasQualityScore?.()
149
+ || this._meta?.properties?.["quality:horizontal_accuracy"]
150
+ || this._meta?.properties?.["panoramax:horizontal_pixel_density"]
151
+ );
152
+ let qualityData, generalGrade;
118
153
  if(hasQualityScore) {
119
154
  const gpsGrade = getGrade(QUALITYSCORE_GPS_VALUES, this._meta?.properties?.["quality:horizontal_accuracy"]);
120
155
  const resGrade = getGrade(
@@ -123,39 +158,13 @@ export default class PictureMetadata extends LitElement {
123
158
  );
124
159
 
125
160
  // Note: score is also calculated in utils/map code
126
- const generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS);
161
+ generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS);
127
162
 
128
163
  qualityData = [
129
- { section: this._parent?._t.pnx.metadata_quality_score, value: createWebComp("pnx-quality-score", { grade: generalGrade }) },
130
- { section: this._parent?._t.pnx.metadata_quality_gps_score, value: createWebComp("pnx-grade", { stars: gpsGrade, _t: this._parent?._t }) },
131
- { section: this._parent?._t.pnx.metadata_quality_resolution_score, value: createWebComp("pnx-grade", { stars: resGrade, _t: this._parent?._t }) },
132
- ];
133
- }
134
-
135
- return html`
136
- <h4>${fa(faCircleInfo)} ${this._parent?._t.pnx.metadata}</h4>
137
-
138
- ${this._parent.api._endpoints.report ?
139
- html`
140
- <pnx-button ._t=${this._parent?._t} @click=${() => this._parent._showReportForm()}>
141
- ${fa(faTriangleExclamation)} ${this._parent?._t.pnx.report}
142
- </pnx-button>
143
- ` :
144
- nothing
145
- }
146
-
147
- ${createTable("pnx-table-light", generalData)}
148
-
149
- <h4>${fa(faCamera)} ${this._parent?._t.pnx.metadata_camera}</h4>
150
- ${createTable("pnx-table-light", cameraData)}
151
-
152
- <h4>${fa(faLocationDot)} ${this._parent?._t.pnx.metadata_location}</h4>
153
- ${createTable("pnx-table-light", locationData)}
154
-
155
- ${hasQualityScore ?
156
- html`
157
- <h4 style="margin-bottom: 5px">
158
- ${fa(faMedal)} ${this._parent?._t.pnx.metadata_quality}
164
+ {
165
+ title: this._parent?._t.pnx.metadata_quality_score,
166
+ content: html`<div style="display: flex; justify-content: center; gap: 10px; align-items: center;">
167
+ <pnx-quality-score grade=${generalGrade} style="font-size: 16px"></pnx-quality-score>
159
168
  <pnx-button
160
169
  title="${this._parent?._t.pnx.metadata_quality_help}"
161
170
  kind="outline"
@@ -163,25 +172,126 @@ export default class PictureMetadata extends LitElement {
163
172
  >
164
173
  ${fa(faInfoCircle)}
165
174
  </pnx-button>
166
- </h4>
167
- ${createTable("pnx-table-light", qualityData)}
168
- ` :
169
- nothing
170
- }
175
+ </div>`,
176
+ style: "width: 100%"
177
+ },
178
+ {
179
+ title: this._parent?._t.pnx.metadata_quality_gps_score,
180
+ content: createWebComp("pnx-grade", { stars: gpsGrade, _t: this._parent?._t })
181
+ },
182
+ {
183
+ title: this._parent?._t.pnx.metadata_quality_resolution_score,
184
+ content: createWebComp("pnx-grade", { stars: resGrade, _t: this._parent?._t })
185
+ },
186
+ ];
187
+ }
188
+
189
+ // EXIF data
190
+ const exifData = Object.entries(this._meta.properties.exif)
191
+ .sort()
192
+ .filter(([, value]) => value)
193
+ .map(([key, value]) => {
194
+ if(JSON.stringify(value).includes("\\u")) {
195
+ value = JSON.stringify(value).replace(/\\u[0-9A-Fa-f]{4}/g, unicode => (
196
+ " 0x" + parseInt(unicode.slice(2), 16).toString(16).toUpperCase().padStart(4, "0")
197
+ )).slice(1, -1).trim();
198
+ }
199
+ return {
200
+ title: key,
201
+ content: value.length > 30 ? html`<textarea readonly>${value}</textarea>`: value,
202
+ style: value.length > 30 ? "width: 100%" : undefined
203
+ };
204
+ });
171
205
 
172
- ${this._meta.properties?.exif ?
173
- html`
174
- <details>
175
- <summary>${fa(faGear)} ${this._parent?._t.pnx.metadata_exif}</summary>
176
- ${createTable("", Object.entries(this._meta.properties.exif)
177
- .sort()
178
- .map(([key, value]) => ({ section: key, value: value })
179
- ))}
180
- </details>
181
- ` :
182
- nothing
206
+ // General metadata
207
+ const overview = [
208
+ // Producer
209
+ this._meta?.caption?.producer?.length > 0 && {
210
+ title: this._parent?._t.pnx.metadata_general_author,
211
+ content: html`
212
+ <strong>${this._meta.caption.producer[this._meta.caption.producer.length - 1]}</strong>
213
+ ${this._meta.caption.producer.length > 1 ? html`<br />${this._meta.caption.producer.slice(0, -1).join(", ")}` : nothing}
214
+ `
215
+ },
216
+ // Capture date
217
+ this._meta?.caption?.date && {
218
+ title: this._parent?._t.pnx.metadata_general_date,
219
+ content: html`
220
+ <strong>${new Intl.DateTimeFormat(undefined, {dateStyle: "long"}).format(this._meta.caption.date)}</strong>
221
+ <br />${new Intl.DateTimeFormat(undefined, {hour: "numeric",minute:"numeric"}).format(this._meta.caption.date)}
222
+ `
223
+ },
224
+ // Camera
225
+ persOrient && {
226
+ title: this._parent?._t.pnx.metadata_camera,
227
+ content: html`
228
+ <strong>${makeModel.length > 0 ? makeModel : missing()}</strong>
229
+ <br />${pictypelong}
230
+ `
231
+ },
232
+ // License
233
+ this._meta?.caption?.license && {
234
+ title: this._parent?._t.pnx.metadata_general_license,
235
+ content: html`${unsafeStatic(this._meta.caption.license)}`
236
+ },
237
+ // Quality score
238
+ hasQualityScore && {
239
+ title: this._parent?._t.pnx.metadata_quality,
240
+ content: html`<pnx-quality-score grade=${generalGrade} style="font-size: 14px" />`
241
+ },
242
+ // Copy ID
243
+ {
244
+ title: this._parent?._t.pnx.metadata_general_copy_id,
245
+ content_style: "display: flex; gap: 5px;",
246
+ content: html`
247
+ <pnx-copy-button
248
+ kind="outline"
249
+ size="sm"
250
+ ._t=${this._parent?._t}
251
+ text=${this._meta.id}
252
+ style="flex: 1"
253
+ >
254
+ ${fa(faImage)} ${this._parent?._t.pnx.metadata_general_picid}
255
+ </pnx-copy-button>
256
+ <pnx-copy-button
257
+ kind="outline"
258
+ size="sm"
259
+ ._t=${this._parent?._t}
260
+ text=${this._meta.sequence.id}
261
+ style="flex: 1"
262
+ >
263
+ ${fa(faImages)} ${this._parent?._t.pnx.metadata_general_seqid}
264
+ </pnx-copy-button>
265
+ `
183
266
  }
184
- `;
267
+ ];
268
+
269
+ return html`<pnx-tabs>
270
+ ${this._toTab( // General
271
+ html`${fa(faImage)} ${this._parent?._t.pnx.metadata_summary}`,
272
+ overview
273
+ )}
274
+
275
+ ${this._toTab( // Camera
276
+ html`${fa(faCamera)} ${this._parent?._t.pnx.metadata_camera}`,
277
+ cameraData
278
+ )}
279
+
280
+ ${this._toTab( // Position
281
+ html`${fa(faLocationDot)} ${this._parent?._t.pnx.metadata_location}`,
282
+ locationData
283
+ )}
284
+
285
+ ${hasQualityScore ? this._toTab( // Quality
286
+ html`${fa(faMedal)} ${this._parent?._t.pnx.metadata_quality}`,
287
+ qualityData
288
+ ) : nothing}
289
+
290
+ ${this._meta.properties?.exif ? this._toTab( // EXIF
291
+ html`${fa(faScroll)} ${this._parent?._t.pnx.metadata_exif}`,
292
+ exifData
293
+ ) : nothing}
294
+ </pnx-tabs>`;
185
295
  }
186
296
  }
187
297