@panoramax/web-viewer 3.1.1 → 3.2.0-develop-8f79d734

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 (38) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/build/index.css +2 -2
  3. package/build/index.css.map +1 -1
  4. package/build/index.js +6 -6
  5. package/build/index.js.map +1 -1
  6. package/docs/02_Usage.md +267 -224
  7. package/docs/03_URL_settings.md +10 -1
  8. package/docs/05_Compatibility.md +1 -0
  9. package/package.json +4 -3
  10. package/src/Viewer.js +51 -3
  11. package/src/components/CoreView.css +6 -0
  12. package/src/components/CoreView.js +25 -10
  13. package/src/components/Map.js +41 -2
  14. package/src/components/Photo.css +5 -0
  15. package/src/translations/de.json +28 -11
  16. package/src/translations/en.json +22 -11
  17. package/src/translations/es.json +23 -11
  18. package/src/translations/fr.json +22 -11
  19. package/src/translations/hu.json +117 -73
  20. package/src/translations/nl.json +73 -1
  21. package/src/translations/pl.json +1 -0
  22. package/src/translations/zh_Hant.json +53 -9
  23. package/src/utils/API.js +20 -3
  24. package/src/utils/Exif.js +5 -10
  25. package/src/utils/Map.js +56 -10
  26. package/src/utils/Utils.js +68 -24
  27. package/src/utils/Widgets.js +64 -2
  28. package/src/viewer/URLHash.js +18 -3
  29. package/src/viewer/Widgets.css +139 -0
  30. package/src/viewer/Widgets.js +207 -42
  31. package/tests/Viewer.test.js +1 -0
  32. package/tests/__snapshots__/Editor.test.js.snap +1 -5
  33. package/tests/components/__snapshots__/Photo.test.js.snap +2 -10
  34. package/tests/utils/Exif.test.js +10 -10
  35. package/tests/utils/Map.test.js +8 -0
  36. package/tests/utils/__snapshots__/API.test.js.snap +5 -0
  37. package/tests/utils/__snapshots__/Widgets.test.js.snap +1 -1
  38. package/tests/viewer/__snapshots__/Widgets.test.js.snap +39 -29
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
@@ -109,10 +127,12 @@ export function combineStyles(parent, options) {
109
127
 
110
128
  // Complete styles
111
129
  style.layers = style.layers.concat(getMissingLayerStyles(style.sources, style.layers));
130
+ if(!style.metadata) { style.metadata = {}; }
112
131
 
113
132
  // Complementary style
114
133
  if(options.supplementaryStyle) {
115
134
  Object.assign(style.sources, options.supplementaryStyle.sources || {});
135
+ Object.assign(style.metadata, options.supplementaryStyle.metadata || {});
116
136
  style.layers = style.layers.concat(options.supplementaryStyle.layers || []);
117
137
  }
118
138
 
@@ -155,13 +175,15 @@ export function combineStyles(parent, options) {
155
175
  });
156
176
 
157
177
  // TODO : remove override once available in default Panoramax style
158
- if(!style.metadata["panoramax:locales"]) {
178
+ if(!style.metadata?.["panoramax:locales"]) {
159
179
  style.metadata["panoramax:locales"] = ["fr", "en", "de", "es", "ru", "pt", "zh", "hi", "latin"];
160
180
  }
161
181
 
162
182
  // Override labels to use appropriate language
163
183
  if(style.metadata["panoramax:locales"]) {
164
- const prefLang = autoDetectLocale(style.metadata["panoramax:locales"], "latin");
184
+ let prefLang = parent._options.lang || autoDetectLocale(style.metadata["panoramax:locales"], "latin");
185
+ if(prefLang.includes("-")) { prefLang = prefLang.split("-")[0]; }
186
+ if(prefLang.includes("_")) { prefLang = prefLang.split("_")[0]; }
165
187
  style.layers.forEach(l => {
166
188
  if(isLabelLayer(l) && l.layout["text-field"].includes("name:latin")) {
167
189
  l.layout["text-field"] = [
@@ -179,18 +201,19 @@ export function combineStyles(parent, options) {
179
201
  let capitalLayer = style.layers.find(l => l.id == "place_label_capital");
180
202
  if(citiesLayer && !capitalLayer) {
181
203
  // Create capital layer from original city style
204
+ citiesLayer.paint = {
205
+ "text-color": "hsl(0, 0%, 0%)",
206
+ "text-halo-blur": 0,
207
+ "text-halo-color": "hsla(0, 0%, 100%, 1)",
208
+ "text-halo-width": 3,
209
+ };
210
+ citiesLayer.layout["text-letter-spacing"] = 0.1;
182
211
  capitalLayer = JSON.parse(JSON.stringify(citiesLayer));
183
212
  capitalLayer.id = "place_label_capital";
184
213
  capitalLayer.filter.push(["<=", "capital", 2]);
185
214
 
186
215
  // Edit original city to make it less import
187
216
  citiesLayer.filter.push([">", "capital", 2]);
188
- citiesLayer.paint = {
189
- "text-color": "hsl(0,0%,15%)",
190
- "text-halo-blur": 0.5,
191
- "text-halo-color": "hsl(0,0%,100%)",
192
- "text-halo-width": 0.8,
193
- };
194
217
  style.layers.push(capitalLayer);
195
218
  }
196
219
 
@@ -317,7 +340,6 @@ export function getMissingLayerStyles(sources, layers) {
317
340
  l.layout = Object.assign(l.layout || {}, VECTOR_STYLES.PICTURES.layout);
318
341
  });
319
342
 
320
-
321
343
  return newLayers;
322
344
  }
323
345
 
@@ -342,6 +364,30 @@ export function getUserSourceId(userId) {
342
364
  return userId === "geovisio" ? "geovisio" : "geovisio_"+userId;
343
365
  }
344
366
 
367
+ /**
368
+ * Switches used coef value in MapLibre style JSON expression
369
+ * @param {*} expr The MapLibre style expression
370
+ * @param {string} newCoefVal The new coef value to use
371
+ * @returns {*} The switched expression
372
+ * @private
373
+ */
374
+ export function switchCoefValue(expr, newCoefVal) {
375
+ if(Array.isArray(expr)) {
376
+ return expr.map(v => switchCoefValue(v, newCoefVal));
377
+ }
378
+ else if(typeof expr === "object" && expr !== null) {
379
+ const newExpr = {};
380
+ for (const key in expr) {
381
+ newExpr[key] = switchCoefValue(expr[key], newCoefVal);
382
+ }
383
+ return newExpr;
384
+ }
385
+ else if(typeof expr === "string" && expr.startsWith("coef")) {
386
+ return newCoefVal;
387
+ }
388
+ return expr;
389
+ }
390
+
345
391
  /**
346
392
  * Transforms a set of parameters into an URL-ready string
347
393
  * It also removes null/undefined values
@@ -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
@@ -350,7 +386,7 @@ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=n
350
386
  };
351
387
  }
352
388
  // 360°
353
- else if(is360) {
389
+ else if(is360 && matrix) {
354
390
  panorama = {
355
391
  baseUrl,
356
392
  origBaseUrl: baseUrl,
@@ -395,7 +431,7 @@ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=n
395
431
  };
396
432
  }
397
433
 
398
- return {
434
+ const node = {
399
435
  id: f.id,
400
436
  caption: getNodeCaption(f, t),
401
437
  panorama,
@@ -410,6 +446,8 @@ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=n
410
446
  horizontalFov,
411
447
  properties: f.properties,
412
448
  };
449
+
450
+ return node;
413
451
  }
414
452
 
415
453
  /**
@@ -514,29 +552,35 @@ export function isInternetFast() {
514
552
  }
515
553
  // Fallback for other browsers
516
554
  catch(e) {
517
- // Check if test has been done before and stored
518
- const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
519
- if(["true", "false"].includes(isFast)) {
520
- return Promise.resolve(isFast === "true");
521
- }
555
+ try {
556
+ // Check if test has been done before and stored
557
+ const isFast = sessionStorage.getItem(INTERNET_FAST_STORAGE);
558
+ if(["true", "false"].includes(isFast)) {
559
+ return Promise.resolve(isFast === "true");
560
+ }
522
561
 
523
- // Run download testing
524
- const startTime = (new Date()).getTime();
525
- return fetch(INTERNET_FAST_TESTFILE+"?nocache="+startTime)
526
- .then(async res => [res, await res.blob()])
527
- .then(([res, blob]) => {
528
- const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
529
- const endTime = (new Date()).getTime();
530
- const duration = (endTime - startTime) / 1000; // Transfer time in seconds
531
- const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
532
- const isFast = speed >= INTERNET_FAST_THRESHOLD;
533
- sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
534
- return isFast;
535
- })
536
- .catch(e => {
537
- console.warn("Failed to run speedtest", e);
538
- return false;
539
- });
562
+ // Run download testing
563
+ const startTime = (new Date()).getTime();
564
+ return fetch(INTERNET_FAST_TESTFILE+"?nocache="+startTime)
565
+ .then(async res => [res, await res.blob()])
566
+ .then(([res, blob]) => {
567
+ const size = parseInt(res.headers.get("Content-Length") || blob.size); // Bytes
568
+ const endTime = (new Date()).getTime();
569
+ const duration = (endTime - startTime) / 1000; // Transfer time in seconds
570
+ const speed = (size * 8 / 1024 / 1024) / duration; // MBits/s
571
+ const isFast = speed >= INTERNET_FAST_THRESHOLD;
572
+ sessionStorage.setItem(INTERNET_FAST_STORAGE, isFast ? "true" : "false");
573
+ return isFast;
574
+ })
575
+ .catch(e => {
576
+ console.warn("Failed to run speedtest", e);
577
+ return false;
578
+ });
579
+ }
580
+ // Fallback for browser blocking third-party downloads or sessionStorage
581
+ catch(e) {
582
+ return Promise.resolve(false);
583
+ }
540
584
  }
541
585
  }
542
586
 
@@ -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
  /**
@@ -59,6 +62,19 @@ export function createExpandableButton(id, icon, label, container, classes = [])
59
62
  btn.title = label;
60
63
  }
61
64
  btn.classList.add("gvs-btn", "gvs-widget-bg", "gvs-btn-expandable", ...classes);
65
+ btn.setActive = val => {
66
+ let span = btn.querySelector(".gvs-filters-active");
67
+ if(val && !span) {
68
+ span = document.createElement("span");
69
+ span.classList.add("gvs-filters-active");
70
+ const svg = btn.querySelector("svg");
71
+ if(svg.nextSibling) { btn.insertBefore(span, svg.nextSibling); }
72
+ else { btn.appendChild(span); }
73
+ }
74
+ else if(!val && span) {
75
+ span.remove();
76
+ }
77
+ };
62
78
  return btn;
63
79
  }
64
80
 
@@ -95,6 +111,7 @@ export function createSearchBar(
95
111
  const input = document.createElement("input");
96
112
  input.type = "text";
97
113
  input.placeholder = placeholder;
114
+ input.id = `${id}-input`;
98
115
  bar.appendChild(input);
99
116
  const extendInput = () => {
100
117
  bar.classList.remove("gvs-search-bar-reduced");
@@ -138,7 +155,7 @@ export function createSearchBar(
138
155
  bar.resetSearch = resetSearch;
139
156
 
140
157
  // Handle result item click
141
- const goItem = (entry) => {
158
+ input.goItem = (entry) => {
142
159
  if(reduced) {
143
160
  onResultClick(entry);
144
161
  resetSearch();
@@ -148,6 +165,7 @@ export function createSearchBar(
148
165
  input.value = entry.title;
149
166
  list.innerHTML = "";
150
167
  list._toggle(false);
168
+ switchIcon(iconEmpty);
151
169
  onResultClick(entry);
152
170
  }
153
171
  };
@@ -195,7 +213,7 @@ export function createSearchBar(
195
213
  listEntry.classList.add("gvs-search-bar-result");
196
214
  listEntry.innerHTML = `${entry.title}<br /><small>${entry?.subtitle || ""}</small>`;
197
215
  list.appendChild(listEntry);
198
- listEntry.addEventListener("click", () => goItem(entry));
216
+ listEntry.addEventListener("click", () => input.goItem(entry));
199
217
  });
200
218
  }).catch(e => {
201
219
  console.error(e);
@@ -479,3 +497,47 @@ export function createLabel(forAttr, text, faIcon = null) {
479
497
  label.appendChild(document.createTextNode(text));
480
498
  return label;
481
499
  }
500
+
501
+ /**
502
+ * Show a grade in a nice, user-friendly way
503
+ * @param {number} grade The obtained grade
504
+ * @returns {string} Nice to display grade display
505
+ */
506
+ export function showGrade(grade, t) {
507
+ let label = "<span class=\"gvs-grade\">";
508
+
509
+ for(let i=1; i <= grade; i++) {
510
+ label += fat(faStar);
511
+ }
512
+ for(let i=grade+1; i <= 5; i++) {
513
+ label += fat(farStar);
514
+ }
515
+
516
+ label += "</span> (";
517
+ if(grade === null) { label += t.gvs.metadata_quality_missing+")"; }
518
+ else { label += grade + "/5)"; }
519
+ return label;
520
+ }
521
+
522
+ /**
523
+ * Displays a nice QualityScore
524
+ * @param {number} grade The 1 to 5 grade
525
+ * @returns {Element} The HTML code for showing the grade
526
+ */
527
+ export function showQualityScore(grade) {
528
+ const span = document.createElement("span");
529
+
530
+ for(let i=1; i <= QUALITYSCORE_VALUES.length; i++) {
531
+ const pv = QUALITYSCORE_VALUES[i-1];
532
+ const sub = document.createElement("span");
533
+ sub.appendChild(document.createTextNode(pv.label));
534
+ sub.classList.add("gvs-qualityscore");
535
+ sub.style.backgroundColor = pv.color;
536
+ if(i === (6-grade)) {
537
+ sub.classList.add("gvs-qualityscore-selected");
538
+ }
539
+ span.appendChild(sub);
540
+ }
541
+
542
+ return span;
543
+ }
@@ -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 {
@@ -158,13 +163,13 @@ export default class URLHash extends EventTarget {
158
163
  if(keyvals.s) {
159
164
  const shortVals = Object.fromEntries(
160
165
  keyvals.s
161
- .split("|")
166
+ .split(";")
162
167
  .map(kv => [kv[0], kv.substring(1)])
163
168
  );
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,11 +341,12 @@ 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 != "")
338
348
  .map(([k,v]) => `${k}${v}`)
339
- .join("|");
349
+ .join(";");
340
350
  url.hash = `s=${short}`;
341
351
  return url;
342
352
  }
@@ -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
 
@@ -207,6 +207,63 @@ span.gvs-input-btn {
207
207
  width: 100%;
208
208
  }
209
209
 
210
+ /* Checkbox looking like buttons */
211
+ .gvs-input-group.gvs-checkbox-btns {
212
+ gap: 0;
213
+ }
214
+ .gvs-checkbox-btns label {
215
+ display: inline-block;
216
+ padding: 2px 7px;
217
+ background: none;
218
+ border: 1px solid var(--widget-border-btn);
219
+ color: var(--widget-font-btn-direct);
220
+ cursor: pointer;
221
+ font-size: 16px;
222
+ text-decoration: none;
223
+ border-left-width: 0px;
224
+ }
225
+ .gvs-checkbox-btns label:hover {
226
+ background-color: var(--widget-bg-hover);
227
+ }
228
+ .gvs-checkbox-btns label:first-of-type {
229
+ border-top-left-radius: 8px;
230
+ border-bottom-left-radius: 8px;
231
+ border-left-width: 1px;
232
+ }
233
+ .gvs-checkbox-btns label:last-of-type {
234
+ border-top-right-radius: 8px;
235
+ border-bottom-right-radius: 8px;
236
+ }
237
+ .gvs-checkbox-btns input[type="checkbox"] { display: none; }
238
+ .gvs-checkbox-btns input[type="checkbox"]:checked + label {
239
+ background-color: var(--widget-bg-active);
240
+ color: var(--widget-font-active);
241
+ }
242
+ .gvs-checkbox-btns input[type="checkbox"]:checked + label:first-of-type {
243
+ border-right-color: white;
244
+ }
245
+
246
+ /* Input shortcuts */
247
+ .gvs-input-shortcuts {
248
+ margin-top: -10px;
249
+ margin-bottom: 5px;
250
+ }
251
+ .gvs-input-shortcuts button {
252
+ border: none;
253
+ height: 20px;
254
+ line-height: 20px;
255
+ font-size: 11px;
256
+ padding: 0 8px;
257
+ vertical-align: middle;
258
+ background-color: var(--grey-pale);
259
+ color: var(--black);
260
+ border-radius: 10px;
261
+ cursor: pointer;
262
+ }
263
+ .gvs-input-shortcuts button:hover {
264
+ background-color: #d9dcd9;
265
+ }
266
+
210
267
 
211
268
  /* Group */
212
269
  .gvs-group {
@@ -455,6 +512,11 @@ a.gvs-btn { text-decoration: none; }
455
512
  margin-right: 5px;
456
513
  }
457
514
 
515
+ /* Grades */
516
+ .gvs-grade {
517
+ color: var(--orange);
518
+ }
519
+
458
520
 
459
521
  /***********************************************
460
522
  * Per-component styles
@@ -586,10 +648,25 @@ a.gvs-btn { text-decoration: none; }
586
648
 
587
649
  /* Filters */
588
650
  #gvs-filter { margin-bottom: 5px; }
651
+ .gvs-filters-active {
652
+ width: 15px;
653
+ height: 15px;
654
+ border-radius: 8px;
655
+ border: 3px solid white;
656
+ position: absolute;
657
+ left: 20px;
658
+ top: 5px;
659
+ background-color: var(--orange);
660
+ }
589
661
  #gvs-filter-panel {
590
662
  width: 350px;
591
663
  max-width: 350px;
592
664
  }
665
+ #gvs-filter-panel .gvs-filter-active {
666
+ background-color: var(--widget-bg-active);
667
+ border-color: var(--widget-bg-active);
668
+ color: var(--widget-font-active);
669
+ }
593
670
  #gvs-filter-panel input[type=date] {
594
671
  min-width: 0;
595
672
  flex-grow: 2;
@@ -597,10 +674,67 @@ a.gvs-btn { text-decoration: none; }
597
674
  text-align: center;
598
675
  }
599
676
  #gvs-filter-camera-model, #gvs-filter-search-user { width: 100%; }
677
+ #gvs-filter-search-user.gvs-filter-active input {
678
+ color: var(--widget-font-active);
679
+ background: none;
680
+ }
600
681
  #gvs-filter-zoomin {
601
682
  text-align: center;
602
683
  font-weight: bold;
603
684
  margin-bottom: 15px;
685
+ color: var(--orange);
686
+ }
687
+
688
+ #gvs-filter-qualityscore {
689
+ gap: 0px;
690
+ justify-content: center;
691
+ height: 42px;
692
+ }
693
+
694
+ .gvs-qualityscore, #gvs-filter-qualityscore label {
695
+ font-size: 18px;
696
+ width: 25px;
697
+ height: 30px;
698
+ line-height: 26px;
699
+ display: inline-block;
700
+ border: 1px solid white;
701
+ text-align: center;
702
+ background-color: gray;
703
+ color: rgba(255,255,255,0.9);
704
+ font-family: sans-serif;
705
+ font-weight: bold;
706
+ vertical-align: middle;
707
+ }
708
+ #gvs-filter-qualityscore label { cursor: pointer; }
709
+ #gvs-filter-qualityscore label:hover {
710
+ width: 28px;
711
+ height: 35px;
712
+ line-height: 30px;
713
+ border-radius: 3px;
714
+ font-size: 22px;
715
+ color: white;
716
+ border: 2px solid white;
717
+ }
718
+ .gvs-qualityscore:first-of-type,
719
+ #gvs-filter-qualityscore label:first-of-type {
720
+ border-top-left-radius: 8px;
721
+ border-bottom-left-radius: 8px;
722
+ }
723
+ .gvs-qualityscore:last-of-type,
724
+ #gvs-filter-qualityscore label:last-of-type {
725
+ border-top-right-radius: 8px;
726
+ border-bottom-right-radius: 8px;
727
+ }
728
+ #gvs-filter-qualityscore input[type="checkbox"] { display: none; }
729
+ .gvs-qualityscore-selected,
730
+ #gvs-filter-qualityscore input[type="checkbox"]:checked + label {
731
+ width: 30px;
732
+ height: 42px;
733
+ line-height: 37px;
734
+ border-radius: 8px;
735
+ font-size: 27px;
736
+ color: white;
737
+ border: 2px solid white;
604
738
  }
605
739
 
606
740
 
@@ -650,6 +784,11 @@ a.gvs-btn { text-decoration: none; }
650
784
  margin-right: 5px;
651
785
  }
652
786
 
787
+ #gvs-map-theme-legend-score {
788
+ justify-content: center;
789
+ gap: 0;
790
+ }
791
+
653
792
 
654
793
  /* Player */
655
794
  #gvs-widget-player { justify-content: center; }