@panoramax/web-viewer 3.1.1-develop-c42d6114 → 3.1.1-develop-4243d3f4

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.
@@ -455,6 +455,11 @@ a.gvs-btn { text-decoration: none; }
455
455
  margin-right: 5px;
456
456
  }
457
457
 
458
+ /* Grades */
459
+ .gvs-grade {
460
+ color: var(--orange);
461
+ }
462
+
458
463
 
459
464
  /***********************************************
460
465
  * Per-component styles
@@ -601,6 +606,59 @@ a.gvs-btn { text-decoration: none; }
601
606
  text-align: center;
602
607
  font-weight: bold;
603
608
  margin-bottom: 15px;
609
+ color: var(--orange);
610
+ }
611
+
612
+ #gvs-filter-qualityscore {
613
+ gap: 0px;
614
+ justify-content: center;
615
+ height: 42px;
616
+ }
617
+
618
+ .gvs-qualityscore, #gvs-filter-qualityscore label {
619
+ font-size: 18px;
620
+ width: 25px;
621
+ height: 30px;
622
+ line-height: 26px;
623
+ display: inline-block;
624
+ border: 1px solid white;
625
+ text-align: center;
626
+ background-color: gray;
627
+ color: rgba(255,255,255,0.9);
628
+ font-family: sans-serif;
629
+ font-weight: bold;
630
+ vertical-align: middle;
631
+ }
632
+ #gvs-filter-qualityscore label { cursor: pointer; }
633
+ #gvs-filter-qualityscore label:hover {
634
+ width: 28px;
635
+ height: 35px;
636
+ line-height: 30px;
637
+ border-radius: 3px;
638
+ font-size: 22px;
639
+ color: white;
640
+ border: 2px solid white;
641
+ }
642
+ .gvs-qualityscore:first-of-type,
643
+ #gvs-filter-qualityscore label:first-of-type {
644
+ border-top-left-radius: 8px;
645
+ border-bottom-left-radius: 8px;
646
+ }
647
+ .gvs-qualityscore:last-of-type,
648
+ #gvs-filter-qualityscore label:last-of-type {
649
+ border-top-right-radius: 8px;
650
+ border-bottom-right-radius: 8px;
651
+ }
652
+ #gvs-filter-qualityscore input[type="checkbox"] { display: none; }
653
+ .gvs-qualityscore-selected,
654
+ #gvs-filter-qualityscore input[type="checkbox"]:checked + label {
655
+ width: 30px;
656
+ height: 42px;
657
+ line-height: 37px;
658
+ border-radius: 8px;
659
+ font-size: 27px;
660
+ color: white;
661
+ border: 2px solid white;
604
662
  }
605
663
 
606
664
 
@@ -650,6 +708,11 @@ a.gvs-btn { text-decoration: none; }
650
708
  margin-right: 5px;
651
709
  }
652
710
 
711
+ #gvs-map-theme-legend-score {
712
+ justify-content: center;
713
+ gap: 0;
714
+ }
715
+
653
716
 
654
717
  /* Player */
655
718
  #gvs-widget-player { justify-content: center; }
@@ -3,9 +3,10 @@ import { PSV_ANIM_DURATION, PSV_ZOOM_DELTA, PIC_MAX_STAY_DURATION } from "../Vie
3
3
  import {
4
4
  createPanel, createGroup, fa, fat, createButton, disableButton,
5
5
  createSearchBar, createExpandableButton, enableButton, enableCopyButton, closeOtherPanels,
6
- createLinkCell, createTable, createHeader, createButtonSpan, createLabel
6
+ createLinkCell, createTable, createHeader, createButtonSpan, createLabel, showGrade,
7
+ showQualityScore,
7
8
  } from "../utils/Widgets";
8
- import { COLORS, isInIframe, getUserAccount } from "../utils/Utils";
9
+ import { COLORS, isInIframe, getUserAccount, QUALITYSCORE_VALUES, getGrade, QUALITYSCORE_GPS_VALUES, QUALITYSCORE_RES_360_VALUES, QUALITYSCORE_RES_FLAT_VALUES, QUALITYSCORE_POND_RES, QUALITYSCORE_POND_GPS } from "../utils/Utils";
9
10
  import SwitchBig from "../img/switch_big.svg";
10
11
  import SwitchMini from "../img/switch_mini.svg";
11
12
  import BackgroundAerial from "../img/bg_aerial.jpg";
@@ -50,6 +51,8 @@ import { faCircleQuestion } from "@fortawesome/free-solid-svg-icons/faCircleQues
50
51
  import { faCommentDots } from "@fortawesome/free-solid-svg-icons/faCommentDots";
51
52
  import { faAt } from "@fortawesome/free-solid-svg-icons/faAt";
52
53
  import { faPaperPlane } from "@fortawesome/free-solid-svg-icons/faPaperPlane";
54
+ import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal";
55
+ import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
53
56
 
54
57
 
55
58
  /**
@@ -112,7 +115,8 @@ export default class Widgets {
112
115
  this._initWidgetSearch();
113
116
  this._initWidgetFilters(
114
117
  this._viewer._api._endpoints.user_search !== null
115
- && this._viewer._api._endpoints.user_tiles !== null
118
+ && this._viewer._api._endpoints.user_tiles !== null,
119
+ this._viewer.map && this._viewer.map._hasQualityScore()
116
120
  );
117
121
  this._initWidgetMapLayers();
118
122
  this._listenMapFiltersChanges();
@@ -494,18 +498,30 @@ export default class Widgets {
494
498
 
495
499
  // Camera details
496
500
  popupContent.push(createHeader("h4", `${fat(faCamera)} ${this._t.gvs.metadata_camera}`));
497
- const focal = picMeta?.properties?.["pers:interior_orientation"]?.focal_length ? `${picMeta?.properties?.["pers:interior_orientation"]?.focal_length} mm` : "unknown";
501
+ const focal = picMeta?.properties?.["pers:interior_orientation"]?.focal_length ? `${picMeta?.properties?.["pers:interior_orientation"]?.focal_length} mm` : "";
502
+ let resmp = picMeta?.properties?.["pers:interior_orientation"]?.["sensor_array_dimensions"];
503
+ if(resmp) {
504
+ resmp = `${resmp[0]} x ${resmp[1]} px (${Math.floor(resmp[0] * resmp[1] / 1000000)} Mpx)`;
505
+ }
506
+ let pictype = this._t.gvs.picture_flat;
507
+ let picFov = picMeta?.properties?.["pers:interior_orientation"]?.["field_of_view"]; // Use raw value instead of horizontalFov to avoid default showing up
508
+ if(picFov !== null && picFov !== undefined) {
509
+ if(picFov === 360) { pictype = this._t.gvs.picture_360; }
510
+ else { pictype += ` (${picFov}°)`; }
511
+ }
512
+
498
513
  const cameraData = [
499
- { section: this._t.gvs.metadata_camera_make, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_manufacturer },
500
- { section: this._t.gvs.metadata_camera_model, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_model },
501
- { section: this._t.gvs.metadata_camera_type, value: picMeta?.horizontalFov === 360 ? this._t.gvs.picture_360 : this._t.gvs.picture_flat },
514
+ { section: this._t.gvs.metadata_camera_make, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_manufacturer || "❓" },
515
+ { section: this._t.gvs.metadata_camera_model, value: picMeta?.properties?.["pers:interior_orientation"]?.camera_model || "❓" },
516
+ { section: this._t.gvs.metadata_camera_type, value: pictype },
517
+ { section: this._t.gvs.metadata_camera_resolution, value: resmp || "❓" },
502
518
  { section: this._t.gvs.metadata_camera_focal_length, value: focal },
503
519
  ];
504
520
  popupContent.push(createTable("gvs-table-light", cameraData));
505
521
 
506
522
  // Location details
507
523
  popupContent.push(createHeader("h4", `${fat(faLocationDot)} ${this._t.gvs.metadata_location}`));
508
- const orientation = picMeta?.properties?.["view:azimuth"] !== undefined ? `${picMeta.properties["view:azimuth"]}°` : "unknown";
524
+ const orientation = picMeta?.properties?.["view:azimuth"] !== undefined ? `${picMeta.properties["view:azimuth"]}°` : "";
509
525
  const gpsPrecisionLabel = getGPSPrecision(picMeta);
510
526
  const locationData = [
511
527
  { section: this._t.gvs.metadata_location_longitude, value: picMeta.gps[0] },
@@ -515,6 +531,28 @@ export default class Widgets {
515
531
  ];
516
532
  popupContent.push(createTable("gvs-table-light", locationData));
517
533
 
534
+ // Picture quality level
535
+ if(this._viewer?.map?._hasQualityScore()) {
536
+ popupContent.push(createHeader(
537
+ "h4",
538
+ `${fat(faMedal)} ${this._t.gvs.metadata_quality} <a href="https://docs.panoramax.fr/pictures-metadata/quality_score/" target="_blank" title="${this._t.gvs.metadata_quality_help}">${fat(faInfoCircle)}</a>`
539
+ ));
540
+ const gpsGrade = getGrade(QUALITYSCORE_GPS_VALUES, picMeta?.properties?.["quality:horizontal_accuracy"]);
541
+ const resGrade = getGrade(
542
+ picMeta?.horizontalFov === 360 ? QUALITYSCORE_RES_360_VALUES : QUALITYSCORE_RES_FLAT_VALUES,
543
+ picMeta?.properties?.["panoramax:horizontal_pixel_density"]
544
+ );
545
+ // Note: score is also calculated in utils/map code
546
+ const generalGrade = Math.round((resGrade || 1) * QUALITYSCORE_POND_RES + (gpsGrade || 1) * QUALITYSCORE_POND_GPS);
547
+
548
+ const qualityData = [
549
+ { section: this._t.gvs.metadata_quality_score, value: showQualityScore(generalGrade) },
550
+ { section: this._t.gvs.metadata_quality_gps_score, value: showGrade(gpsGrade, this._t) },
551
+ { section: this._t.gvs.metadata_quality_resolution_score, value: showGrade(resGrade, this._t) },
552
+ ];
553
+ popupContent.push(createTable("gvs-table-light", qualityData));
554
+ }
555
+
518
556
  // EXIF
519
557
  if (picMeta.properties?.exif) {
520
558
  const exifDetails = document.createElement("details");
@@ -829,6 +867,7 @@ export default class Widgets {
829
867
  <option value="default">${this._t.gvs.map_theme_default}</option>
830
868
  <option value="age">${this._t.gvs.map_theme_age}</option>
831
869
  <option value="type">${this._t.gvs.map_theme_type}</option>
870
+ ${this._viewer?.map?._hasQualityScore() ? "<option value=\"score\">"+this._t.gvs.map_theme_score+"</option>" : ""}
832
871
  </select>
833
872
  </div>
834
873
  <div>
@@ -864,6 +903,9 @@ export default class Widgets {
864
903
  ${this._t.gvs.picture_flat}
865
904
  </div>
866
905
  </div>
906
+ <div id="gvs-map-theme-legend-score" class="gvs-map-theme-legend gvs-hidden">
907
+ ${QUALITYSCORE_VALUES.map(pv => "<span class=\"gvs-qualityscore\" style=\"background-color: "+pv.color+";\">"+pv.label+"</span>").join("")}
908
+ </div>
867
909
  </div>`;
868
910
 
869
911
  // Map theme events
@@ -929,7 +971,7 @@ export default class Widgets {
929
971
  * This should be called only if map is enabled.
930
972
  * @private
931
973
  */
932
- _initWidgetFilters(hasUserSearch) {
974
+ _initWidgetFilters(hasUserSearch, hasQualityScore) {
933
975
  const btnFilter = createExpandableButton("gvs-filter", faSliders, this._t.gvs.filters, this);
934
976
  const pnlFilter = createPanel(this, btnFilter, []);
935
977
  pnlFilter.innerHTML = `
@@ -948,10 +990,9 @@ export default class Widgets {
948
990
  <input type="checkbox" id="gvs-filter-type-360" name="360" checked />
949
991
  <label for="gvs-filter-type-360">${this._t.gvs.picture_360}</label>
950
992
  </div>
951
- <!--h4>${fat(faCamera)} ${this._t.gvs.filter_camera_model}</h4>
952
- <div class="gvs-input-group" id="gvs-filter-model"></div-->
953
993
  </form>
954
994
  `;
995
+ const form = pnlFilter.children[0];
955
996
  createGroup(
956
997
  "gvs-widget-filter",
957
998
  this._viewer.isWidthSmall() ? "main-top-right" : "main-top-left",
@@ -964,10 +1005,39 @@ export default class Widgets {
964
1005
  pnlFilter.style.width = `${this._viewer.container.offsetWidth - 70}px`;
965
1006
  }
966
1007
 
1008
+ // Create qualityscore filter
1009
+ if(hasQualityScore) {
1010
+ const title = document.createElement("h4");
1011
+ title.innerHTML = `${fat(faMedal)} ${this._t.gvs.filter_qualityscore}`;
1012
+ title.style.marginBottom = "3px";
1013
+ form.appendChild(title);
1014
+
1015
+ const div = document.createElement("div");
1016
+ div.id = "gvs-filter-qualityscore";
1017
+ div.classList.add("gvs-input-group");
1018
+
1019
+ QUALITYSCORE_VALUES.forEach(pv => {
1020
+ const input = document.createElement("input");
1021
+ input.id = "gvs-filter-qualityscore-" + pv.label;
1022
+ input.type = "checkbox";
1023
+ input.name = "qualityscore";
1024
+ input.value = pv.label;
1025
+
1026
+ const label = document.createElement("label");
1027
+ label.setAttribute("for", input.id);
1028
+ label.title = this._t.gvs.filter_qualityscore_help;
1029
+ label.appendChild(document.createTextNode(pv.label));
1030
+ label.style.backgroundColor = pv.color;
1031
+
1032
+ div.appendChild(input);
1033
+ div.appendChild(label);
1034
+ });
1035
+
1036
+ form.appendChild(div);
1037
+ }
1038
+
967
1039
  // Create search bar for users
968
1040
  if(hasUserSearch) {
969
- const form = pnlFilter.querySelector("#gvs-filter-form");
970
-
971
1041
  const title = document.createElement("h4");
972
1042
  title.innerHTML = `${fat(faUser)} ${this._t.gvs.filter_user}`;
973
1043
  form.appendChild(title);
@@ -994,18 +1064,6 @@ export default class Widgets {
994
1064
  form.appendChild(input);
995
1065
  }
996
1066
 
997
- // Create search bar for camera model
998
- // TODO : implement when API is ready
999
- // const cameraSearch = createSearchBar(
1000
- // "gvs-filter-camera-model",
1001
- // this._t.gvs.search,
1002
- // () => Promise.reject(),
1003
- // () => {},
1004
- // this
1005
- // );
1006
- // document.getElementById("gvs-filter-model").appendChild(cameraSearch);
1007
-
1008
- const form = pnlFilter.children[0];
1009
1067
  this._formDelay = null;
1010
1068
 
1011
1069
  const onFormChange = () => {
@@ -1041,19 +1099,33 @@ export default class Widgets {
1041
1099
  const fMaxDate = document.getElementById("gvs-filter-date-end");
1042
1100
  const fTypeFlat = document.getElementById("gvs-filter-type-flat");
1043
1101
  const fType360 = document.getElementById("gvs-filter-type-360");
1044
- // const fCamera = document.getElementById("gvs-filter-camera");
1045
1102
  const fMapTheme = document.getElementById("gvs-map-theme");
1046
1103
 
1047
1104
  let type = "";
1048
1105
  if(fType360.checked && !fTypeFlat.checked) { type = "equirectangular"; }
1049
1106
  if(!fType360.checked && fTypeFlat.checked) { type = "flat"; }
1050
1107
 
1108
+ let qualityscore = [];
1109
+ if(this._viewer?.map?._hasQualityScore()) {
1110
+ const fScoreA = document.getElementById("gvs-filter-qualityscore-A");
1111
+ const fScoreB = document.getElementById("gvs-filter-qualityscore-B");
1112
+ const fScoreC = document.getElementById("gvs-filter-qualityscore-C");
1113
+ const fScoreD = document.getElementById("gvs-filter-qualityscore-D");
1114
+ const fScoreE = document.getElementById("gvs-filter-qualityscore-E");
1115
+ if(fScoreA.checked) { qualityscore.push(5); }
1116
+ if(fScoreB.checked) { qualityscore.push(4); }
1117
+ if(fScoreC.checked) { qualityscore.push(3); }
1118
+ if(fScoreD.checked) { qualityscore.push(2); }
1119
+ if(fScoreE.checked) { qualityscore.push(1); }
1120
+ if(qualityscore.length == 5) { qualityscore = []; }
1121
+ }
1122
+
1051
1123
  const values = {
1052
1124
  minDate: fMinDate.value,
1053
1125
  maxDate: fMaxDate.value,
1054
1126
  type,
1055
- // camera: fCamera.value,
1056
1127
  theme: fMapTheme.value,
1128
+ qualityscore,
1057
1129
  };
1058
1130
 
1059
1131
  this._viewer.setFilters(values);
@@ -1068,19 +1140,29 @@ export default class Widgets {
1068
1140
  const fMaxDate = document.getElementById("gvs-filter-date-end");
1069
1141
  const fTypeFlat = document.getElementById("gvs-filter-type-flat");
1070
1142
  const fType360 = document.getElementById("gvs-filter-type-360");
1071
- // const fCamera = document.getElementById("gvs-filter-camera");
1072
1143
  const fMapTheme = document.getElementById("gvs-map-theme");
1144
+ const fScoreA = document.getElementById("gvs-filter-qualityscore-A");
1145
+ const fScoreB = document.getElementById("gvs-filter-qualityscore-B");
1146
+ const fScoreC = document.getElementById("gvs-filter-qualityscore-C");
1147
+ const fScoreD = document.getElementById("gvs-filter-qualityscore-D");
1148
+ const fScoreE = document.getElementById("gvs-filter-qualityscore-E");
1073
1149
 
1074
1150
  // Update widget based on programmatic filter changes
1075
1151
  this._viewer.addEventListener("filters-changed", e => {
1076
1152
  if(e.detail.minDate) { fMinDate.value = e.detail.minDate; }
1077
1153
  if(e.detail.maxDate) { fMaxDate.value = e.detail.maxDate; }
1078
- // if(e.detail.camera) { fCamera.value = e.detail.camera; }
1079
1154
  if(e.detail.theme) { fMapTheme.value = e.detail.theme; }
1080
1155
  if(e.detail.type) {
1081
1156
  fType360.checked = ["", "equirectangular"].includes(e.detail.type);
1082
1157
  fTypeFlat.checked = ["", "flat"].includes(e.detail.type);
1083
1158
  }
1159
+ if(e.detail.qualityscore) {
1160
+ fScoreA.checked = e.detail.qualityscore.includes(5) && e.detail.qualityscore.length < 5;
1161
+ fScoreB.checked = e.detail.qualityscore.includes(4) && e.detail.qualityscore.length < 5;
1162
+ fScoreC.checked = e.detail.qualityscore.includes(3) && e.detail.qualityscore.length < 5;
1163
+ fScoreD.checked = e.detail.qualityscore.includes(2) && e.detail.qualityscore.length < 5;
1164
+ fScoreE.checked = e.detail.qualityscore.includes(1) && e.detail.qualityscore.length < 5;
1165
+ }
1084
1166
  this._onMapThemeChange();
1085
1167
  });
1086
1168
 
@@ -19,22 +19,22 @@ describe("getExifFloat", () => {
19
19
 
20
20
  describe("getGPSPrecision", () => {
21
21
  it.each([
22
- [undefined, "unknown"],
23
- [0.4, "ideal"],
24
- [0.9, "excellent"],
25
- [2.9, "good"],
26
- [6.9, "moderate"],
27
- [9.9, "fair"],
28
- [20, "poor"],
29
- ["9/10", "excellent"],
30
- ["99/10", "fair"],
22
+ [undefined, ""],
23
+ [0.4, "0.4 m"],
24
+ [0.9, "0.9 m"],
25
+ [2.9, "2.9 m"],
26
+ [6.9, "6.9 m"],
27
+ [9.9, "9.9 m"],
28
+ [20, "20 m"],
29
+ ["9/10", "0.9 m"],
30
+ ["99/10", "9.9 m"],
31
31
  ])("handles GPSHPos %s > %s", (input, expected) => {
32
32
  const p = { properties: { exif: { "Exif.GPSInfo.GPSHPositioningError": input, "Exif.GPSInfo.GPSDOP": input } } };
33
33
  expect(Exif.getGPSPrecision(p)).toBe(expected);
34
34
  });
35
35
 
36
36
  it.each([
37
- [undefined, "unknown"],
37
+ [undefined, ""],
38
38
  [0.9, "ideal"],
39
39
  [1.9, "excellent"],
40
40
  [4.9, "good"],
@@ -3,6 +3,7 @@
3
3
  exports[`_loadMapStyles handles default user 1`] = `
4
4
  Object {
5
5
  "layers": Array [],
6
+ "metadata": Object {},
6
7
  "sources": Object {
7
8
  "geovisio": Object {
8
9
  "maxzoom": 15,
@@ -30,6 +31,7 @@ Object {
30
31
  "id": "provider_blo",
31
32
  },
32
33
  ],
34
+ "metadata": Object {},
33
35
  "sources": Object {
34
36
  "provider": Object {},
35
37
  "provider_bla": Object {},
@@ -46,6 +48,7 @@ Object {
46
48
  "id": "provlayer",
47
49
  },
48
50
  ],
51
+ "metadata": Object {},
49
52
  "name": "Provider",
50
53
  "sources": Object {
51
54
  "geovisio": Object {
@@ -69,6 +72,7 @@ Object {
69
72
  "id": "provlayer",
70
73
  },
71
74
  ],
75
+ "metadata": Object {},
72
76
  "name": "Provider",
73
77
  "sources": Object {
74
78
  "geovisio": Object {
@@ -88,6 +92,7 @@ Object {
88
92
  exports[`_loadMapStyles works if no background style set 1`] = `
89
93
  Object {
90
94
  "layers": Array [],
95
+ "metadata": Object {},
91
96
  "sources": Object {
92
97
  "geovisio": Object {
93
98
  "maxzoom": 15,