@panoramax/web-viewer 3.1.1-develop-c42d6114 → 3.1.1-develop-a3fa5272

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.
@@ -116,13 +116,22 @@ The camera make and model to filter shown pictures and sequences on map (if map
116
116
  - `camera=gopro%20max` will only show pictures taken with a _GoPro Max_ camera
117
117
  - `camera=max` will not shown any picture on map, as it doesn't match any camera make
118
118
 
119
+ ### :medal: `pic_score`: filter map data by quality score
120
+
121
+ The pictures quality level wanted for map display (if map is enabled). Values are `A`, `B`, `C`, `D`, `E` and can be used that way:
122
+
123
+ - `pic_score=A` for only best pictures
124
+ - `pic_score=ABC` for A, B or C-grade pictures
125
+
126
+
119
127
  ### :material-format-paint: `theme`: map colouring for pictures and sequences
120
128
 
121
129
  The map theme to use for displaying pictures and sequences (if map is enabled). Available themes are:
122
130
 
123
131
  - `theme=default` (or no setting defined): single color for display (no classification)
124
132
  - `theme=age`: color based on picture/sequence age (red = recent, yellow = 2+ years old)
125
- - `theme=type`: color based on camera type (orange = classic, green = 360°)
133
+ - `theme=type`: color based on camera type (orange = classic, green = 360°)
134
+ - `theme=score`: color based on quality score (green = best quality, yellow = worst)
126
135
 
127
136
  ### :material-nature-people: `background`: map background
128
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panoramax/web-viewer",
3
- "version": "3.1.1-develop-c42d6114",
3
+ "version": "3.1.1-develop-a3fa5272",
4
4
  "description": "Panoramax web viewer for geolocated pictures",
5
5
  "main": "build/index.js",
6
6
  "author": "Panoramax team",
@@ -88,8 +88,9 @@
88
88
  "workbox-webpack-plugin": "^6.5.4"
89
89
  },
90
90
  "dependencies": {
91
- "@fortawesome/fontawesome-svg-core": "^6.4.0",
92
- "@fortawesome/free-solid-svg-icons": "^6.4.0",
91
+ "@fortawesome/fontawesome-svg-core": "^6.6.0",
92
+ "@fortawesome/free-regular-svg-icons": "^6.6.0",
93
+ "@fortawesome/free-solid-svg-icons": "^6.6.0",
93
94
  "@photo-sphere-viewer/core": "5.11.0-beta.1",
94
95
  "@photo-sphere-viewer/equirectangular-tiles-adapter": "5.11.0-beta.1",
95
96
  "@photo-sphere-viewer/gallery-plugin": "5.11.0-beta.1",
package/src/Viewer.js CHANGED
@@ -2,11 +2,11 @@ import "./Viewer.css";
2
2
  import { SYSTEM as PSSystem, DEFAULTS as PSDefaults } from "@photo-sphere-viewer/core";
3
3
  import Widgets from "./viewer/Widgets";
4
4
  import URLHash from "./viewer/URLHash";
5
- import { COLORS, josmBboxParameters, linkMapAndPhoto } from "./utils/Utils";
5
+ import { COLORS, QUALITYSCORE_VALUES, josmBboxParameters, linkMapAndPhoto } from "./utils/Utils";
6
6
  import CoreView from "./components/CoreView";
7
7
  import Photo, { PSV_DEFAULT_ZOOM, PSV_ANIM_DURATION, PIC_MAX_STAY_DURATION } from "./components/Photo";
8
8
  import Map from "./components/Map";
9
- import { TILES_PICTURES_ZOOM } from "./utils/Map";
9
+ import { TILES_PICTURES_ZOOM, MAP_EXPR_QUALITYSCORE } from "./utils/Map";
10
10
  import { enableCopyButton, fa } from "./utils/Widgets";
11
11
  import { faXmark } from "@fortawesome/free-solid-svg-icons/faXmark";
12
12
 
@@ -20,6 +20,7 @@ const MAP_THEMES = {
20
20
  DEFAULT: "default",
21
21
  AGE: "age",
22
22
  TYPE: "type",
23
+ SCORE: "score",
23
24
  };
24
25
 
25
26
 
@@ -809,6 +810,15 @@ class Viewer extends CoreView {
809
810
  COLORS.QUALI_2
810
811
  );
811
812
  }
813
+ else if(this._mapTheme == MAP_THEMES.SCORE) {
814
+ s.push(
815
+ ["==", MAP_EXPR_QUALITYSCORE, 5], QUALITYSCORE_VALUES[0].color,
816
+ ["==", MAP_EXPR_QUALITYSCORE, 4], QUALITYSCORE_VALUES[1].color,
817
+ ["==", MAP_EXPR_QUALITYSCORE, 3], QUALITYSCORE_VALUES[2].color,
818
+ ["==", MAP_EXPR_QUALITYSCORE, 2], QUALITYSCORE_VALUES[3].color,
819
+ QUALITYSCORE_VALUES[4].color,
820
+ );
821
+ }
812
822
  else {
813
823
  s.push(COLORS.BASE);
814
824
  }
@@ -827,6 +837,7 @@ class Viewer extends CoreView {
827
837
  // - 20-80 : custom ranges
828
838
  // - 10 : basic feature
829
839
  // - 0 : on bottom / feature with undefined property
840
+
830
841
  // Hidden style
831
842
  const s = ["case",
832
843
  ["==", ["get", "hidden"], true], 90
@@ -859,6 +870,15 @@ class Viewer extends CoreView {
859
870
  ["==", ["get", "type"], "equirectangular"], 50,
860
871
  );
861
872
  }
873
+ else if(this._mapTheme == MAP_THEMES.SCORE) {
874
+ s.push(
875
+ ["==", MAP_EXPR_QUALITYSCORE, 5], 80,
876
+ ["==", MAP_EXPR_QUALITYSCORE, 4], 65,
877
+ ["==", MAP_EXPR_QUALITYSCORE, 3], 50,
878
+ ["==", MAP_EXPR_QUALITYSCORE, 2], 35,
879
+ ["==", MAP_EXPR_QUALITYSCORE, 1], 20,
880
+ );
881
+ }
862
882
 
863
883
  s.push(10);
864
884
  return s;
@@ -1076,6 +1096,7 @@ class Viewer extends CoreView {
1076
1096
  * @param {string} [filters.type] Type of picture to keep (flat, equirectangular)
1077
1097
  * @param {string} [filters.camera] Camera make and model to keep
1078
1098
  * @param {string} [filters.theme] Map theme to use
1099
+ * @param {number[]} [filters.qualityscore] QualityScore values, as a list of 1 to 5 grades
1079
1100
  * @param {boolean} [skipZoomIn=false] If true, doesn't force zoom in to map level >= 7
1080
1101
  */
1081
1102
  setFilters(filters, skipZoomIn = false) {
@@ -1088,6 +1109,7 @@ class Viewer extends CoreView {
1088
1109
  mapSeqFilters.push([">=", ["get", "date"], filters.minDate]);
1089
1110
  mapPicFilters.push([">=", ["get", "ts"], filters.minDate]);
1090
1111
  }
1112
+
1091
1113
  if(filters.maxDate && filters.maxDate !== "") {
1092
1114
  this._mapFilters.maxDate = filters.maxDate;
1093
1115
  mapSeqFilters.push(["<=", ["get", "date"], filters.maxDate]);
@@ -1099,11 +1121,13 @@ class Viewer extends CoreView {
1099
1121
  d = d.toISOString().split("T")[0];
1100
1122
  mapPicFilters.push(["<=", ["get", "ts"], d]);
1101
1123
  }
1124
+
1102
1125
  if(filters.type && filters.type !== "") {
1103
1126
  this._mapFilters.type = filters.type;
1104
1127
  mapSeqFilters.push(["==", ["get", "type"], filters.type]);
1105
1128
  mapPicFilters.push(["==", ["get", "type"], filters.type]);
1106
1129
  }
1130
+
1107
1131
  if(filters.camera && filters.camera !== "") {
1108
1132
  this._mapFilters.camera = filters.camera;
1109
1133
  // low/high model hack : to enable fuzzy filtering of camera make and model
@@ -1116,6 +1140,12 @@ class Viewer extends CoreView {
1116
1140
  mapPicFilters.push(["<=", ["get", "model"], highModel, collator]);
1117
1141
  }
1118
1142
 
1143
+ if(filters.qualityscore && filters.qualityscore.length > 0) {
1144
+ this._mapFilters.qualityscore = filters.qualityscore;
1145
+ mapSeqFilters.push(["in", MAP_EXPR_QUALITYSCORE, ["literal", this._mapFilters.qualityscore]]);
1146
+ mapPicFilters.push(["in", MAP_EXPR_QUALITYSCORE, ["literal", this._mapFilters.qualityscore]]);
1147
+ }
1148
+
1119
1149
  if(filters.theme && Object.values(MAP_THEMES).includes(filters.theme)) {
1120
1150
  this._mapFilters.theme = filters.theme;
1121
1151
  if(this.map) {
@@ -1175,6 +1205,7 @@ class Viewer extends CoreView {
1175
1205
  * @property {string} [detail.type] Camera type (equirectangular, flat, null/empty string for both)
1176
1206
  * @property {string} [detail.camera] Camera make and model
1177
1207
  * @property {string} [detail.theme] Map theme
1208
+ * @property {number[]} [detail.qualityscore] QualityScore values, as a list of 1 to 5 grades
1178
1209
  */
1179
1210
  const event = new CustomEvent("filters-changed", { detail: Object.assign({}, this._mapFilters) });
1180
1211
  this.dispatchEvent(event);
@@ -53,6 +53,12 @@
53
53
  margin-right: 2px;
54
54
  }
55
55
 
56
+ .gvs h4 a svg {
57
+ height: 16px;
58
+ vertical-align: sub;
59
+ margin-left: 2px;
60
+ }
61
+
56
62
  /* Hidden elements on mobile */
57
63
  @container (max-width: 576px) {
58
64
  .gvs-mobile-hidden { display: none !important; }
@@ -68,16 +68,31 @@ export default class CoreView extends EventTarget {
68
68
  this.container.appendChild(this.loaderContainer);
69
69
  this._loader = new Loader(this, this.loaderContainer);
70
70
 
71
- // API init
72
- endpoint = endpoint.replace("/api/search", "/api");
73
- this._api = new API(endpoint, {
74
- users: this._options.users,
75
- fetch: this._options?.fetchOptions,
76
- style: this._options.style,
77
- });
78
- this._api.onceReady()
79
- .then(() => console.info(`🌐 Connected to API "${this._api._metadata.name}" (${this._api._endpoint})\nℹ️ API runs STAC ${this._api._metadata.stac_version} & GeoVisio ${this._api._metadata.geovisio_version}`))
80
- .catch(e => this._loader.dismiss(e, this._t.gvs.error_api));
71
+ // API init)
72
+ endpoint = (endpoint || "").replace("/api/search", "/api");
73
+ try {
74
+ this._api = new API(endpoint, {
75
+ users: this._options.users,
76
+ fetch: this._options?.fetchOptions,
77
+ style: this._options.style,
78
+ });
79
+ this._api.onceReady()
80
+ .then(() => {
81
+ let unavailable = this._api.getUnavailableFeatures();
82
+ let available = this._api.getAvailableFeatures();
83
+ available = unavailable.length === 0 ? "✅ All features available" : "✅ Available features: "+available.join(", ");
84
+ unavailable = unavailable.length === 0 ? "" : "🚫 Unavailable features: "+unavailable.join(", ");
85
+ console.info(`🌐 Connected to API "${this._api._metadata.name}" (${this._api._endpoint})
86
+ ℹ️ API runs STAC ${this._api._metadata.stac_version} ${this._api._metadata.geovisio_version ? "& GeoVisio "+this._api._metadata.geovisio_version : ""}
87
+ ${available}
88
+ ${unavailable}
89
+ `.trim());
90
+ })
91
+ .catch(e => this._loader.dismiss(e, this._t.gvs.error_api));
92
+ }
93
+ catch(e) {
94
+ this._loader.dismiss(e, this._t.gvs.error_api);
95
+ }
81
96
  }
82
97
 
83
98
  /**
@@ -187,6 +187,15 @@ export default class Map extends maplibregl.Map {
187
187
  });
188
188
  }
189
189
 
190
+ /**
191
+ * Is QualityScore available in vector tiles ?
192
+ * @private
193
+ */
194
+ _hasQualityScore() {
195
+ const fields = this.getStyle()?.metadata?.["panoramax:fields"] || {};
196
+ return fields?.pictures?.includes("gps_accuracy") && fields?.pictures?.includes("h_pixel_density");
197
+ }
198
+
190
199
  /**
191
200
  * Force refresh of vector tiles data
192
201
  */
@@ -62,7 +62,8 @@
62
62
  "🔍 Analyzing EXIF metadata",
63
63
  "🏘️ 3D rendering",
64
64
  "📷 Initializing 360° pictures",
65
- "🟠 Colour balancing"
65
+ "🟠 Colour balancing",
66
+ "💯 Computing Quality Score ©"
66
67
  ],
67
68
  "loading_labels_fun": [
68
69
  "🦌 Deer crossing sign detection",
@@ -97,8 +98,9 @@
97
98
  "filter_zoom_in": "Zoom-in for filters to be visible",
98
99
  "picture_flat": "Classic",
99
100
  "picture_360": "360°",
100
- "filter_camera_model": "By camera model",
101
101
  "filter_reset": "Clear filters",
102
+ "filter_qualityscore": "Quality score",
103
+ "filter_qualityscore_help": "Click to enable or disable",
102
104
  "map_background": "Map background",
103
105
  "map_background_aerial": "Aerial",
104
106
  "map_background_streets": "Streets",
@@ -110,6 +112,7 @@
110
112
  "map_theme_age_3": "< 1 year",
111
113
  "map_theme_age_4": "< 1 month",
112
114
  "map_theme_type": "Camera type",
115
+ "map_theme_score": "Quality score",
113
116
  "contrast": "Enable higher image contrast",
114
117
  "metadata": "Picture metadata",
115
118
  "metadata_general_picid": "Picture identifier",
@@ -124,12 +127,19 @@
124
127
  "metadata_camera_make": "Make",
125
128
  "metadata_camera_model": "Model",
126
129
  "metadata_camera_type": "Type",
130
+ "metadata_camera_resolution": "Resolution",
127
131
  "metadata_camera_focal_length": "Focal length",
128
132
  "metadata_location": "Location",
129
133
  "metadata_location_longitude": "Longitude",
130
134
  "metadata_location_latitude": "Latitude",
131
135
  "metadata_location_orientation": "Capture direction",
132
136
  "metadata_location_precision": "Positioning precision",
137
+ "metadata_quality": "Quality Score",
138
+ "metadata_quality_help": "Know more about Quality Score",
139
+ "metadata_quality_score": "Global score",
140
+ "metadata_quality_gps_score": "Positioning score",
141
+ "metadata_quality_resolution_score": "Resolution score",
142
+ "metadata_quality_missing": "no value set in picture",
133
143
  "metadata_exif": "EXIF / XMP",
134
144
  "metadata_exif_name": "Tag",
135
145
  "metadata_exif_value": "Value",
@@ -62,7 +62,8 @@
62
62
  "🔍 Analyse des métadonnées EXIF",
63
63
  "🏘️ Création du rendu 3D",
64
64
  "📷 Initialisation des vues 360°",
65
- "🟠 Équilibrage des couleurs"
65
+ "🟠 Équilibrage des couleurs",
66
+ "💯 Calcul du score qualité ©"
66
67
  ],
67
68
  "loading_labels_fun": [
68
69
  "🦌 Détection des panneaux biche",
@@ -96,8 +97,9 @@
96
97
  "filter_picture": "Type d'image",
97
98
  "picture_flat": "Classique",
98
99
  "picture_360": "360°",
99
- "filter_camera_model": "Par modèle d'appareil",
100
100
  "filter_reset": "Retirer les filtres",
101
+ "filter_qualityscore": "Score de qualité",
102
+ "filter_qualityscore_help": "Cliquez pour activer ou désactiver",
101
103
  "filter_zoom_in": "Zoomez plus pour voir les filtres",
102
104
  "map_background": "Fond de carte",
103
105
  "map_background_aerial": "Satellite",
@@ -110,6 +112,7 @@
110
112
  "map_theme_age_3": "< 1 an",
111
113
  "map_theme_age_4": "< 1 mois",
112
114
  "map_theme_type": "Type de caméra",
115
+ "map_theme_score": "Score de qualité",
113
116
  "contrast": "Augmenter le contraste de l'image",
114
117
  "metadata": "Métadonnées de la photo",
115
118
  "metadata_general_picid": "Identifiant de photo",
@@ -124,12 +127,19 @@
124
127
  "metadata_camera_make": "Fabricant",
125
128
  "metadata_camera_model": "Modèle",
126
129
  "metadata_camera_type": "Type",
130
+ "metadata_camera_resolution": "Résolution",
127
131
  "metadata_camera_focal_length": "Longueur focale",
128
132
  "metadata_location": "Localisation",
129
133
  "metadata_location_longitude": "Longitude",
130
134
  "metadata_location_latitude": "Latitude",
131
135
  "metadata_location_orientation": "Direction de prise de vue",
132
136
  "metadata_location_precision": "Précision du positionnement",
137
+ "metadata_quality": "Score de qualité",
138
+ "metadata_quality_help": "En savoir plus sur le score de qualité",
139
+ "metadata_quality_score": "Note globale",
140
+ "metadata_quality_gps_score": "Note du positionnement",
141
+ "metadata_quality_resolution_score": "Note de la résolution",
142
+ "metadata_quality_missing": "pas d'info dans l'image",
133
143
  "metadata_exif": "EXIF / XMP",
134
144
  "metadata_exif_name": "Balise",
135
145
  "metadata_exif_value": "Valeur",
package/src/utils/API.js CHANGED
@@ -76,6 +76,22 @@ export default class API {
76
76
  return this._isReady == 1;
77
77
  }
78
78
 
79
+ /**
80
+ * List of available features offered by API
81
+ * @returns {string[]} Keywords of enabled features
82
+ */
83
+ getAvailableFeatures() {
84
+ return Object.entries(this._endpoints).filter(e => e[1] !== null).map(e => e[0]);
85
+ }
86
+
87
+ /**
88
+ * List of unavailable features on API
89
+ * @returns {string[]} Keywords of disabled features
90
+ */
91
+ getUnavailableFeatures() {
92
+ return Object.entries(this._endpoints).filter(e => e[1] === null).map(e => e[0]);
93
+ }
94
+
79
95
  /**
80
96
  * Interprets JSON landing page and store important information
81
97
  *
@@ -107,7 +123,7 @@ export default class API {
107
123
  // Read metadata
108
124
  this._metadata.name = landing.title || "Unnamed";
109
125
  this._metadata.stac_version = landing.stac_version;
110
- this._metadata.geovisio_version = landing.geovisio_version || "Unknown";
126
+ this._metadata.geovisio_version = landing.geovisio_version;
111
127
 
112
128
  // Read links
113
129
  const supportedLinks = [
@@ -243,7 +259,7 @@ export default class API {
243
259
  const mapUsers = new Set(users || []);
244
260
 
245
261
  // Load all necessary map styles
246
- this.mapStyle = { version: 8, sources: {}, layers: [] };
262
+ this.mapStyle = { version: 8, sources: {}, layers: [], metadata: {} };
247
263
  const stylePromises = [ this.getMapStyle() ];
248
264
 
249
265
  // General map style
@@ -263,7 +279,7 @@ export default class API {
263
279
  return Promise.all(stylePromises)
264
280
  .then(styles => {
265
281
  const overridableProps = [
266
- "bearing", "center", "glyphs", "light", "metadata", "name",
282
+ "bearing", "center", "glyphs", "light", "name",
267
283
  "pitch", "sky", "sprite", "terrain", "transition", "zoom"
268
284
  ];
269
285
 
@@ -272,6 +288,7 @@ export default class API {
272
288
  if(style[p]) { this.mapStyle[p] = style[p] || this.mapStyle[p]; }
273
289
  });
274
290
  Object.assign(this.mapStyle.sources, style?.sources || {});
291
+ Object.assign(this.mapStyle.metadata, style?.metadata || {});
275
292
  this.mapStyle.layers = this.mapStyle.layers.concat(style?.layers || []);
276
293
  });
277
294
  })
package/src/utils/Exif.js CHANGED
@@ -41,17 +41,12 @@ export function getExifFloat(val) {
41
41
  * @private
42
42
  */
43
43
  export function getGPSPrecision(picture) {
44
- let quality = "unknown";
45
- const gpsHPosError = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSHPositioningError"]);
44
+ let quality = "";
45
+ const gpsHPosError = picture?.properties?.["quality:horizontal_accuracy"] || getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSHPositioningError"]);
46
46
  const gpsDop = getExifFloat(picture?.properties?.exif?.["Exif.GPSInfo.GPSDOP"]);
47
47
 
48
48
  if(gpsHPosError !== undefined) {
49
- if(gpsHPosError < 0.5) { quality = "ideal"; }
50
- else if(gpsHPosError < 1) { quality = "excellent"; }
51
- else if(gpsHPosError < 3) { quality = "good"; }
52
- else if(gpsHPosError < 7) { quality = "moderate"; }
53
- else if(gpsHPosError < 10) { quality = "fair"; }
54
- else { quality = "poor"; }
49
+ quality = `${gpsHPosError} m`;
55
50
  }
56
51
  else if(gpsDop !== undefined) {
57
52
  if(gpsDop < 1) { quality = "ideal"; }
package/src/utils/Map.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // DO NOT REMOVE THE "!": bundled builds breaks otherwise !!!
2
2
  import maplibregl from "!maplibre-gl";
3
3
  import LoaderImg from "../img/marker.svg";
4
- import { COLORS } from "./Utils";
4
+ import { COLORS, QUALITYSCORE_RES_FLAT_VALUES, QUALITYSCORE_RES_360_VALUES, QUALITYSCORE_GPS_VALUES, QUALITYSCORE_POND_RES, QUALITYSCORE_POND_GPS } from "./Utils";
5
5
  import { autoDetectLocale } from "./I18n";
6
6
 
7
7
  export const DEFAULT_TILES = "https://panoramax.openstreetmap.fr/pmtiles/basic.json";
@@ -63,6 +63,24 @@ export const VECTOR_STYLES = {
63
63
  }
64
64
  };
65
65
 
66
+
67
+ // See MapLibre docs for explanation of expressions magic: https://maplibre.org/maplibre-style-spec/expressions/
68
+ const MAP_EXPR_QUALITYSCORE_RES_360 = ["case", ["has", "h_pixel_density"], ["step", ["get", "h_pixel_density"], ...QUALITYSCORE_RES_360_VALUES], 1];
69
+ const MAP_EXPR_QUALITYSCORE_RES_FLAT = ["case", ["has", "h_pixel_density"], ["step", ["get", "h_pixel_density"], ...QUALITYSCORE_RES_FLAT_VALUES], 1];
70
+ const MAP_EXPR_QUALITYSCORE_RES = [
71
+ "case", ["==", ["get", "type"], "equirectangular"],
72
+ MAP_EXPR_QUALITYSCORE_RES_360, MAP_EXPR_QUALITYSCORE_RES_FLAT
73
+ ];
74
+ const MAP_EXPR_QUALITYSCORE_GPS = ["case", ["has", "gps_accuracy"], ["step", ["get", "gps_accuracy"], ...QUALITYSCORE_GPS_VALUES], 1];
75
+ // Note: score is also calculated in widgets/popup code
76
+ export const MAP_EXPR_QUALITYSCORE = [
77
+ "round",
78
+ ["+",
79
+ ["*", MAP_EXPR_QUALITYSCORE_RES, QUALITYSCORE_POND_RES],
80
+ ["*", MAP_EXPR_QUALITYSCORE_GPS, QUALITYSCORE_POND_GPS]]
81
+ ];
82
+
83
+
66
84
  /**
67
85
  * Get the GIF shown while thumbnail loads
68
86
  * @param {object} lang Translations
@@ -114,6 +132,7 @@ export function combineStyles(parent, options) {
114
132
  // Complementary style
115
133
  if(options.supplementaryStyle) {
116
134
  Object.assign(style.sources, options.supplementaryStyle.sources || {});
135
+ Object.assign(style.metadata, options.supplementaryStyle.metadata || {});
117
136
  style.layers = style.layers.concat(options.supplementaryStyle.layers || []);
118
137
  }
119
138
 
@@ -23,9 +23,45 @@ export const COLORS_HEX = Object.fromEntries(Object.entries(COLORS).map(e => {
23
23
  return e;
24
24
  }));
25
25
 
26
+ export const QUALITYSCORE_VALUES = [
27
+ { color: "#007f4e", label: "A" },
28
+ { color: "#72b043", label: "B" },
29
+ { color: "#b5be2f", label: "C" },
30
+ { color: "#f8cc1b", label: "D" },
31
+ { color: "#f6a020", label: "E" },
32
+ ];
33
+
34
+ export const QUALITYSCORE_RES_FLAT_VALUES = [1, 15, 2, 38, 3, 60, 4]; // Grade, < Px/FOV value
35
+ export const QUALITYSCORE_RES_360_VALUES = [2, 15, 3, 20, 4, 38, 5]; // Grade, < Px/FOV value
36
+ export const QUALITYSCORE_GPS_VALUES = [5, 1.01, 4, 2.01, 3, 5.01, 2, 10.01, 1]; // Grade, < Meters value
37
+ export const QUALITYSCORE_POND_RES = 4/5;
38
+ export const QUALITYSCORE_POND_GPS = 1/5;
39
+
26
40
  const ArrowTriangle = svgToPSVLink(ArrowTriangleSVG, "white");
27
41
  const ArrowTurn = svgToPSVLink(ArrowTurnSVG, COLORS.NEXT);
28
42
 
43
+
44
+ /**
45
+ * Find the grade associated to an input Quality Score definition.
46
+ * @param {number[]} ranges The QUALITYSCORE_*_VALUES definition
47
+ * @param {number} value The picture value
48
+ * @return {number} The corresponding grade (1 to 5, or null if missing)
49
+ */
50
+ export function getGrade(ranges, value) {
51
+ if(value === null || value === undefined || value === "") { return null; }
52
+
53
+ // Read each pair from table (grade, reference value)
54
+ for(let i = 0; i < ranges.length; i += 2) {
55
+ const grade = ranges[i];
56
+ const limit = ranges[i+1];
57
+
58
+ // Send grade if value is under limit
59
+ if (value < limit) { return grade;}
60
+ }
61
+ // Otherwise, send last grade
62
+ return ranges[ranges.length - 1];
63
+ }
64
+
29
65
  /**
30
66
  * Get cartesian distance between two points
31
67
  * @param {number[]} from Start [x,y] coordinates
@@ -7,6 +7,9 @@ import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons/faMagnifyin
7
7
  import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
8
8
  import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
9
9
  import { faCopy } from "@fortawesome/free-solid-svg-icons/faCopy";
10
+ import { faStar } from "@fortawesome/free-solid-svg-icons/faStar";
11
+ import { faStar as farStar } from "@fortawesome/free-regular-svg-icons/faStar";
12
+ import { QUALITYSCORE_VALUES } from "./Utils";
10
13
 
11
14
 
12
15
  /**
@@ -479,3 +482,47 @@ export function createLabel(forAttr, text, faIcon = null) {
479
482
  label.appendChild(document.createTextNode(text));
480
483
  return label;
481
484
  }
485
+
486
+ /**
487
+ * Show a grade in a nice, user-friendly way
488
+ * @param {number} grade The obtained grade
489
+ * @returns {string} Nice to display grade display
490
+ */
491
+ export function showGrade(grade, t) {
492
+ let label = "<span class=\"gvs-grade\">";
493
+
494
+ for(let i=1; i <= grade; i++) {
495
+ label += fat(faStar);
496
+ }
497
+ for(let i=grade+1; i <= 5; i++) {
498
+ label += fat(farStar);
499
+ }
500
+
501
+ label += "</span> (";
502
+ if(grade === null) { label += t.gvs.metadata_quality_missing+")"; }
503
+ else { label += grade + "/5)"; }
504
+ return label;
505
+ }
506
+
507
+ /**
508
+ * Displays a nice QualityScore
509
+ * @param {number} grade The 1 to 5 grade
510
+ * @returns {Element} The HTML code for showing the grade
511
+ */
512
+ export function showQualityScore(grade) {
513
+ const span = document.createElement("span");
514
+
515
+ for(let i=1; i <= QUALITYSCORE_VALUES.length; i++) {
516
+ const pv = QUALITYSCORE_VALUES[i-1];
517
+ const sub = document.createElement("span");
518
+ sub.appendChild(document.createTextNode(pv.label));
519
+ sub.classList.add("gvs-qualityscore");
520
+ sub.style.backgroundColor = pv.color;
521
+ if(i === (6-grade)) {
522
+ sub.classList.add("gvs-qualityscore-selected");
523
+ }
524
+ span.appendChild(sub);
525
+ }
526
+
527
+ return span;
528
+ }
@@ -4,6 +4,7 @@ const MAP_FILTERS_JS2URL = {
4
4
  "type": "pic_type",
5
5
  "camera": "camera",
6
6
  "theme": "theme",
7
+ "qualityscore": "pic_score",
7
8
  };
8
9
  const MAP_FILTERS_URL2JS = Object.fromEntries(Object.entries(MAP_FILTERS_JS2URL).map(v => [v[1], v[0]]));
9
10
  const UPDATE_HASH_EVENTS = [
@@ -98,6 +99,10 @@ export default class URLHash extends EventTarget {
98
99
  hashParts[MAP_FILTERS_JS2URL[k]] = this._viewer._mapFilters[k];
99
100
  }
100
101
  }
102
+ if(hashParts.pic_score) {
103
+ const mapping = [null, "E", "D", "C", "B", "A"];
104
+ hashParts.pic_score = hashParts.pic_score.map(v => mapping[v]).join("");
105
+ }
101
106
  }
102
107
  }
103
108
  else {
@@ -164,7 +169,7 @@ export default class URLHash extends EventTarget {
164
169
 
165
170
  keyvals = {};
166
171
 
167
- // Used letters: b c d e f k m n p s t u v
172
+ // Used letters: b c d e f k m n p q s t u v
168
173
  // Focus
169
174
  if(shortVals.f === "m") { keyvals.focus = "map"; }
170
175
  else if(shortVals.f === "p") { keyvals.focus = "pic"; }
@@ -202,6 +207,7 @@ export default class URLHash extends EventTarget {
202
207
  if(shortVals.v === "d") { keyvals.theme = "default"; }
203
208
  else if(shortVals.v === "a") { keyvals.theme = "age"; }
204
209
  else if(shortVals.v === "t") { keyvals.theme = "type"; }
210
+ else if(shortVals.v === "s") { keyvals.theme = "score"; }
205
211
 
206
212
  // Background
207
213
  if(shortVals.b === "s") { keyvals.background = "streets"; }
@@ -209,6 +215,9 @@ export default class URLHash extends EventTarget {
209
215
 
210
216
  // Users
211
217
  if(shortVals.u !== "") { keyvals.users = shortVals.u; }
218
+
219
+ // Photoscore
220
+ if(shortVals.q !== "") { keyvals.pic_score = shortVals.q; }
212
221
  }
213
222
 
214
223
  return keyvals;
@@ -332,6 +341,7 @@ export default class URLHash extends EventTarget {
332
341
  v: (hashParts.theme || "").substring(0, 1),
333
342
  b: (hashParts.background || "").substring(0, 1),
334
343
  u: hashParts.users,
344
+ q: hashParts.pic_score,
335
345
  };
336
346
  const short = Object.entries(shortVals)
337
347
  .filter(([,v]) => v != undefined && v != "")
@@ -353,6 +363,11 @@ export default class URLHash extends EventTarget {
353
363
  newMapFilters[MAP_FILTERS_URL2JS[k]] = vals[k];
354
364
  }
355
365
  }
366
+ if(newMapFilters.qualityscore) {
367
+ let values = newMapFilters.qualityscore.split("");
368
+ const mapping = {"A": 5, "B": 4, "C": 3, "D": 2, "E": 1};
369
+ newMapFilters.qualityscore = values.map(v => mapping[v]);
370
+ }
356
371
  return newMapFilters;
357
372
  }
358
373