geovisio 2.5.0__py3-none-any.whl → 2.7.0__py3-none-any.whl

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 (59) hide show
  1. geovisio/__init__.py +38 -8
  2. geovisio/admin_cli/__init__.py +2 -2
  3. geovisio/admin_cli/db.py +8 -0
  4. geovisio/config_app.py +64 -0
  5. geovisio/db_migrations.py +24 -3
  6. geovisio/templates/main.html +14 -14
  7. geovisio/templates/viewer.html +3 -3
  8. geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
  9. geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
  10. geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
  11. geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
  12. geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
  13. geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
  14. geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
  15. geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
  16. geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
  17. geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
  18. geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
  19. geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
  20. geovisio/translations/messages.pot +686 -0
  21. geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
  22. geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
  23. geovisio/utils/__init__.py +1 -1
  24. geovisio/utils/auth.py +50 -11
  25. geovisio/utils/db.py +65 -0
  26. geovisio/utils/excluded_areas.py +83 -0
  27. geovisio/utils/extent.py +30 -0
  28. geovisio/utils/fields.py +1 -1
  29. geovisio/utils/filesystems.py +0 -1
  30. geovisio/utils/link.py +14 -0
  31. geovisio/utils/params.py +20 -0
  32. geovisio/utils/pictures.py +94 -69
  33. geovisio/utils/reports.py +171 -0
  34. geovisio/utils/sequences.py +288 -126
  35. geovisio/utils/tokens.py +37 -42
  36. geovisio/utils/upload_set.py +654 -0
  37. geovisio/web/auth.py +50 -37
  38. geovisio/web/collections.py +305 -319
  39. geovisio/web/configuration.py +14 -0
  40. geovisio/web/docs.py +288 -12
  41. geovisio/web/excluded_areas.py +377 -0
  42. geovisio/web/items.py +203 -151
  43. geovisio/web/map.py +322 -106
  44. geovisio/web/params.py +69 -26
  45. geovisio/web/pictures.py +14 -31
  46. geovisio/web/reports.py +399 -0
  47. geovisio/web/rss.py +13 -7
  48. geovisio/web/stac.py +129 -121
  49. geovisio/web/tokens.py +105 -112
  50. geovisio/web/upload_set.py +768 -0
  51. geovisio/web/users.py +100 -73
  52. geovisio/web/utils.py +38 -9
  53. geovisio/workers/runner_pictures.py +278 -183
  54. geovisio-2.7.0.dist-info/METADATA +95 -0
  55. geovisio-2.7.0.dist-info/RECORD +66 -0
  56. geovisio-2.5.0.dist-info/METADATA +0 -115
  57. geovisio-2.5.0.dist-info/RECORD +0 -41
  58. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
  59. {geovisio-2.5.0.dist-info → geovisio-2.7.0.dist-info}/WHEEL +0 -0
geovisio/__init__.py CHANGED
@@ -1,19 +1,34 @@
1
1
  """GeoVisio API - Main"""
2
2
 
3
- __version__ = "2.5.0"
3
+ __version__ = "2.7.0"
4
4
 
5
5
  import os
6
- from flask import Flask, jsonify, stream_template, send_from_directory, redirect
6
+ from flask import Flask, jsonify, stream_template, send_from_directory, redirect, request
7
7
  from flask.cli import with_appcontext
8
8
  from flask_cors import CORS
9
9
  from flask_compress import Compress
10
+ from flask_babel import Babel
10
11
  from flasgger import Swagger
11
12
  import logging
12
13
  from logging.config import dictConfig
13
14
 
14
15
  from geovisio import db_migrations, config_app, admin_cli, errors, utils
15
- from geovisio.utils import filesystems, sentry
16
- from geovisio.web import auth, docs, pictures, stac, map, users, configuration, tokens, collections, items
16
+ from geovisio.utils import db, filesystems, sentry
17
+ from geovisio.web import (
18
+ auth,
19
+ docs,
20
+ pictures,
21
+ stac,
22
+ map,
23
+ users,
24
+ configuration,
25
+ tokens,
26
+ collections,
27
+ items,
28
+ upload_set,
29
+ reports,
30
+ excluded_areas,
31
+ )
17
32
  from geovisio.workers import runner_pictures
18
33
 
19
34
 
@@ -42,6 +57,14 @@ LOGGING_CONFIG = {
42
57
  dictConfig(LOGGING_CONFIG)
43
58
 
44
59
 
60
+ # Init i18n
61
+ def get_locale():
62
+ try:
63
+ return request.accept_languages.best_match(["de", "fr", "en"]) or "en"
64
+ except:
65
+ return "en"
66
+
67
+
45
68
  def create_app(test_config=None, app=None):
46
69
  """API launcher method"""
47
70
  #
@@ -54,13 +77,16 @@ def create_app(test_config=None, app=None):
54
77
 
55
78
  config_app.read_config(app, test_config)
56
79
  sentry.init(app)
80
+ db.create_db_pool(app)
81
+ Babel(app, locale_selector=get_locale)
57
82
 
58
83
  # Prepare filesystem
59
84
  createDirNoFailure(app.instance_path)
60
85
  app.config["FILESYSTEMS"] = filesystems.openFilesystemsFromConfig(app.config)
61
86
 
62
87
  # Check database connection and update its schema if needed
63
- db_migrations.update_db_schema(app.config["DB_URL"])
88
+ if app.config.get("DB_CHECK_SCHEMA"):
89
+ db_migrations.update_db_schema(app.config["DB_URL"])
64
90
 
65
91
  if app.config.get("OAUTH_PROVIDER"):
66
92
  utils.auth.make_auth(app)
@@ -77,7 +103,8 @@ def create_app(test_config=None, app=None):
77
103
 
78
104
  app.wsgi_app = ProxyFix(app.wsgi_app, x_for=nb_proxies, x_proto=nb_proxies, x_host=nb_proxies, x_prefix=nb_proxies)
79
105
 
80
- runner_pictures.background_processor.init_app(app)
106
+ # store the background processor in the app context
107
+ app.background_processor = runner_pictures.PictureBackgroundProcessor(app)
81
108
 
82
109
  #
83
110
  # List available routes/blueprints
@@ -91,6 +118,9 @@ def create_app(test_config=None, app=None):
91
118
  app.register_blueprint(users.bp)
92
119
  app.register_blueprint(configuration.bp)
93
120
  app.register_blueprint(tokens.bp)
121
+ app.register_blueprint(upload_set.bp)
122
+ app.register_blueprint(reports.bp)
123
+ app.register_blueprint(excluded_areas.bp)
94
124
 
95
125
  # Register CLI comands
96
126
  app.register_blueprint(admin_cli.bp, cli_group=None)
@@ -100,8 +130,8 @@ def create_app(test_config=None, app=None):
100
130
  def run_picture_worker():
101
131
  """Run a worker to process pictures after upload. Each worker use one thread, and several workers can be run in parallel"""
102
132
  logging.info("Running picture worker")
103
- worker = runner_pictures.PictureProcessor(config=app.config, stop=False)
104
- worker.process_next_pictures()
133
+ worker = runner_pictures.PictureProcessor(app=app, stop=False)
134
+ worker.process_jobs()
105
135
 
106
136
  #
107
137
  # API documentation
@@ -58,8 +58,8 @@ def cleanup_cmd(sequencesids, full, database, cache, permanent_pictures):
58
58
  @bp.cli.command("process-sequences")
59
59
  @with_appcontext
60
60
  def process_sequences():
61
- """Deprecated entry point, use https://gitlab.com/geovisio/cli to upload a sequence instead"""
62
- logging.error("This function has been deprecated, use https://gitlab.com/geovisio/cli to upload a sequence instead.")
61
+ """Deprecated entry point, use https://gitlab.com/panoramax/clients/cli to upload a sequence instead"""
62
+ logging.error("This function has been deprecated, use https://gitlab.com/panoramax/clients/cli to upload a sequence instead.")
63
63
  logging.error(
64
64
  "To upload a sequence with this tool, install it with `pip install geovisio_cli`, then run:\ngeovisio upload --path <directory> --api-url <api-url>"
65
65
  )
geovisio/admin_cli/db.py CHANGED
@@ -2,6 +2,7 @@ from flask import Blueprint, current_app
2
2
  from flask.cli import with_appcontext
3
3
  import click
4
4
  from geovisio import db_migrations
5
+ from geovisio.utils import sequences
5
6
 
6
7
  bp = Blueprint("db", __name__)
7
8
 
@@ -25,3 +26,10 @@ def upgrade():
25
26
  def rollback(all):
26
27
  """Rollbacks the latest database migration"""
27
28
  db_migrations.rollback_db_schema(current_app.config["DB_URL"], all)
29
+
30
+
31
+ @bp.cli.command("refresh")
32
+ @with_appcontext
33
+ def refresh():
34
+ """Refresh cached data (pictures_grid)"""
35
+ sequences.update_pictures_grid()
geovisio/config_app.py CHANGED
@@ -3,9 +3,23 @@ import os.path
3
3
  from urllib.parse import urlparse
4
4
  import datetime
5
5
  import logging
6
+ from typing import Optional, Dict
7
+ import croniter
8
+ from pydantic import BaseModel
9
+ from pydantic.color import Color
10
+ from pydantic.networks import HttpUrl
11
+ import json
12
+
13
+
14
+ class ApiSummary(BaseModel):
15
+ name: Dict[str, str] = {"en": "GeoVisio"}
16
+ description: Dict[str, str] = {"en": "The open source photo mapping solution"}
17
+ logo: HttpUrl = "https://gitlab.com/panoramax/gitlab-profile/-/raw/main/images/logo.svg"
18
+ color: Color = "#bf360c"
6
19
 
7
20
 
8
21
  class DefaultConfig:
22
+ API_SUMMARY = ApiSummary()
9
23
  API_VIEWER_PAGE = "viewer.html"
10
24
  API_MAIN_PAGE = "main.html"
11
25
  # we default we keep the session cookie 7 days, users would have to renew their loggin after this
@@ -14,9 +28,18 @@ class DefaultConfig:
14
28
  PICTURE_PROCESS_DERIVATES_STRATEGY = "ON_DEMAND"
15
29
  API_BLUR_URL = None
16
30
  PICTURE_PROCESS_THREADS_LIMIT = 1
31
+ DB_CHECK_SCHEMA = True # If True check the database schema, and do not start the api if not up to date
17
32
  API_PICTURES_LICENSE_SPDX_ID = None
18
33
  API_PICTURES_LICENSE_URL = None
19
34
  DEBUG_PICTURES_SKIP_FS_CHECKS_WITH_PUBLIC_URL = False
35
+ SESSION_COOKIE_HTTPONLY = False
36
+ PICTURE_PROCESS_REFRESH_CRON = (
37
+ "0 2 * * *" # Background worker will refresh by default some stats at 2 o'clock in the night (local time of the server)
38
+ )
39
+ DB_MIN_CNX = 0
40
+ DB_MAX_CNX = 10
41
+ DB_STATEMENT_TIMEOUT = 5 * 60 * 1000 # default statement timeout in ms (5mn)
42
+ API_ACCEPT_DUPLICATE = False
20
43
 
21
44
 
22
45
  def read_config(app, test_config):
@@ -38,7 +61,12 @@ def read_config(app, test_config):
38
61
  "DB_USERNAME",
39
62
  "DB_PASSWORD",
40
63
  "DB_NAME",
64
+ "DB_CHECK_SCHEMA",
65
+ "DB_MIN_CNX",
66
+ "DB_MAX_CNX",
67
+ "DB_STATEMENT_TIMEOUT",
41
68
  # API
69
+ "API_SUMMARY",
42
70
  "API_BLUR_URL",
43
71
  "API_VIEWER_PAGE",
44
72
  "API_MAIN_PAGE",
@@ -48,9 +76,12 @@ def read_config(app, test_config):
48
76
  "API_DERIVATES_PICTURES_PUBLIC_URL",
49
77
  "API_PICTURES_LICENSE_SPDX_ID",
50
78
  "API_PICTURES_LICENSE_URL",
79
+ "API_ACCEPT_DUPLICATE",
80
+ "API_GIT_VERSION",
51
81
  # Picture process
52
82
  "PICTURE_PROCESS_DERIVATES_STRATEGY",
53
83
  "PICTURE_PROCESS_THREADS_LIMIT",
84
+ "PICTURE_PROCESS_REFRESH_CRON",
54
85
  # OAUTH
55
86
  "OAUTH_PROVIDER",
56
87
  "OAUTH_OIDC_URL",
@@ -106,6 +137,8 @@ def read_config(app, test_config):
106
137
 
107
138
  app.config["DB_URL"] = f"postgres://{username}:{passw}@{host}:{port}/{dbname}"
108
139
 
140
+ app.config["DB_CHECK_SCHEMA"] = _read_bool(app.config, "DB_CHECK_SCHEMA")
141
+
109
142
  if app.config.get("API_BLUR_URL") is not None and len(app.config.get("API_BLUR_URL")) > 0:
110
143
  try:
111
144
  urlparse(app.config.get("API_BLUR_URL"))
@@ -119,6 +152,18 @@ def read_config(app, test_config):
119
152
  f"Unknown picture derivates strategy: '{app.config['PICTURE_PROCESS_DERIVATES_STRATEGY']}'. Please set to one of ON_DEMAND, PREPROCESS"
120
153
  )
121
154
 
155
+ # Parse API summary
156
+ if not isinstance(app.config.get("API_SUMMARY"), ApiSummary):
157
+ try:
158
+ if isinstance(app.config.get("API_SUMMARY"), str):
159
+ app.config["API_SUMMARY"] = ApiSummary(**json.loads(app.config["API_SUMMARY"]))
160
+ elif isinstance(app.config.get("API_SUMMARY"), dict):
161
+ app.config["API_SUMMARY"] = ApiSummary(**app.config["API_SUMMARY"])
162
+ elif app.config.get("API_SUMMARY") is not None:
163
+ raise Exception("Value is not a JSON")
164
+ except Exception as e:
165
+ raise Exception("Parameter API_SUMMARY is not recognized") from e
166
+
122
167
  # Checks on front-end related variables
123
168
  templateFolder = os.path.join(app.root_path, app.template_folder)
124
169
  for pageParam in ["API_MAIN_PAGE", "API_VIEWER_PAGE"]:
@@ -155,6 +200,14 @@ def read_config(app, test_config):
155
200
  if app.config.get("API_PICTURES_LICENSE_SPDX_ID") is None:
156
201
  app.config["API_PICTURES_LICENSE_SPDX_ID"] = "proprietary"
157
202
 
203
+ cron_val = app.config["PICTURE_PROCESS_REFRESH_CRON"]
204
+ if not croniter.croniter.is_valid(cron_val):
205
+ raise Exception(f"PICTURE_PROCESS_REFRESH_CRON should be a valid cron syntax, got '{cron_val}'")
206
+
207
+ app.config["API_ACCEPT_DUPLICATE"] = _read_bool(app.config, "API_ACCEPT_DUPLICATE")
208
+
209
+ app.config["DB_STATEMENT_TIMEOUT"] = int(app.config["DB_STATEMENT_TIMEOUT"])
210
+
158
211
  #
159
212
  # Add generated config vars
160
213
  #
@@ -164,6 +217,17 @@ def read_config(app, test_config):
164
217
  app.config["EXECUTOR_PROPAGATE_EXCEPTIONS"] = True # propagate the excecutor's exceptions, to be able to trace them
165
218
 
166
219
 
220
+ def _read_bool(config, value_name: str) -> Optional[bool]:
221
+ value = config.get(value_name)
222
+ if value is None:
223
+ return value
224
+ if type(value) == bool:
225
+ return value
226
+ if type(value) == str:
227
+ return value.lower() == "true"
228
+ raise Exception(f"Configuration {value_name} should either be a boolean or a string, got '{value}'")
229
+
230
+
167
231
  def _get_threads_limit(param: str) -> int:
168
232
  """Computes maximum thread limit depending on environment variables and available CPU.
169
233
 
geovisio/db_migrations.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import yoyo
2
2
  import psycopg
3
+ from psycopg.sql import SQL, Identifier
3
4
  import os
4
5
  import sys
5
6
 
@@ -8,6 +9,26 @@ def update_db_schema(dbUrl, force=False):
8
9
  # Check if DB has its structure initialized
9
10
  with psycopg.connect(dbUrl) as conn:
10
11
  with conn.cursor() as cursor:
12
+ # Alert if database is not UTC
13
+ dbTz = cursor.execute("SHOW timezone").fetchone()[0]
14
+ isUtcOffset = cursor.execute(
15
+ "SELECT EXISTS(SELECT * FROM pg_timezone_names WHERE name = %s AND utc_offset='00:00:00' AND NOT is_dst)", [dbTz]
16
+ ).fetchone()[0]
17
+ if not isUtcOffset:
18
+ dbName = Identifier(dbUrl.split("/")[-1])
19
+ if force:
20
+ cursor.execute(SQL("ALTER DATABASE {db} SET TIMEZONE TO 'UTC'").format(db=dbName))
21
+ else:
22
+ raise Exception(
23
+ f"""Database is not running at UTC timezone !
24
+
25
+ Your database actually uses timezone \"{dbTz}\".
26
+ Issues could happen if your database runs at a different timezone.
27
+ You can set the database timezone using the following command:
28
+
29
+ ALTER DATABASE {dbName.as_string()} SET TIMEZONE TO 'UTC';"""
30
+ )
31
+
11
32
  picturesTableExists = cursor.execute("SELECT EXISTS(SELECT relname FROM pg_class WHERE relname = 'pictures')").fetchone()[0]
12
33
  yoyoExists = cursor.execute("SELECT EXISTS(SELECT relname FROM pg_class WHERE relname like '_yoyo_%')").fetchone()[0]
13
34
  conn.close()
@@ -22,7 +43,7 @@ def update_db_schema(dbUrl, force=False):
22
43
  handledMigrations = [
23
44
  m for m in migrations if m.id in ["20221201_01_wpCGc-initial-schema", "20221201_02_ZG8AR-camera-information"]
24
45
  ]
25
- print(f"Database migrated to use Yoyo tools...")
46
+ print("Database migrated to use Yoyo tools...")
26
47
  backend.mark_migrations(handledMigrations)
27
48
 
28
49
  migrationToApply = backend.to_apply(migrations)
@@ -69,7 +90,7 @@ def rollback_db_schema(dbUrl, rollback_all):
69
90
  if not rollback_all:
70
91
  migrations_to_rollback = migrations_to_rollback[:1] # we only rollback the last one
71
92
  if len(migrations_to_rollback) > 0:
72
- print(f"Starting rollback for migrations:")
93
+ print("Starting rollback for migrations:")
73
94
  for m in migrations_to_rollback:
74
95
  print(f" - {m.id}")
75
96
  backend.rollback_migrations(migrations_to_rollback)
@@ -79,7 +100,7 @@ def rollback_db_schema(dbUrl, rollback_all):
79
100
 
80
101
 
81
102
  def get_yoyo_backend(dbUrl):
82
- dbUrl = dbUrl.replace("postgres://", "postgresql+psycopg://") # force spycopg3 usage on yolo
103
+ dbUrl = dbUrl.replace("postgres://", "postgresql+psycopg://") # force psycopg3 usage on yolo
83
104
  return yoyo.get_backend(dbUrl)
84
105
 
85
106
 
@@ -29,7 +29,7 @@
29
29
 
30
30
  <script>
31
31
  var baseUrl = window.location.href.replace(/\/$/, '');
32
- var viewerUrl = "https://cdn.jsdelivr.net/npm/geovisio@~{{API_VERSION_MAJOR_MINOR}}/build";
32
+ var viewerUrl = "https://cdn.jsdelivr.net/npm/geovisio@~3.0/build";
33
33
  function encodeHTML(html) {
34
34
  return html
35
35
  .replace(/</g, "&lt;")
@@ -42,22 +42,22 @@
42
42
  </head>
43
43
  <body>
44
44
  <h1><img src="/static/img/logo_full.svg" style="width: 350px; max-width: 90%" alt="GeoVisio" /></h1>
45
- <p>Simple 360° geolocated pictures hosting</p>
45
+ <p>{%trans%}Simple 360° geolocated pictures hosting{%endtrans%}</p>
46
46
  <iframe src="/viewer" style="border: none; width: 95%; height: 300px"></iframe>
47
- <p><small><a href="/viewer">Full page version</a></small></p>
47
+ <p><small><a href="/viewer">{%trans%}Full page version{%endtrans%}</a></small></p>
48
48
 
49
49
  <hr />
50
50
 
51
- <h2>Viewer</h2>
52
- <h3>Embed pre-configured viewer</h3>
53
- <p>Easiest way to have a working GeoVisio viewer on your website</p>
51
+ <h2>{%trans%}Viewer{%endtrans%}</h2>
52
+ <h3>{%trans%}Embed pre-configured viewer{%endtrans%}</h3>
53
+ <p>{%trans%}Easiest way to have a working GeoVisio viewer on your website{%endtrans%}</p>
54
54
  <pre><code id="code-preconf"></code></pre>
55
55
  <script>
56
56
  document.getElementById("code-preconf").innerHTML = encodeHTML('<iframe \n\tsrc="'+baseUrl+'/viewer"\n\tstyle="border: none; width: 500px; height: 300px">\n</iframe>');
57
57
  </script>
58
58
 
59
- <h3>Use JS library</h3>
60
- <p>A completely configurable viewer for your website</p>
59
+ <h3>{%trans%}Use JS library{%endtrans%}</h3>
60
+ <p>{%trans%}A completely configurable viewer for your website{%endtrans%}</p>
61
61
  <pre><code id="code-js-html">
62
62
  </code></pre>
63
63
  <script>
@@ -67,7 +67,7 @@
67
67
  +'<div id="viewer" style="width: 500px; height: 300px"></div>\n\n'
68
68
  +'&lt;script>\n'
69
69
  +'\t// All options available are listed here\n'
70
- +'\t// https://gitlab.com/geovisio/web-viewer/-/blob/develop/docs/02_Usage.md\n'
70
+ +'\t// https://gitlab.com/panoramax/clients/web-viewer/-/blob/develop/docs/02_Usage.md\n'
71
71
  +'\tvar instance = new GeoVisio.default(\n'
72
72
  +'\t\t"viewer",\n'
73
73
  +'\t\t"'+baseUrl+'/api",\n'
@@ -79,15 +79,15 @@
79
79
 
80
80
  <hr />
81
81
 
82
- <h2>Links</h2>
82
+ <h2>{%trans%}Links{%endtrans%}</h2>
83
83
  <p>
84
- <a href="/viewer">Pictures viewer</a>
84
+ <a href="/viewer">{%trans%}Pictures viewer{%endtrans%}</a>
85
85
  -
86
- <a href="/api/docs/swagger">API docs</a>
86
+ <a href="/api/docs/swagger">{%trans%}API docs{%endtrans%}</a>
87
87
  -
88
- <a href="https://gitlab.com/geovisio/web-viewer/-/tree/develop/docs">JS library docs</a>
88
+ <a href="https://gitlab.com/panoramax/clients/web-viewer/-/tree/develop/docs">{%trans%}JS library docs{%endtrans%}</a>
89
89
  -
90
- <a href="https://gitlab.com/geovisio">Repositories</a>
90
+ <a href="https://gitlab.com/panoramax">{%trans%}Repositories{%endtrans%}</a>
91
91
  </p>
92
92
  </body>
93
93
  </html>
@@ -6,7 +6,7 @@
6
6
  <link rel="shortcut icon" href="/favicon.ico" />
7
7
  <link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg" />
8
8
  <title>GeoVisio</title>
9
- <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/geovisio@~{{API_VERSION_MAJOR_MINOR}}/build/index.css" />
9
+ <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/geovisio@~3.0/build/index.css" />
10
10
  <style>
11
11
  #viewer {
12
12
  position: absolute;
@@ -19,10 +19,10 @@
19
19
  </head>
20
20
  <body>
21
21
  <div id="viewer">
22
- <noscript>You need to enable JavaScript to run this app.</noscript>
22
+ <noscript>{%trans%}You need to enable JavaScript to run this app.{%endtrans%}</noscript>
23
23
  </div>
24
24
 
25
- <script src="https://cdn.jsdelivr.net/npm/geovisio@~{{API_VERSION_MAJOR_MINOR}}/build/index.js"></script>
25
+ <script src="https://cdn.jsdelivr.net/npm/geovisio@~3.0/build/index.js"></script>
26
26
  <script>
27
27
  var instance = new GeoVisio.default(
28
28
  "viewer",