@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,374 @@
1
+ import { LitElement, css, html, nothing } from "lit";
2
+ import { classMap } from "lit/directives/class-map.js";
3
+ import { map } from "lit/directives/map.js";
4
+ import { fa, listenForMenuClosure } from "../../utils/widgets.js";
5
+ import { faSvg } from "../styles.js";
6
+ import {
7
+ faCircleExclamation, faCircleNotch, faMagnifyingGlass, faXmark
8
+ } from "@fortawesome/free-solid-svg-icons";
9
+
10
+
11
+ /**
12
+ * Search Bar Element displays an interactive search widget.
13
+ * @class Panoramax.components.ui.SearchBar
14
+ * @element pnx-search-bar
15
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
16
+ * @fires Panoramax.components.ui.SearchBar#result
17
+ * @example
18
+ * ```html
19
+ * <pnx-search-bar
20
+ * id="my-search-bar"
21
+ * placeholder="Search something..."
22
+ * .searcher=${mysearchfct}
23
+ * ._parent=${viewer}
24
+ * reduceable="false"
25
+ * reduced="false"
26
+ * size="xxl" @result=${e => console.log(e.detail)}
27
+ * >
28
+ * <!-- Optional icon for display on left-side of search bar -->
29
+ * </pnx-search-bar>
30
+ * ```
31
+ */
32
+ export default class SearchBar extends LitElement {
33
+ /** @private */
34
+ static styles = [ faSvg, css`
35
+ /* Container */
36
+ .sb {
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ gap: 5px;
41
+ border: 1px solid var(--widget-border-div);
42
+ border-radius: 5px;
43
+ background-color: var(--widget-bg);
44
+ color: var(--widget-font);
45
+ height: 30px;
46
+ border-radius: 20px;
47
+ position: relative;
48
+ padding: 0 0 0 10px;
49
+ width: fit-content;
50
+ max-width: 100%;
51
+ box-sizing: border-box;
52
+ font-family: var(--font-family);
53
+ }
54
+ .sb.sb-xxl {
55
+ height: 40px;
56
+ line-height: 40px;
57
+ }
58
+ .sb.sb-reduceable {
59
+ width: 360px;
60
+ }
61
+ .sb.sb-reduceable.sb-reduced {
62
+ width: fit-content;
63
+ padding: 0;
64
+ gap: 0;
65
+ }
66
+
67
+ /* Text field */
68
+ .sb input {
69
+ background: none;
70
+ border: none !important;
71
+ outline: none;
72
+ height: 20px;
73
+ width: 100%;
74
+ font-family: var(--font-family);
75
+ }
76
+ .sb.sb-xxl input {
77
+ font-size: 1.1em;
78
+ }
79
+ .sb.sb-reduceable.sb-reduced input {
80
+ display: none;
81
+ }
82
+
83
+ /* Status icon */
84
+ .sb-icon {
85
+ cursor: pointer;
86
+ height: 30px;
87
+ line-height: 30px;
88
+ width: 30px;
89
+ min-width: 30px;
90
+ text-align: center;
91
+ }
92
+ .sb.sb-xxl .sb-icon {
93
+ height: 40px;
94
+ line-height: 40px;
95
+ width: 40px;
96
+ min-width: 40px;
97
+ }
98
+ .sb-icon svg {
99
+ pointer-events: none;
100
+ width: 14px;
101
+ height: 14px;
102
+ }
103
+
104
+ /* Search results */
105
+ .sb-results {
106
+ position: absolute;
107
+ top: 35px;
108
+ list-style: none;
109
+ margin: 0;
110
+ padding: 0;
111
+ max-width: calc(100% - 20px);
112
+ border: 1px solid var(--widget-border-div);
113
+ border-radius: 10px;
114
+ background-color: var(--widget-bg);
115
+ color: var(--widget-font);
116
+ z-index: 130;
117
+ font-size: 1.05em;
118
+ line-height: normal;
119
+ font-family: var(--font-family);
120
+ max-height: 50vh;
121
+ overflow-y: auto;
122
+ }
123
+ .sb.sb-xxl .sb-results {
124
+ top: 45px;
125
+ }
126
+ .sb-result,
127
+ .sb-empty {
128
+ display: block;
129
+ padding: 5px 15px;
130
+ white-space: nowrap;
131
+ overflow: hidden;
132
+ text-overflow: ellipsis;
133
+ cursor: pointer;
134
+ border-radius: 0;
135
+ }
136
+ .sb-result:hover {
137
+ background-color: var(--widget-bg-hover);
138
+ }
139
+ .sb-result:first-child {
140
+ border-top-right-radius: 10px;
141
+ border-top-left-radius: 10px;
142
+ padding-top: 15px;
143
+ }
144
+ .sb-result:last-child {
145
+ border-bottom-right-radius: 10px;
146
+ border-bottom-left-radius: 10px;
147
+ padding-bottom: 15px;
148
+ }
149
+ ` ];
150
+
151
+ /**
152
+ * Component properties.
153
+ * @memberof Panoramax.components.ui.SearchBar#
154
+ * @type {Object}
155
+ * @property {string} [id] The ID attribute set on component and prefix for input as well
156
+ * @property {string} [placeholder] Default text to display on empty field
157
+ * @property {boolean} [reduceable=false] Can the search bar be collapsed (for mobile view)
158
+ * @property {boolean} [reduced=false] Is the search bar currently collapsed ?
159
+ * @property {string} [value] The default input value
160
+ * @property {string} [size=md] The component sizing (md, xxl)
161
+ * @property {function} [searcher] Search callback, function that takes as parameter the input text value, and resolves on list of results ({title, subtitle} and any other data you'd like to get on validation)
162
+ * @property {boolean} [no-menu-closure=false] Set to true to ignore menu closure events
163
+ */
164
+ static properties = {
165
+ id: {type: String},
166
+ placeholder: {type: String},
167
+ reduceable: {type: Boolean, reflect: true},
168
+ reduced: {type: Boolean, reflect: true},
169
+ value: {type: String},
170
+ size: {type: String},
171
+ _icon: {state: true},
172
+ _results: {state: true},
173
+ searcher: {type: Function},
174
+ "no-menu-closure": {type: Boolean},
175
+ };
176
+
177
+ constructor() {
178
+ super();
179
+
180
+ // State properties
181
+ this.reduceable = false;
182
+ this.reduced = false;
183
+ this.placeholder = nothing;
184
+ this.size = "md";
185
+ this._icon = "search";
186
+ this._results = null;
187
+ this["no-menu-closure"] = false;
188
+
189
+ // Other properties
190
+ this._throttler = null;
191
+ delete this._lastSearch;
192
+ }
193
+
194
+ /** @private */
195
+ connectedCallback() {
196
+ super.connectedCallback();
197
+ if(!this["no-menu-closure"]) { listenForMenuClosure(this, this.reset.bind(this)); }
198
+ }
199
+
200
+ /** @private */
201
+ attributeChangedCallback(name, _old, value) {
202
+ super.attributeChangedCallback(name, _old, value);
203
+
204
+ if(name === "value") {
205
+ if([null, undefined, "", "null"].includes(value)) {
206
+ this._icon = "search";
207
+ }
208
+ else {
209
+ this._icon = "empty";
210
+ }
211
+ }
212
+ }
213
+
214
+ /** @private */
215
+ _onIconClick() {
216
+ if(["empty", "warn"].includes(this._icon)) {
217
+ this.reset();
218
+ }
219
+ if(this.reduceable && this._icon == "search") {
220
+ this.reduced = !this.reduced;
221
+ }
222
+ }
223
+
224
+ /** @private */
225
+ _onInputChange(e) {
226
+ this.value = e.target.value;
227
+ this._throttledSearch();
228
+ }
229
+
230
+ /** @private */
231
+ _onResultClick(item) {
232
+ /**
233
+ * Event for search result clicked
234
+ * @event Panoramax.components.ui.SearchBar#result
235
+ * @type {CustomEvent}
236
+ * @property {object|null} detail The data associated to clicked item (format based on searcher function results), or null on reset
237
+ */
238
+ this.dispatchEvent(new CustomEvent("result", {bubbles: true, composed: true, detail: item}));
239
+
240
+ this._results = null;
241
+ if(this._throttler) {
242
+ clearTimeout(this._throttler);
243
+ this._throttler = null;
244
+ }
245
+
246
+ if(!this.reduceable && item) {
247
+ this.value = item?.title;
248
+ this._icon = "empty";
249
+ }
250
+ else {
251
+ this.value = "";
252
+ this._icon = "search";
253
+ if(this.reduceable) { this.reduced = true; }
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Limit search calls to every 500ms
259
+ * @private
260
+ */
261
+ _throttledSearch() {
262
+ if(this._throttler) {
263
+ clearTimeout(this._throttler);
264
+ delete this._throttler;
265
+ }
266
+
267
+ this._throttler = setTimeout(this._search.bind(this), 500);
268
+ }
269
+
270
+ /**
271
+ * Perform real search
272
+ * @private
273
+ */
274
+ _search() {
275
+ if(!this.value || this.value.length == 0) {
276
+ this.reset();
277
+ return;
278
+ }
279
+
280
+ if(!this.searcher) {
281
+ console.warn("No search handler defined");
282
+ return;
283
+ }
284
+
285
+ this._icon = "loading";
286
+ this._results = null;
287
+
288
+ this.searcher(this.value).then(data => {
289
+ if(this._icon !== "loading") { return; }
290
+
291
+ this._icon = "empty";
292
+ if(!data || data.length == 0) {
293
+ this._results = [];
294
+ }
295
+ else if(data === true) {
296
+ this._results = null;
297
+ }
298
+ else {
299
+ this._results = data;
300
+ if(!this["no-menu-closure"]) {
301
+ this._parent?.dispatchEvent(new CustomEvent("menu-opened", { detail: { menu: this }}));
302
+ }
303
+ }
304
+ }).catch(e => {
305
+ console.error(e);
306
+ this._icon = "warn";
307
+ });
308
+ }
309
+
310
+ /**
311
+ * Empty results list and reset search bar content.
312
+ */
313
+ reset() {
314
+ this._onResultClick(null);
315
+ }
316
+
317
+ /** @private */
318
+ render() {
319
+ const classes = {
320
+ sb: true,
321
+ "sb-xxl": this.size === "xxl",
322
+ "sb-reduceable": this.reduceable,
323
+ "sb-reduced": this.reduced,
324
+ };
325
+
326
+ return html`<div
327
+ id=${this.id}
328
+ class=${classMap(classes)}
329
+ part="container"
330
+ >
331
+ <slot name="pre" class=${classMap({"sb-reduced": this.reduced})}></slot>
332
+
333
+ <input
334
+ id="${this.id}-input"
335
+ type="text"
336
+ placeholder=${this.placeholder}
337
+ autocomplete="off"
338
+ @change=${this._onInputChange.bind(this)}
339
+ @keypress=${this._onInputChange.bind(this)}
340
+ @paste=${this._onInputChange.bind(this)}
341
+ @input=${this._onInputChange.bind(this)}
342
+ .value=${this.value || ""}
343
+ part="input"
344
+ />
345
+
346
+ <span
347
+ class="sb-icon"
348
+ @click=${this._onIconClick.bind(this)}
349
+ >
350
+ ${this._icon == "search" ? fa(faMagnifyingGlass) : nothing}
351
+ ${this._icon == "loading" ? fa(faCircleNotch, { classes: ["fa-spin"] }) : nothing}
352
+ ${this._icon == "empty" ? fa(faXmark) : nothing}
353
+ ${this._icon == "warn" ? fa(faCircleExclamation) : nothing}
354
+ </span>
355
+
356
+ ${!this.reduced && this._results ? html`
357
+ <div class="sb-results">
358
+ ${this._results.length === 0 ? html`
359
+ <div class="sb-empty">${this._parent?._t?.pnx.search_empty}</div>
360
+ ` : nothing}
361
+
362
+ ${map(this._results, i => html`
363
+ <div class="sb-result" @click=${() => this._onResultClick(i)}>
364
+ ${i.title}
365
+ ${i.subtitle && i.subtitle != "" ? html`<br /><small>${i.subtitle}</small>` : nothing}
366
+ </div>
367
+ `)}
368
+ </div>
369
+ ` : nothing}
370
+ </div>`;
371
+ }
372
+ }
373
+
374
+ customElements.define("pnx-search-bar", SearchBar);
@@ -0,0 +1,191 @@
1
+ import { LitElement, css, html } from "lit";
2
+ import Basic from "../core/Basic.js";
3
+ import { computeDiffTags, parseSemanticsString, semanticsToString } from "../../utils/semantics.js";
4
+ import { textarea } from "../styles.js";
5
+ import JSON5 from "json5";
6
+
7
+ /**
8
+ * Semantics Editor offer an easy-to-use input for adding or editing semantics tags.
9
+ *
10
+ * It manipulates list of `{key: "mypanokey", value: "myvalue"}` entries through `semantics` attribute.
11
+ *
12
+ * @class Panoramax.components.ui.SemanticsEditor
13
+ * @element pnx-semantics-editor
14
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
15
+ * @fires Panoramax.components.ui.SemanticsEditor#change
16
+ * @example
17
+ * ```html
18
+ * <!-- Basic example -->
19
+ * <pnx-semantics-editor
20
+ * id="editor"
21
+ * semantics=${mypic.semantics}
22
+ * ._t=${viewer._t}
23
+ * onchange=${e => console.log(e.detail.semantics)}
24
+ * />
25
+ *
26
+ * <!-- You can access editor and check its validity through native web browser functions -->
27
+ * <script>
28
+ * const editor = document.getElementById("editor");
29
+ * console.log(editor.checkValidity()); // True if input is valid
30
+ * </script>
31
+ *
32
+ * <!-- You can change specifically style of textarea -->
33
+ * <style>
34
+ * pnx-semantics-editor::part(text) {
35
+ * color: blue;
36
+ * }
37
+ * </style>
38
+ * ```
39
+ */
40
+ export default class SemanticsEditor extends LitElement {
41
+ static styles = [textarea, css`
42
+ textarea:invalid {
43
+ border: 2px solid var(--red);
44
+ background-color:var(--beige);
45
+ }
46
+ `];
47
+
48
+ /**
49
+ * Component properties.
50
+ * @memberof Panoramax.components.ui.SemanticsEditor#
51
+ * @type {Object}
52
+ * @property {object} [semantics] The `semantics` field of a picture or annotation feature. It is updated when field changes, but reflect the whole list of new tags (not delta needed by API). If you want delta, please use getDiff function.
53
+ * @property {number} [rows=3] The amount of rows shown for textarea
54
+ */
55
+ static properties = {
56
+ semantics: {converter: Basic.GetJSONConverter(), reflect: true},
57
+ rows: {type: Number},
58
+ _valid: {state: true},
59
+ _t: {converter: Basic.GetJSONConverter()},
60
+ _firstSemantics: {state: true},
61
+ };
62
+
63
+ constructor() {
64
+ super();
65
+ this.semantics = null;
66
+ this._valid = true;
67
+ this._firstSemantics = null;
68
+ this.rows = 3;
69
+ }
70
+
71
+ /** @private */
72
+ connectedCallback() {
73
+ super.connectedCallback();
74
+ this._firstSemantics = this.semantics === null ? [] : JSON5.parse(JSON5.stringify(this.semantics));
75
+ }
76
+
77
+ /**
78
+ * Get current delta between initial tags and user changes.
79
+ * @memberof Panoramax.components.ui.SemanticsEditor#
80
+ * @returns {object[]} The list of tag changes (in API format)
81
+ */
82
+ getDiff() {
83
+ return computeDiffTags(this._firstSemantics || [], this.semantics);
84
+ }
85
+
86
+ /**
87
+ * Check if input is having a valid value.
88
+ * @memberof Panoramax.components.ui.SemanticsEditor#
89
+ * @returns {boolean} True if it's valid
90
+ */
91
+ checkValidity() {
92
+ return this._valid;
93
+ }
94
+
95
+ /**
96
+ * Force display of invalid input
97
+ * @memberof Panoramax.components.ui.SemanticsEditor#
98
+ */
99
+ reportValidity() {
100
+ this.renderRoot.querySelector("textarea")?.reportValidity();
101
+ }
102
+
103
+ /**
104
+ * Forces Editor to goes back to its empty, initial state.
105
+ * @param {object} [baseSemantics=null] Initial existing semantics you want to show in editor. This is useful for getting a diff of only new semantic entries. Leave empty if no tags pre-exist.
106
+ * @memberof Panoramax.components.ui.SemanticsEditor#
107
+ */
108
+ reset(baseSemantics = null) {
109
+ this.semantics = null;
110
+ this._valid = true;
111
+
112
+ const txt = this.renderRoot.querySelector("textarea");
113
+ txt?.setCustomValidity("");
114
+ txt?.reportValidity();
115
+
116
+ this._firstSemantics = baseSemantics;
117
+ this.semantics = baseSemantics;
118
+ }
119
+
120
+ /**
121
+ * Forces Editor to save current text and parse semantics.
122
+ * @memberof Panoramax.components.ui.SemanticsEditor#
123
+ */
124
+ forceInput() {
125
+ const target = this.renderRoot.querySelector("textarea");
126
+ this._onInput({ target });
127
+ this._onBlur({ target });
128
+ }
129
+
130
+ /** @private */
131
+ _onInput(e) {
132
+ const tarea = e.target;
133
+ try {
134
+ this.semantics = parseSemanticsString(tarea.value) || [];
135
+ this._valid = true;
136
+ } catch (err) {
137
+ if(err.message !== "Invalid tags") { console.error(err); }
138
+ this._valid = false;
139
+ }
140
+ }
141
+
142
+ /** @private */
143
+ _onBlur(e) {
144
+ const prevValidity = e.target.checkValidity();
145
+
146
+ if(this._valid) {
147
+ e.target.setCustomValidity("");
148
+
149
+ /**
150
+ * Event for value change.
151
+ *
152
+ * Note that this event is launched only on valid input.
153
+ *
154
+ * @event Panoramax.components.ui.SemanticsEditor#change
155
+ * @type {CustomEvent}
156
+ * @property {object[]} detail.semantics The new tags list (in API semantics property format)
157
+ * @property {object[]} detail.delta The delta between old and current tags (expected by API)
158
+ */
159
+ this.dispatchEvent(new CustomEvent("change", {
160
+ detail: {
161
+ semantics: this.semantics || [],
162
+ delta: computeDiffTags(this._firstSemantics || [], this.semantics),
163
+ }
164
+ }));
165
+ }
166
+ else if(prevValidity) { // Do not call again if already shows up, to fix Chrome issue
167
+ e.target.setCustomValidity(this._t?.pnx.semantics_editor_error || "Invalid syntax");
168
+ e.target.reportValidity();
169
+ }
170
+ }
171
+
172
+ /** @private */
173
+ render() {
174
+ return html`
175
+ <textarea
176
+ part="text"
177
+ autocomplete="off"
178
+ autocorrect="off"
179
+ autocapitalize="off"
180
+ spellcheck="false"
181
+ placeholder=${this._t?.pnx.semantics_editor_example || "key1=value1\nprefix|key2=value2"}
182
+ @input=${this._onInput}
183
+ @blur=${this._onBlur}
184
+ rows=${this.rows}
185
+ .value=${semanticsToString(this.semantics)}
186
+ ></textarea>
187
+ `;
188
+ }
189
+ }
190
+
191
+ customElements.define("pnx-semantics-editor", SemanticsEditor);
@@ -0,0 +1,88 @@
1
+ import { LitElement, css, html, nothing } from "lit";
2
+ import { table } from "../styles.js";
3
+ import { groupByPrefix } from "../../utils/semantics.js";
4
+
5
+ /**
6
+ * Semantics Tables display all semantics tags from either a picture or an annotation.
7
+ *
8
+ * @class Panoramax.components.ui.SemanticsTable
9
+ * @element pnx-semantics-table
10
+ * @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
11
+ * @example
12
+ * ```html
13
+ * <pnx-semantics-table source=${picture.properties} ._t=${viewer._t} />
14
+ * ```
15
+ */
16
+ export default class SemanticsTable extends LitElement {
17
+ /** @private */
18
+ static styles = [ table, css`
19
+ :host { display: block; }
20
+ th:first-child, td:first-child { width: 40%; }
21
+ td { vertical-align: top; }
22
+
23
+ td small { color: var(--grey-semi-dark); }
24
+ td a small { color: unset; }
25
+ .qualifiers { margin-top: 5px; }
26
+ .qualifiers > b { margin: 8px 0; }
27
+ .qualifiers ul {
28
+ list-style: none;
29
+ margin: 0px 0px 0px 15px;
30
+ padding: 0;
31
+ }
32
+
33
+ img.prefix-logo {
34
+ max-width: 15px;
35
+ max-height: 15px;
36
+ aspect-ratio: 1;
37
+ vertical-align: middle;
38
+ }
39
+ ` ];
40
+
41
+ /**
42
+ * Component properties.
43
+ * @memberof Panoramax.components.ui.SemanticsTable#
44
+ * @type {Object}
45
+ * @property {object} [source] The picture or annotation feature (having a `semantics` property)
46
+ */
47
+ static properties = {
48
+ source: {type: Object},
49
+ };
50
+
51
+ render() {
52
+ if(!this.source) { return nothing; }
53
+
54
+ const groups = groupByPrefix(this.source.semantics);
55
+
56
+ return html`<table>
57
+ <thead>
58
+ <tr><th>${this._t.pnx.semantics_key}</th><th>${this._t.pnx.semantics_value}</th></tr>
59
+ </thead>
60
+ <tbody>
61
+ ${groups.map(g => g.tags.map(tag => html`
62
+ <tr>
63
+ <td>
64
+ ${g.prefix != ""
65
+ ? (g.logo
66
+ ? html`<img src=${g.logo} class="prefix-logo" alt=${g.prefix} title=${g.title} />`
67
+ : html`<small>(${g.prefix})</small>`)
68
+ : nothing
69
+ }
70
+ ${g.key_transform ? g.key_transform(tag, this._t) : tag.key}
71
+ </td>
72
+ <td>
73
+ ${g.value_transform ? g.value_transform(tag, this._t) : tag.value}
74
+ ${tag.qualifiers ? html`<div class="qualifiers">
75
+ <b>${this._t.pnx.semantics_qualifiers}</b>
76
+ <ul>
77
+ ${tag.qualifiers.map(q => html`<li>${q.subkey} = ${q.value}</li>`)}
78
+ </ul>
79
+ </div>` : nothing}
80
+ </td>
81
+ </tr>
82
+ `))}
83
+ </tbody>
84
+ </table>`;
85
+ }
86
+ }
87
+
88
+ customElements.define("pnx-semantics-table", SemanticsTable);