geovisio 2.6.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.
- geovisio/__init__.py +36 -7
- geovisio/admin_cli/db.py +1 -4
- geovisio/config_app.py +40 -1
- geovisio/db_migrations.py +24 -3
- geovisio/templates/main.html +13 -13
- geovisio/templates/viewer.html +3 -3
- geovisio/translations/de/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/de/LC_MESSAGES/messages.po +667 -0
- geovisio/translations/en/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/en/LC_MESSAGES/messages.po +730 -0
- geovisio/translations/es/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/es/LC_MESSAGES/messages.po +778 -0
- geovisio/translations/fi/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fi/LC_MESSAGES/messages.po +589 -0
- geovisio/translations/fr/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/fr/LC_MESSAGES/messages.po +814 -0
- geovisio/translations/ko/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/ko/LC_MESSAGES/messages.po +685 -0
- geovisio/translations/messages.pot +686 -0
- geovisio/translations/nl/LC_MESSAGES/messages.mo +0 -0
- geovisio/translations/nl/LC_MESSAGES/messages.po +594 -0
- geovisio/utils/__init__.py +1 -1
- geovisio/utils/auth.py +50 -11
- geovisio/utils/db.py +65 -0
- geovisio/utils/excluded_areas.py +83 -0
- geovisio/utils/extent.py +30 -0
- geovisio/utils/fields.py +1 -1
- geovisio/utils/filesystems.py +0 -1
- geovisio/utils/link.py +14 -0
- geovisio/utils/params.py +20 -0
- geovisio/utils/pictures.py +92 -68
- geovisio/utils/reports.py +171 -0
- geovisio/utils/sequences.py +264 -126
- geovisio/utils/tokens.py +37 -42
- geovisio/utils/upload_set.py +654 -0
- geovisio/web/auth.py +37 -37
- geovisio/web/collections.py +286 -302
- geovisio/web/configuration.py +14 -0
- geovisio/web/docs.py +241 -14
- geovisio/web/excluded_areas.py +377 -0
- geovisio/web/items.py +156 -108
- geovisio/web/map.py +20 -20
- geovisio/web/params.py +69 -26
- geovisio/web/pictures.py +14 -31
- geovisio/web/reports.py +399 -0
- geovisio/web/rss.py +13 -7
- geovisio/web/stac.py +129 -134
- geovisio/web/tokens.py +98 -109
- geovisio/web/upload_set.py +768 -0
- geovisio/web/users.py +100 -73
- geovisio/web/utils.py +28 -9
- geovisio/workers/runner_pictures.py +252 -204
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/METADATA +16 -13
- geovisio-2.7.0.dist-info/RECORD +66 -0
- geovisio-2.6.0.dist-info/RECORD +0 -41
- {geovisio-2.6.0.dist-info → geovisio-2.7.0.dist-info}/LICENSE +0 -0
- {geovisio-2.6.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.
|
|
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
|
|
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,6 +77,8 @@ 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)
|
|
@@ -78,7 +103,8 @@ def create_app(test_config=None, app=None):
|
|
|
78
103
|
|
|
79
104
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=nb_proxies, x_proto=nb_proxies, x_host=nb_proxies, x_prefix=nb_proxies)
|
|
80
105
|
|
|
81
|
-
|
|
106
|
+
# store the background processor in the app context
|
|
107
|
+
app.background_processor = runner_pictures.PictureBackgroundProcessor(app)
|
|
82
108
|
|
|
83
109
|
#
|
|
84
110
|
# List available routes/blueprints
|
|
@@ -92,6 +118,9 @@ def create_app(test_config=None, app=None):
|
|
|
92
118
|
app.register_blueprint(users.bp)
|
|
93
119
|
app.register_blueprint(configuration.bp)
|
|
94
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)
|
|
95
124
|
|
|
96
125
|
# Register CLI comands
|
|
97
126
|
app.register_blueprint(admin_cli.bp, cli_group=None)
|
|
@@ -101,8 +130,8 @@ def create_app(test_config=None, app=None):
|
|
|
101
130
|
def run_picture_worker():
|
|
102
131
|
"""Run a worker to process pictures after upload. Each worker use one thread, and several workers can be run in parallel"""
|
|
103
132
|
logging.info("Running picture worker")
|
|
104
|
-
worker = runner_pictures.PictureProcessor(
|
|
105
|
-
worker.
|
|
133
|
+
worker = runner_pictures.PictureProcessor(app=app, stop=False)
|
|
134
|
+
worker.process_jobs()
|
|
106
135
|
|
|
107
136
|
#
|
|
108
137
|
# API documentation
|
geovisio/admin_cli/db.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from flask import Blueprint, current_app
|
|
2
2
|
from flask.cli import with_appcontext
|
|
3
3
|
import click
|
|
4
|
-
import psycopg
|
|
5
4
|
from geovisio import db_migrations
|
|
6
5
|
from geovisio.utils import sequences
|
|
7
6
|
|
|
@@ -33,6 +32,4 @@ def rollback(all):
|
|
|
33
32
|
@with_appcontext
|
|
34
33
|
def refresh():
|
|
35
34
|
"""Refresh cached data (pictures_grid)"""
|
|
36
|
-
|
|
37
|
-
sequences.update_pictures_grid(db)
|
|
38
|
-
db.commit()
|
|
35
|
+
sequences.update_pictures_grid()
|
geovisio/config_app.py
CHANGED
|
@@ -3,11 +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
|
|
6
|
+
from typing import Optional, Dict
|
|
7
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"
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
class DefaultConfig:
|
|
22
|
+
API_SUMMARY = ApiSummary()
|
|
11
23
|
API_VIEWER_PAGE = "viewer.html"
|
|
12
24
|
API_MAIN_PAGE = "main.html"
|
|
13
25
|
# we default we keep the session cookie 7 days, users would have to renew their loggin after this
|
|
@@ -24,6 +36,10 @@ class DefaultConfig:
|
|
|
24
36
|
PICTURE_PROCESS_REFRESH_CRON = (
|
|
25
37
|
"0 2 * * *" # Background worker will refresh by default some stats at 2 o'clock in the night (local time of the server)
|
|
26
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
|
|
27
43
|
|
|
28
44
|
|
|
29
45
|
def read_config(app, test_config):
|
|
@@ -46,7 +62,11 @@ def read_config(app, test_config):
|
|
|
46
62
|
"DB_PASSWORD",
|
|
47
63
|
"DB_NAME",
|
|
48
64
|
"DB_CHECK_SCHEMA",
|
|
65
|
+
"DB_MIN_CNX",
|
|
66
|
+
"DB_MAX_CNX",
|
|
67
|
+
"DB_STATEMENT_TIMEOUT",
|
|
49
68
|
# API
|
|
69
|
+
"API_SUMMARY",
|
|
50
70
|
"API_BLUR_URL",
|
|
51
71
|
"API_VIEWER_PAGE",
|
|
52
72
|
"API_MAIN_PAGE",
|
|
@@ -56,6 +76,8 @@ def read_config(app, test_config):
|
|
|
56
76
|
"API_DERIVATES_PICTURES_PUBLIC_URL",
|
|
57
77
|
"API_PICTURES_LICENSE_SPDX_ID",
|
|
58
78
|
"API_PICTURES_LICENSE_URL",
|
|
79
|
+
"API_ACCEPT_DUPLICATE",
|
|
80
|
+
"API_GIT_VERSION",
|
|
59
81
|
# Picture process
|
|
60
82
|
"PICTURE_PROCESS_DERIVATES_STRATEGY",
|
|
61
83
|
"PICTURE_PROCESS_THREADS_LIMIT",
|
|
@@ -130,6 +152,18 @@ def read_config(app, test_config):
|
|
|
130
152
|
f"Unknown picture derivates strategy: '{app.config['PICTURE_PROCESS_DERIVATES_STRATEGY']}'. Please set to one of ON_DEMAND, PREPROCESS"
|
|
131
153
|
)
|
|
132
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
|
+
|
|
133
167
|
# Checks on front-end related variables
|
|
134
168
|
templateFolder = os.path.join(app.root_path, app.template_folder)
|
|
135
169
|
for pageParam in ["API_MAIN_PAGE", "API_VIEWER_PAGE"]:
|
|
@@ -169,6 +203,11 @@ def read_config(app, test_config):
|
|
|
169
203
|
cron_val = app.config["PICTURE_PROCESS_REFRESH_CRON"]
|
|
170
204
|
if not croniter.croniter.is_valid(cron_val):
|
|
171
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
|
+
|
|
172
211
|
#
|
|
173
212
|
# Add generated config vars
|
|
174
213
|
#
|
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(
|
|
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(
|
|
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
|
|
103
|
+
dbUrl = dbUrl.replace("postgres://", "postgresql+psycopg://") # force psycopg3 usage on yolo
|
|
83
104
|
return yoyo.get_backend(dbUrl)
|
|
84
105
|
|
|
85
106
|
|
geovisio/templates/main.html
CHANGED
|
@@ -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@~
|
|
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, "<")
|
|
@@ -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>
|
|
@@ -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/panoramax/clients/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/
|
|
90
|
+
<a href="https://gitlab.com/panoramax">{%trans%}Repositories{%endtrans%}</a>
|
|
91
91
|
</p>
|
|
92
92
|
</body>
|
|
93
93
|
</html>
|
geovisio/templates/viewer.html
CHANGED
|
@@ -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@~
|
|
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
|
|
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@~
|
|
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",
|
|
Binary file
|