datacube-ows 1.9.1__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.
- datacube_ows/__init__.py +12 -0
- datacube_ows/_version.py +21 -0
- datacube_ows/band_utils.py +241 -0
- datacube_ows/cfg_parser_impl.py +388 -0
- datacube_ows/config_toolkit.py +52 -0
- datacube_ows/config_utils.py +1056 -0
- datacube_ows/data.py +281 -0
- datacube_ows/feature_info.py +325 -0
- datacube_ows/gunicorn_config.py +17 -0
- datacube_ows/http_utils.py +136 -0
- datacube_ows/index/__init__.py +10 -0
- datacube_ows/index/api.py +214 -0
- datacube_ows/index/driver.py +55 -0
- datacube_ows/index/postgis/__init__.py +5 -0
- datacube_ows/index/postgis/api.py +198 -0
- datacube_ows/index/postgis/product_ranges.py +281 -0
- datacube_ows/index/postgres/__init__.py +5 -0
- datacube_ows/index/postgres/api.py +154 -0
- datacube_ows/index/postgres/mv_index.py +181 -0
- datacube_ows/index/postgres/product_ranges.py +244 -0
- datacube_ows/index/sql.py +109 -0
- datacube_ows/legend_generator.py +60 -0
- datacube_ows/legend_utils.py +33 -0
- datacube_ows/loading.py +434 -0
- datacube_ows/ogc.py +262 -0
- datacube_ows/ogc_exceptions.py +123 -0
- datacube_ows/ogc_utils.py +258 -0
- datacube_ows/ows_cfg_example.py +2104 -0
- datacube_ows/ows_configuration.py +1616 -0
- datacube_ows/protocol_versions.py +101 -0
- datacube_ows/query_profiler.py +44 -0
- datacube_ows/resource_limits.py +292 -0
- datacube_ows/sql/postgis/ows_schema/create/001_create_schema.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/create/002_create_product_rng.sql +17 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_only/001_grant_usage_requires_role.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_only/003_grant_odc_user_requires_role.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_write/001_grant_usage_requires_role.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql +3 -0
- datacube_ows/sql/postgis/ows_schema/grants/read_write/003_grant_odc_user_requires_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/001_postgis_extension.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/002_timezone.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/003_create_view_owner_role_ignore_duplicates.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/004_grant_agdc_user_to_view_owner_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/005_ows_read_to_view_owner_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/006_odc_read_to_view_owner_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/010_create_new_time_view.sql +46 -0
- datacube_ows/sql/postgres/extent_views/create/011_create_new_space_view.sql +102 -0
- datacube_ows/sql/postgres/extent_views/create/012_create_new_spacetime_view.sql +5 -0
- datacube_ows/sql/postgres/extent_views/create/020_create_index_1.sql +5 -0
- datacube_ows/sql/postgres/extent_views/create/021_create_index_2.sql +5 -0
- datacube_ows/sql/postgres/extent_views/create/022_create_index_3.sql +5 -0
- datacube_ows/sql/postgres/extent_views/create/023_create_index_4.sql +5 -0
- datacube_ows/sql/postgres/extent_views/create/030_rename_old_space_time_view.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/031_rename_new_space_time_view.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/032_drop_old_space_time_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/040_drop_old_space_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/041_drop_old_time_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/create/042_rename_new_space_view.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/043_rename_new_time_view.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/050_rename_index_1.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/051_rename_index_2.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/052_rename_index_3.sql +4 -0
- datacube_ows/sql/postgres/extent_views/create/053_rename_index_4.sql +4 -0
- datacube_ows/sql/postgres/extent_views/grants/read_only/001_grant_read_requires_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/grants/refresh_owner/001_grant_space_view_perms_to_owner_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/grants/refresh_owner/010_set_owner_time_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/grants/refresh_owner/011_set_owner_space_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/grants/refresh_owner/012_set_owner_spacetime_view.sql +3 -0
- datacube_ows/sql/postgres/extent_views/grants/write_refresh/001_grant_refresh_requires_role.sql +3 -0
- datacube_ows/sql/postgres/extent_views/refresh/001_timezone.sql +3 -0
- datacube_ows/sql/postgres/extent_views/refresh/002_refresh_time.sql +3 -0
- datacube_ows/sql/postgres/extent_views/refresh/003_refresh_space.sql +3 -0
- datacube_ows/sql/postgres/extent_views/refresh/004_refresh_spacetime.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/001_drop_space_time_view.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/002_drop_time_view.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/003_drop_space_view.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/010_drop_subproduct_range.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/011_drop_multiproduct_range.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/cleanup/012_drop_product_range.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/create/001_create_schema.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/create/002_create_product_rng.sql +17 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_only/001_grant_usage_requires_role.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_only/002_grant_range_read_requires_role.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_only/003_grant_agdc_user_requires_role.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_write/001_grant_usage_requires_role.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_write/002_grant_writetables_requires_role.sql +3 -0
- datacube_ows/sql/postgres/ows_schema/grants/read_write/003_grant_agdc_user_requires_role.sql +3 -0
- datacube_ows/startup_utils.py +244 -0
- datacube_ows/styles/__init__.py +13 -0
- datacube_ows/styles/api/__init__.py +14 -0
- datacube_ows/styles/api/base.py +200 -0
- datacube_ows/styles/base.py +667 -0
- datacube_ows/styles/colormap.py +446 -0
- datacube_ows/styles/component.py +182 -0
- datacube_ows/styles/expression.py +141 -0
- datacube_ows/styles/hybrid.py +88 -0
- datacube_ows/styles/ramp.py +587 -0
- datacube_ows/templates/html_feature_info.html +63 -0
- datacube_ows/templates/index.html +118 -0
- datacube_ows/templates/ogc_error.xml +26 -0
- datacube_ows/templates/ping.html +19 -0
- datacube_ows/templates/test_client.html +88 -0
- datacube_ows/templates/wcs_capabilities.xml +133 -0
- datacube_ows/templates/wcs_desc_coverage.xml +131 -0
- datacube_ows/templates/wms_capabilities.xml +298 -0
- datacube_ows/templates/wmts_capabilities.xml +234 -0
- datacube_ows/tile_matrix_sets.py +149 -0
- datacube_ows/time_utils.py +196 -0
- datacube_ows/update_ranges_impl.py +201 -0
- datacube_ows/utils.py +173 -0
- datacube_ows/wcs1.py +130 -0
- datacube_ows/wcs1_utils.py +472 -0
- datacube_ows/wcs2.py +311 -0
- datacube_ows/wcs2_utils.py +407 -0
- datacube_ows/wcs_scaler.py +254 -0
- datacube_ows/wcs_utils.py +23 -0
- datacube_ows/wms.py +56 -0
- datacube_ows/wms_utils.py +523 -0
- datacube_ows/wmts.py +205 -0
- datacube_ows/wsgi.py +36 -0
- datacube_ows-1.9.1.dist-info/METADATA +150 -0
- datacube_ows-1.9.1.dist-info/RECORD +166 -0
- datacube_ows-1.9.1.dist-info/WHEEL +5 -0
- datacube_ows-1.9.1.dist-info/entry_points.txt +8 -0
- datacube_ows-1.9.1.dist-info/licenses/LICENSE +14 -0
- datacube_ows-1.9.1.dist-info/top_level.txt +3 -0
- hack_cfg/__init__.py +0 -0
- hack_cfg/ows_cfg_local.py +6824 -0
- local_cfg/__init__.py +7 -0
- local_cfg/africa_ls_sr_local_cfg/__init__.py +7 -0
- local_cfg/africa_ls_sr_local_cfg/bands.py +90 -0
- local_cfg/africa_ls_sr_local_cfg/layers.py +130 -0
- local_cfg/africa_ls_sr_local_cfg/styles.py +1152 -0
- local_cfg/africa_s2_local_cfg/__init__.py +7 -0
- local_cfg/africa_s2_local_cfg/bands.py +95 -0
- local_cfg/africa_s2_local_cfg/lambdas.py +20 -0
- local_cfg/africa_s2_local_cfg/layers.py +41 -0
- local_cfg/africa_s2_local_cfg/styles.py +1130 -0
- local_cfg/c3_local_cfg/__init__.py +23 -0
- local_cfg/c3_local_cfg/bands.py +53 -0
- local_cfg/c3_local_cfg/layers.py +229 -0
- local_cfg/c3_local_cfg/legends.py +119 -0
- local_cfg/c3_local_cfg/resource_limits.py +33 -0
- local_cfg/c3_local_cfg/styles.py +735 -0
- local_cfg/c3_mosaic_cfg/__init__.py +7 -0
- local_cfg/c3_mosaic_cfg/utils.py +16 -0
- local_cfg/dep_utils.py +9 -0
- local_cfg/lccs_local_cfg/__init__.py +7 -0
- local_cfg/lccs_local_cfg/lc_class_desc.py +155 -0
- local_cfg/lccs_local_cfg/lccs_cfg.py +681 -0
- local_cfg/lccs_local_cfg/lccs_delta.py +19 -0
- local_cfg/ows_cfg_africa.py +1458 -0
- local_cfg/ows_cfg_local.py +7981 -0
- local_cfg/ows_cfg_local_backup_20200821.py +7267 -0
- local_cfg/ows_cfg_local_geomedian.py +1341 -0
- local_cfg/ows_cfg_local_inherit.py +7240 -0
- local_cfg/ows_cfg_stripped.py +369 -0
- local_cfg/ows_pacific_cfg.py +1118 -0
- local_cfg/s2_c3_local_cfg/__init__.py +7 -0
- local_cfg/s2_c3_local_cfg/bands.py +29 -0
- local_cfg/s2_c3_local_cfg/layers.py +131 -0
- local_cfg/s2_c3_local_cfg/pure_styles.py +130 -0
- local_cfg/s2_c3_local_cfg/resource_limits.py +19 -0
- local_cfg/s2_c3_local_cfg/styles.py +688 -0
- local_cfg/s2_c3_local_cfg/utils.py +19 -0
datacube_ows/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# This file is part of datacube-ows, part of the Open Data Cube project.
|
|
2
|
+
# See https://opendatacube.org for more information.
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2017-2024 OWS Contributors
|
|
5
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from ._version import version as __version__
|
|
9
|
+
except ImportError:
|
|
10
|
+
# Default version number.
|
|
11
|
+
# Will only be used when running datacube-ows direct from source code (not properly installed)
|
|
12
|
+
__version__ = "1.9.1?"
|
datacube_ows/_version.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '1.9.1'
|
|
21
|
+
__version_tuple__ = version_tuple = (1, 9, 1)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# This file is part of datacube-ows, part of the Open Data Cube project.
|
|
2
|
+
# See https://opendatacube.org for more information.
|
|
3
|
+
#
|
|
4
|
+
# Copyright (c) 2017-2024 OWS Contributors
|
|
5
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
6
|
+
|
|
7
|
+
import numpy
|
|
8
|
+
|
|
9
|
+
# Style index functions
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def scale_data(imgband_data, scale_from, scale_to):
|
|
13
|
+
sc_min, sc_max = scale_from
|
|
14
|
+
tc_min, tc_max = scale_to
|
|
15
|
+
clipped = imgband_data.clip(sc_min, sc_max)
|
|
16
|
+
normalised = (clipped - sc_min) / (sc_max - sc_min)
|
|
17
|
+
scaled = normalised * (tc_max - tc_min)
|
|
18
|
+
return scaled + tc_min
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def scalable(undecorated):
|
|
22
|
+
def decorated(*args, **kwargs):
|
|
23
|
+
scale_from = kwargs.pop("scale_from", None)
|
|
24
|
+
scale_to = kwargs.pop("scale_to", None)
|
|
25
|
+
if scale_from is not None and scale_to is None:
|
|
26
|
+
scale_to = [0, 255]
|
|
27
|
+
unscaled = undecorated(*args, **kwargs)
|
|
28
|
+
if scale_from:
|
|
29
|
+
return scale_data(unscaled, scale_from, scale_to)
|
|
30
|
+
return unscaled
|
|
31
|
+
|
|
32
|
+
return decorated
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def band_modulator(undecorated):
|
|
36
|
+
def decorated(data, *args, **kwargs):
|
|
37
|
+
band_mapper = kwargs.get("band_mapper", None)
|
|
38
|
+
mult_band = kwargs.pop("mult_band", None)
|
|
39
|
+
raw_data = undecorated(data, *args, **kwargs)
|
|
40
|
+
if mult_band and band_mapper:
|
|
41
|
+
mult_band = band_mapper(mult_band)
|
|
42
|
+
if mult_band:
|
|
43
|
+
return data[mult_band] * raw_data
|
|
44
|
+
return raw_data
|
|
45
|
+
return decorated
|
|
46
|
+
|
|
47
|
+
def pre_scaled_band(data, band, scale, offset):
|
|
48
|
+
# Pre-scale a band as `data[band] * scale + offset`
|
|
49
|
+
return data[band] * scale + offset
|
|
50
|
+
|
|
51
|
+
def sum_bands(data, band1, band2, band_mapper=None):
|
|
52
|
+
if band_mapper:
|
|
53
|
+
band1 = band_mapper(band1)
|
|
54
|
+
band2 = band_mapper(band2)
|
|
55
|
+
return data[band1] + data[band2]
|
|
56
|
+
|
|
57
|
+
def pre_scaled_sum_bands(
|
|
58
|
+
data, band1, band2,
|
|
59
|
+
scale1=1.0, offset1=0.0,
|
|
60
|
+
scale2=1.0, offset2=0.0,
|
|
61
|
+
band_mapper=None):
|
|
62
|
+
# Calculate the sum of two bands, after pre-scaling them with a scale and offset
|
|
63
|
+
if band_mapper:
|
|
64
|
+
band1 = band_mapper(band1)
|
|
65
|
+
band2 = band_mapper(band2)
|
|
66
|
+
# Pre-scaled bands data
|
|
67
|
+
data1 = pre_scaled_band(data, band1, scale1, offset1)
|
|
68
|
+
data2 = pre_scaled_band(data, band2, scale2, offset2)
|
|
69
|
+
return data1 + data2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def delta_bands(data, band1, band2, band_mapper=None):
|
|
73
|
+
if band_mapper:
|
|
74
|
+
band1 = band_mapper(band1)
|
|
75
|
+
band2 = band_mapper(band2)
|
|
76
|
+
typ1 = data[band1].dtype
|
|
77
|
+
typ2 = data[band2].dtype
|
|
78
|
+
if typ1.name.startswith('uint'):
|
|
79
|
+
nodata = data[band1].nodata
|
|
80
|
+
data[band1] = data[band1].astype('int32')
|
|
81
|
+
data[band1].attrs["nodata"] = nodata
|
|
82
|
+
if typ2.name.startswith('uint'):
|
|
83
|
+
nodata = data[band2].nodata
|
|
84
|
+
data[band2] = data[band2].astype('int32')
|
|
85
|
+
data[band2].attrs["nodata"] = nodata
|
|
86
|
+
# if typ1.name.startswith('uint') or typ2.name.startswith('uint'):
|
|
87
|
+
# data = data.astype('int32', copy=False)
|
|
88
|
+
return data[band1] - data[band2]
|
|
89
|
+
|
|
90
|
+
def pre_scaled_delta_bands(
|
|
91
|
+
data, band1, band2,
|
|
92
|
+
scale1=1.0, offset1=0.0,
|
|
93
|
+
scale2=1.0, offset2=0.0,
|
|
94
|
+
band_mapper=None):
|
|
95
|
+
# Calculate the difference between two bands, after pre-scaling them with a scale
|
|
96
|
+
# and offset
|
|
97
|
+
if band_mapper:
|
|
98
|
+
band1 = band_mapper(band1)
|
|
99
|
+
band2 = band_mapper(band2)
|
|
100
|
+
typ1 = data[band1].dtype
|
|
101
|
+
typ2 = data[band2].dtype
|
|
102
|
+
if typ1.name.startswith('uint'):
|
|
103
|
+
nodata = data[band1].nodata
|
|
104
|
+
data[band1] = data[band1].astype('int32')
|
|
105
|
+
data[band1].attrs["nodata"] = nodata
|
|
106
|
+
if typ2.name.startswith('uint'):
|
|
107
|
+
nodata = data[band2].nodata
|
|
108
|
+
data[band2] = data[band2].astype('int32')
|
|
109
|
+
data[band2].attrs["nodata"] = nodata
|
|
110
|
+
# if typ1.name.startswith('uint') or typ2.name.startswith('uint'):
|
|
111
|
+
# data = data.astype('int32', copy=False)
|
|
112
|
+
# Pre-scaled bands data
|
|
113
|
+
data1 = pre_scaled_band(data, band1, scale1, offset1)
|
|
114
|
+
data2 = pre_scaled_band(data, band2, scale2, offset2)
|
|
115
|
+
return data1 - data2
|
|
116
|
+
|
|
117
|
+
# N.B. Modifying scale_to would be dangerous - don't do it.
|
|
118
|
+
# pylint: disable=dangerous-default-value
|
|
119
|
+
@scalable
|
|
120
|
+
def norm_diff(data, band1, band2, band_mapper=None):
|
|
121
|
+
# Calculate a normalised difference index.
|
|
122
|
+
return delta_bands(data, band1, band2, band_mapper) / sum_bands(data, band1, band2, band_mapper)
|
|
123
|
+
|
|
124
|
+
@scalable
|
|
125
|
+
def pre_scaled_norm_diff(data, band1, band2,
|
|
126
|
+
scale1=1.0, offset1=0.0,
|
|
127
|
+
scale2=1.0, offset2=0.0,
|
|
128
|
+
band_mapper=None,):
|
|
129
|
+
# Calculate a normalised difference index, after scaling the input bands with a
|
|
130
|
+
# scale and offset
|
|
131
|
+
return (pre_scaled_delta_bands(
|
|
132
|
+
data, band1, band2, scale1, offset1, scale2, offset2, band_mapper) /
|
|
133
|
+
pre_scaled_sum_bands(
|
|
134
|
+
data, band1, band2, scale1, offset1, scale2, offset2, band_mapper)
|
|
135
|
+
)
|
|
136
|
+
@scalable
|
|
137
|
+
def constant(data, band, const, band_mapper=None):
|
|
138
|
+
# Covert an xarray for a flat constant.
|
|
139
|
+
# Useful for displaying mask extents as a flat colour and debugging.
|
|
140
|
+
# params is assumed to be a tuple containing a constant value and a band name/alias.
|
|
141
|
+
|
|
142
|
+
if band_mapper:
|
|
143
|
+
band = band_mapper(band)
|
|
144
|
+
return data[band] * 0.0 + const
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@scalable
|
|
148
|
+
def single_band(data, band, band_mapper=None):
|
|
149
|
+
# Use the raw value of a band directly as the index function.
|
|
150
|
+
|
|
151
|
+
if band_mapper:
|
|
152
|
+
band = band_mapper(band)
|
|
153
|
+
return data[band]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@scalable
|
|
157
|
+
def band_quotient(data, band1, band2, band_mapper=None):
|
|
158
|
+
if band_mapper:
|
|
159
|
+
band1 = band_mapper(band1)
|
|
160
|
+
band2 = band_mapper(band2)
|
|
161
|
+
return data[band1] / data[band2]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@scalable
|
|
165
|
+
def band_quotient_sum(data, band1a, band1b, band2a, band2b, band_mapper=None):
|
|
166
|
+
return band_quotient(data, band1a, band1b, band_mapper) + band_quotient(data, band2a, band2b, band_mapper)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@scalable
|
|
170
|
+
def sentinel2_ndci(data, b_red_edge, b_red, b_green, b_swir, band_mapper=None):
|
|
171
|
+
red_delta = delta_bands(data, b_red_edge, b_red, band_mapper)
|
|
172
|
+
red_sum = sum_bands(data, b_red_edge, b_red, band_mapper)
|
|
173
|
+
mndwi = norm_diff(data, b_green, b_swir, band_mapper)
|
|
174
|
+
|
|
175
|
+
return red_delta / red_sum.where(mndwi > 0.1)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def multi_date_delta(data, time_direction=-1):
|
|
179
|
+
data1, data2 = (data.sel(time=dt) for dt in data.coords["time"].values)
|
|
180
|
+
|
|
181
|
+
# data1, data2 = data.values.item(0), data.values.item(1)
|
|
182
|
+
if time_direction >= 0:
|
|
183
|
+
return data1 - data2
|
|
184
|
+
else:
|
|
185
|
+
return data2 - data1
|
|
186
|
+
|
|
187
|
+
def multi_date_pass(data):
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
def multi_date_raw_example(data, band1, band2, band_mapper=None):
|
|
191
|
+
if band_mapper:
|
|
192
|
+
band1 = band_mapper(band1)
|
|
193
|
+
band2 = band_mapper(band2)
|
|
194
|
+
data1, data2 = (data.sel(time=dt) for dt in data.coords["time"].values)
|
|
195
|
+
return (
|
|
196
|
+
(data2[band1] - data1[band1]) - (data2[band2] - data1[band2])
|
|
197
|
+
) / (
|
|
198
|
+
data1[band1] + data1[band2] + data2[band1] + data2[band2]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@band_modulator
|
|
203
|
+
@scalable
|
|
204
|
+
def single_band_log(data, band, scale_factor, exponent, band_mapper=None):
|
|
205
|
+
if band_mapper:
|
|
206
|
+
band = band_mapper(band)
|
|
207
|
+
d = data[band]
|
|
208
|
+
return scale_factor * ((d ** exponent) - 1.0)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@band_modulator
|
|
212
|
+
@scalable
|
|
213
|
+
def single_band_arcsec(data, band, band_mapper=None):
|
|
214
|
+
if band_mapper:
|
|
215
|
+
band = band_mapper(band)
|
|
216
|
+
d = data[band]
|
|
217
|
+
return numpy.arccos(1 / (d + 1))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@band_modulator
|
|
221
|
+
@scalable
|
|
222
|
+
def single_band_offset_log(data, band, scale=1.0, offset=None, band_mapper=None):
|
|
223
|
+
if band_mapper:
|
|
224
|
+
band = band_mapper(band)
|
|
225
|
+
d = data[band]
|
|
226
|
+
if offset is not None:
|
|
227
|
+
d = data[band] * scale + offset
|
|
228
|
+
unscaled = numpy.log(d)
|
|
229
|
+
else:
|
|
230
|
+
unscaled = numpy.log1p(d * scale)
|
|
231
|
+
return unscaled
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@scalable
|
|
235
|
+
def radar_vegetation_index(data, band_hv, band_hh, band_mapper=None):
|
|
236
|
+
if band_mapper:
|
|
237
|
+
band_hv = band_mapper(band_hv)
|
|
238
|
+
band_hh = band_mapper(band_hh)
|
|
239
|
+
hv_sq = data[band_hv] * data[band_hv]
|
|
240
|
+
hh_sq = data[band_hh] * data[band_hh]
|
|
241
|
+
return (hv_sq * 4.0) / (hh_sq + hv_sq)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# This file is part of datacube-ows, part of the Open Data Cube project.
|
|
3
|
+
# See https://opendatacube.org for more information.
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2017-2024 OWS Contributors
|
|
6
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from babel.messages.pofile import write_po
|
|
16
|
+
from datacube import Datacube
|
|
17
|
+
from deepdiff import DeepDiff
|
|
18
|
+
|
|
19
|
+
from datacube_ows import __version__
|
|
20
|
+
from datacube_ows.config_utils import ConfigException
|
|
21
|
+
from datacube_ows.ows_configuration import (OWSConfig, OWSFolder, OWSLayer,
|
|
22
|
+
OWSNamedLayer, read_config)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group(invoke_without_command=True)
|
|
26
|
+
@click.option(
|
|
27
|
+
"--version", is_flag=True, default=False, help="Show OWS version number and exit"
|
|
28
|
+
)
|
|
29
|
+
def main(version: bool) -> int:
|
|
30
|
+
# --version
|
|
31
|
+
if version:
|
|
32
|
+
click.echo(f"Open Data Cube Open Web Services (datacube-ows) version {__version__}")
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@main.command()
|
|
37
|
+
@click.argument("paths", nargs=-1)
|
|
38
|
+
@click.option(
|
|
39
|
+
"-p",
|
|
40
|
+
"--parse-only",
|
|
41
|
+
is_flag=True,
|
|
42
|
+
default=False,
|
|
43
|
+
help="Only parse the syntax of the config file - do not validate against database",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"-f",
|
|
47
|
+
"--folders",
|
|
48
|
+
is_flag=True,
|
|
49
|
+
default=False,
|
|
50
|
+
help="Print the folder/layer heirarchy(ies) to stdout.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"-s",
|
|
54
|
+
"--styles",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
default=False,
|
|
57
|
+
help="Print the styles for each layer to stdout (format depends on --folders flag).",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"-i",
|
|
61
|
+
"--input-file",
|
|
62
|
+
help="Provide a file path for the input inventory json file to be compared with config file",
|
|
63
|
+
)
|
|
64
|
+
@click.option(
|
|
65
|
+
"-o",
|
|
66
|
+
"--output-file",
|
|
67
|
+
help="Provide an output inventory file name with extension .json",
|
|
68
|
+
)
|
|
69
|
+
def check(parse_only: bool, folders: bool, styles: bool, input_file: str, output_file: str, paths: list[str]) -> int:
|
|
70
|
+
"""Check configuration files
|
|
71
|
+
|
|
72
|
+
Takes a list of configuration specifications which are each loaded and validated in turn,
|
|
73
|
+
with each specification being interpreted as per the $DATACUBE_OWS_CFG environment variable.
|
|
74
|
+
|
|
75
|
+
If no specification is provided, the $DATACUBE_OWS_CFG environment variable is used.
|
|
76
|
+
"""
|
|
77
|
+
if parse_only and (folders or styles):
|
|
78
|
+
click.echo(
|
|
79
|
+
"The --folders (-f) and --styles (-s) flags cannot be used in conjunction with the --parser-only (-p) flag."
|
|
80
|
+
)
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
all_ok = True
|
|
83
|
+
if not paths:
|
|
84
|
+
if parse_path(None, parse_only, folders, styles, input_file, output_file):
|
|
85
|
+
return 0
|
|
86
|
+
else:
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
for path in paths:
|
|
89
|
+
if not parse_path(path, parse_only, folders, styles, input_file, output_file):
|
|
90
|
+
all_ok = False
|
|
91
|
+
|
|
92
|
+
if not all_ok:
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_path(path: str | None, parse_only: bool, folders: bool, styles: bool, input_file: str, output_file: str) -> bool:
|
|
98
|
+
try:
|
|
99
|
+
raw_cfg = read_config(path)
|
|
100
|
+
cfg = OWSConfig(refresh=True, cfg=raw_cfg)
|
|
101
|
+
if not parse_only:
|
|
102
|
+
with Datacube() as dc:
|
|
103
|
+
cfg.make_ready(dc)
|
|
104
|
+
except ConfigException as e:
|
|
105
|
+
click.echo(f"Config exception for path {str(e)}")
|
|
106
|
+
return False
|
|
107
|
+
click.echo("Configuration parsed OK")
|
|
108
|
+
click.echo(f"Configured message file location: {cfg.msg_file_name}")
|
|
109
|
+
click.echo(f"Configured translations directory location: {cfg.translations_dir}")
|
|
110
|
+
if folders:
|
|
111
|
+
click.echo()
|
|
112
|
+
click.echo("Folder/Layer Hierarchy")
|
|
113
|
+
click.echo("======================")
|
|
114
|
+
print_layers(cfg.layers, styles, depth=0)
|
|
115
|
+
click.echo()
|
|
116
|
+
elif styles:
|
|
117
|
+
click.echo()
|
|
118
|
+
click.echo("Layers and Styles")
|
|
119
|
+
click.echo("=================")
|
|
120
|
+
for lyr in cfg.layer_index.values():
|
|
121
|
+
click.echo(f"{lyr.name} [{','.join(lyr.product_names)}]")
|
|
122
|
+
print_styles(lyr)
|
|
123
|
+
click.echo()
|
|
124
|
+
if input_file or output_file:
|
|
125
|
+
layers_report(cfg.layer_index, input_file, output_file)
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@main.command()
|
|
130
|
+
@click.option(
|
|
131
|
+
"-c",
|
|
132
|
+
"--cfg-only",
|
|
133
|
+
is_flag=True,
|
|
134
|
+
default=False,
|
|
135
|
+
help="Read metadata from config only - ignore configured metadata message file.",
|
|
136
|
+
)
|
|
137
|
+
@click.option(
|
|
138
|
+
"-m",
|
|
139
|
+
"--msg-file",
|
|
140
|
+
default="messages.po",
|
|
141
|
+
help="Write to a message file with the translatable metadata from the configuration. (Defaults to 'messages.po')"
|
|
142
|
+
)
|
|
143
|
+
@click.argument("path", nargs=1, required=False)
|
|
144
|
+
def extract(path: str, cfg_only: bool, msg_file: str) -> int:
|
|
145
|
+
"""Extract metadata from existing configuration into a message file template.
|
|
146
|
+
|
|
147
|
+
Takes a configuration specification which is loaded as per the $DATACUBE_OWS_CFG environment variable.
|
|
148
|
+
|
|
149
|
+
If no specification is provided, the $DATACUBE_OWS_CFG environment variable is used.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
raw_cfg = read_config(path)
|
|
153
|
+
cfg = OWSConfig(refresh=True, cfg=raw_cfg, ignore_msgfile=cfg_only)
|
|
154
|
+
with Datacube() as dc:
|
|
155
|
+
cfg.make_ready(dc)
|
|
156
|
+
except ConfigException as e:
|
|
157
|
+
click.echo(f"Config exception for path {str(e)}")
|
|
158
|
+
return False
|
|
159
|
+
click.echo("Configuration parsed OK")
|
|
160
|
+
click.echo(f"Configured message file location: {cfg.msg_file_name}")
|
|
161
|
+
click.echo(f"Configured translations directory location: {cfg.translations_dir}")
|
|
162
|
+
write_msg_file(msg_file, cfg)
|
|
163
|
+
click.echo(f"Message file {msg_file} written")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@main.command()
|
|
168
|
+
@click.option(
|
|
169
|
+
"-n",
|
|
170
|
+
"--new",
|
|
171
|
+
is_flag=True,
|
|
172
|
+
default=False,
|
|
173
|
+
help="Create a new translation template. (Default is to update an existing one.)"
|
|
174
|
+
)
|
|
175
|
+
@click.option(
|
|
176
|
+
"-m",
|
|
177
|
+
"--msg-file",
|
|
178
|
+
default=None,
|
|
179
|
+
help="Use this message file as the template for translation files. (defaults to message filename from configuration)"
|
|
180
|
+
)
|
|
181
|
+
@click.option(
|
|
182
|
+
"-d",
|
|
183
|
+
"--translations-dir",
|
|
184
|
+
default=None,
|
|
185
|
+
help="Path to the output translations directory. Defaults to value from configuration"
|
|
186
|
+
)
|
|
187
|
+
@click.option(
|
|
188
|
+
"-D",
|
|
189
|
+
"--domain",
|
|
190
|
+
default=None,
|
|
191
|
+
help="The domain of the translation files. Defaults to value from configuration"
|
|
192
|
+
)
|
|
193
|
+
@click.option(
|
|
194
|
+
"-c",
|
|
195
|
+
"--cfg",
|
|
196
|
+
default=None,
|
|
197
|
+
help="Configuration specification to use to determine translations directory and domain (defaults to environment $DATACUBE_OWS_CFG)"
|
|
198
|
+
)
|
|
199
|
+
@click.argument("languages", nargs=-1)
|
|
200
|
+
def translation(languages: list[str], msg_file: str | None, new: bool,
|
|
201
|
+
domain: str | None, translations_dir: str | None, cfg: str | None) -> int:
|
|
202
|
+
"""Generate a new translations catalog based on the specified message file.
|
|
203
|
+
|
|
204
|
+
Takes a list of languages to generate catalogs for. "all" can be included as a shorthand
|
|
205
|
+
for all languages listed as supported in the configuration.
|
|
206
|
+
"""
|
|
207
|
+
if len(languages) == 0:
|
|
208
|
+
click.echo("No language(s) specified.")
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
if msg_file is None or domain is None or translations_dir is None or "all" in languages:
|
|
211
|
+
try:
|
|
212
|
+
raw_cfg = read_config(cfg)
|
|
213
|
+
config = OWSConfig(refresh=True, cfg=raw_cfg)
|
|
214
|
+
except ConfigException as e:
|
|
215
|
+
click.echo(f"Config exception for path: {str(e)}")
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
if domain is None:
|
|
218
|
+
click.echo(f"Using message domain '{config.message_domain}' from configuration")
|
|
219
|
+
domain = config.message_domain
|
|
220
|
+
if translations_dir is None and config.translations_dir is None:
|
|
221
|
+
click.echo("No translations directory was supplied or is configured")
|
|
222
|
+
sys.exit(1)
|
|
223
|
+
elif translations_dir is None:
|
|
224
|
+
click.echo(f"Using translations directory '{config.translations_dir}' from configuration")
|
|
225
|
+
translations_dir = config.translations_dir
|
|
226
|
+
if msg_file is None and config.msg_file_name is None:
|
|
227
|
+
click.echo("No message file name was supplied or is configured")
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
elif msg_file is None:
|
|
230
|
+
click.echo(f"Using message file location '{config.msg_file_name}' from configuration")
|
|
231
|
+
msg_file = config.msg_file_name
|
|
232
|
+
all_langs = config.locales
|
|
233
|
+
else:
|
|
234
|
+
all_langs = []
|
|
235
|
+
assert msg_file is not None # For type checker
|
|
236
|
+
assert domain is not None # For type checker
|
|
237
|
+
assert translations_dir is not None # For type checker
|
|
238
|
+
try:
|
|
239
|
+
fp = open(msg_file, "rb")
|
|
240
|
+
fp.close()
|
|
241
|
+
except OSError:
|
|
242
|
+
click.echo("Message file {msg_file} does not exist or cannot be read.")
|
|
243
|
+
sys.exit(1)
|
|
244
|
+
for language in languages:
|
|
245
|
+
if language == "all":
|
|
246
|
+
for supp_lang in all_langs:
|
|
247
|
+
if new:
|
|
248
|
+
create_translation(msg_file, translations_dir, domain, supp_lang)
|
|
249
|
+
else:
|
|
250
|
+
update_translation(msg_file, translations_dir, domain, supp_lang)
|
|
251
|
+
else:
|
|
252
|
+
if new:
|
|
253
|
+
create_translation(msg_file, translations_dir, domain, language)
|
|
254
|
+
else:
|
|
255
|
+
update_translation(msg_file, translations_dir, domain, language)
|
|
256
|
+
click.echo("Language templates created.")
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def create_translation(msg_file: str, translations_dir: str, domain: str, locale: str) -> bool:
|
|
261
|
+
click.echo(f"Creating template for language: {locale}")
|
|
262
|
+
os.system(f"pybabel init -i {msg_file} -d {translations_dir} -D {domain} -l {locale}")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def update_translation(msg_file: str, translations_dir: str, domain: str, locale: str) -> bool:
|
|
267
|
+
click.echo(f"Updating template for language: {locale}")
|
|
268
|
+
os.system(f"pybabel update --no-fuzzy-matching --ignore-obsolete "
|
|
269
|
+
f"-i {msg_file} -d {translations_dir} -D {domain} -l {locale}")
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@main.command(name="compile")
|
|
274
|
+
@click.option(
|
|
275
|
+
"-d",
|
|
276
|
+
"--translations-dir",
|
|
277
|
+
default=None,
|
|
278
|
+
help="Path to the output translations directory. Defaults to value from configuration"
|
|
279
|
+
)
|
|
280
|
+
@click.option(
|
|
281
|
+
"-D",
|
|
282
|
+
"--domain",
|
|
283
|
+
default=None,
|
|
284
|
+
help="The domain of the translation files. Defaults to value from configuration"
|
|
285
|
+
)
|
|
286
|
+
@click.option(
|
|
287
|
+
"-c",
|
|
288
|
+
"--cfg",
|
|
289
|
+
default=None,
|
|
290
|
+
help="Configuration specification to use to determine translations directory and domain (defaults to environment $DATACUBE_OWS_CFG)"
|
|
291
|
+
)
|
|
292
|
+
@click.argument("languages", nargs=-1)
|
|
293
|
+
def compile_cmd(languages: list[str], domain: str | None, translations_dir: str | None, cfg: str | None) -> int:
|
|
294
|
+
"""Compile completed translation files.
|
|
295
|
+
|
|
296
|
+
Takes a list of languages to generate catalogs for. "all" can be included as a shorthand
|
|
297
|
+
for all languages listed as supported in the configuration.
|
|
298
|
+
"""
|
|
299
|
+
if len(languages) == 0:
|
|
300
|
+
click.echo("No language(s) specified.")
|
|
301
|
+
sys.exit(1)
|
|
302
|
+
if domain is None or translations_dir is None or "all" in languages:
|
|
303
|
+
try:
|
|
304
|
+
raw_cfg = read_config(cfg)
|
|
305
|
+
config = OWSConfig(refresh=True, cfg=raw_cfg)
|
|
306
|
+
except ConfigException as e:
|
|
307
|
+
click.echo(f"Config exception for path: {str(e)}")
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
if domain is None:
|
|
310
|
+
click.echo(f"Using message domain '{config.message_domain}' from configuration")
|
|
311
|
+
domain = config.message_domain
|
|
312
|
+
if translations_dir is None and config.translations_dir is None:
|
|
313
|
+
click.echo("No translations directory was supplied or is configured")
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
elif translations_dir is None:
|
|
316
|
+
click.echo(f"Using translations directory '{config.translations_dir}' from configuration")
|
|
317
|
+
translations_dir = config.translations_dir
|
|
318
|
+
all_langs = config.locales
|
|
319
|
+
else:
|
|
320
|
+
all_langs = []
|
|
321
|
+
assert translations_dir is not None
|
|
322
|
+
for language in languages:
|
|
323
|
+
if language == "all":
|
|
324
|
+
for supp_lang in all_langs:
|
|
325
|
+
compile_translation(translations_dir, domain, supp_lang)
|
|
326
|
+
else:
|
|
327
|
+
compile_translation(translations_dir, domain, language)
|
|
328
|
+
click.echo("Language templates created.")
|
|
329
|
+
return 0
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def compile_translation(translations_dir: str, domain: str, language: str) -> bool:
|
|
333
|
+
click.echo(f"Compiling template for language: {language}")
|
|
334
|
+
os.system(f"pybabel compile -d {translations_dir} -D {domain} -l {language}")
|
|
335
|
+
return True
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def write_msg_file(msg_file: str, cfg: OWSConfig) -> None:
|
|
339
|
+
with open(msg_file, "wb") as fp:
|
|
340
|
+
write_po(fp, cfg.export_metadata())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def layers_report(config_values: dict[str, OWSNamedLayer], input_file: str, output_file: str) -> None:
|
|
344
|
+
report = {"total_layers_count": len(config_values.values()), "layers": []}
|
|
345
|
+
for lyr in config_values.values():
|
|
346
|
+
layer = {
|
|
347
|
+
"layer": lyr.name,
|
|
348
|
+
"product": list(lyr.product_names),
|
|
349
|
+
"styles_count": len(lyr.styles),
|
|
350
|
+
"styles_list": [styl.name for styl in lyr.styles],
|
|
351
|
+
}
|
|
352
|
+
cast(list[dict], report["layers"]).append(layer)
|
|
353
|
+
if input_file:
|
|
354
|
+
with open(input_file) as f:
|
|
355
|
+
input_file_data = json.load(f)
|
|
356
|
+
ddiff = DeepDiff(input_file_data, report, ignore_order=True)
|
|
357
|
+
if len(ddiff) != 0:
|
|
358
|
+
click.echo(ddiff)
|
|
359
|
+
sys.exit(1)
|
|
360
|
+
elif output_file:
|
|
361
|
+
with open(output_file, 'w') as reportfile:
|
|
362
|
+
json.dump(report, reportfile, indent=4)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def print_layers(layers: list[OWSLayer], styles: bool, depth: int) -> None:
|
|
366
|
+
for lyr in layers:
|
|
367
|
+
if isinstance(lyr, OWSFolder):
|
|
368
|
+
indent(depth)
|
|
369
|
+
click.echo(f"* {lyr.title}")
|
|
370
|
+
click.echo(f"{lyr.child_layers} {styles} {depth + 1}")
|
|
371
|
+
else:
|
|
372
|
+
indent(depth)
|
|
373
|
+
click.echo(f"{lyr.name} [{','.join(lyr.product_names)}]")
|
|
374
|
+
if styles:
|
|
375
|
+
click.echo(f"{lyr} {depth}")
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def print_styles(lyr: OWSNamedLayer, depth: int = 0) -> None:
|
|
379
|
+
for styl in lyr.styles:
|
|
380
|
+
indent(0, for_styles=True)
|
|
381
|
+
print(f". {styl.name}")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def indent(depth: int, for_styles: bool = False) -> None:
|
|
385
|
+
for i in range(depth):
|
|
386
|
+
click.echo(" ", nl=False)
|
|
387
|
+
if for_styles:
|
|
388
|
+
click.echo(" ", nl=False)
|