diff --git a/.github/ci-hpc-config.yml b/.github/ci-hpc-config.yml new file mode 100644 index 0000000..67a79c3 --- /dev/null +++ b/.github/ci-hpc-config.yml @@ -0,0 +1,3 @@ +build: + python: 3.10 + parallel: 1 \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b3c3d3f..84af453 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,153 +1,40 @@ name: ci + on: # Trigger the workflow on push to master or develop, except tag creation push: branches: - 'main' - 'develop' + tags-ignore: + - '**' + # Trigger the workflow on pull request pull_request: ~ + # Trigger the workflow manually workflow_dispatch: ~ + # Trigger after public PR approved for CI pull_request_target: types: [labeled] - release: - types: [created] -jobs: - qa: - name: qa - runs-on: ubuntu-20.04 - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - with: - repository: ${{ inputs.repository }} - ref: ${{ inputs.ref }} - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python_version }} - - name: Install Python Dependencies - run: | - python -m pip install --upgrade pip - python -m pip install black flake8 isort - - name: Check isort - run: isort --check . - - - name: Check black - run: black --check . - - - name: Check flake8 - run: flake8 . - setup: - name: setup - runs-on: ubuntu-20.04 - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - inputs: ${{ steps.prepare-inputs.outputs.inputs }} - inputs-for-ubuntu: ${{ steps.prepare-inputs.outputs.inputs-for-ubuntu }} - steps: - - name: Set Matrix - id: set-matrix - shell: bash -eux {0} - run: | - MATRIX=$(cat << 'EOS' - name: - - gnu-11@ubuntu-22.04 - - clang-14@ubuntu-22.04 - include: - - name: gnu-11@ubuntu-22.04 - os: ubuntu-22.04 - compiler: gnu-11 - compiler_cc: gcc-11 - compiler_cxx: g++-11 - compiler_fc: gfortran-11 - - name: clang-14@ubuntu-22.04 - os: ubuntu-22.04 - compiler: clang-14 - compiler_cc: clang-14 - compiler_cxx: clang++-14 - compiler_fc: gfortran-11 - # Xcode compiler requires empty environment variables, so we pass null (~) here - EOS - ) - SKIP_MATRIX_JOBS=$(cat << 'EOS' - ${{ inputs.skip_matrix_jobs }} - EOS - ) - SELECT_NAME_COND="1 != 1" - SELECT_INCLUDE_COND="1 != 1" - for skip_job in $SKIP_MATRIX_JOBS; do SELECT_NAME_COND="$SELECT_NAME_COND or . == \"$skip_job\""; SELECT_INCLUDE_COND="$SELECT_INCLUDE_COND or .name == \"$skip_job\""; done - echo matrix=$(echo "$MATRIX" | yq eval "del(.name[] | select($SELECT_NAME_COND)) | del(.include[] | select($SELECT_INCLUDE_COND))" --output-format json --indent 0 -) >> $GITHUB_OUTPUT - - name: Prepare build-package Inputs - id: prepare-inputs - shell: bash -eux {0} - run: | - echo inputs=$(echo "${{ inputs.build_package_inputs || '{}' }}" | yq eval '.' --output-format json --indent 0 -) >> $GITHUB_OUTPUT - echo inputs-for-ubuntu=$(echo "${{ inputs.build_package_inputs || '{}' }}" | yq eval '. * {"os":"ubuntu-20.04","compiler":"gnu-10","compiler_cc":"gcc-10","compiler_cxx":"g++-10","compiler_fc":"gfortran-10"}' --output-format json --indent 0 -) >> $GITHUB_OUTPUT - test: - name: test - needs: - - qa - - setup - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.setup.outputs.matrix) }} - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python_version }} - - - name: Install Python Dependencies - run: | - python -m pip install --upgrade pip - pip install --upgrade pip setuptools wheel - python -m pip install pytest pytest-cov - python -m pip install -r requirements.txt - python -m pip install -r ./tests/requirements_test.txt - - - name: Verify Source Distribution - shell: bash -eux {0} - run: | - python setup.py sdist - python -m pip install dist/* - - name: Run Tests with Repository Code - env: - LD_LIBRARY_PATH: ${{ steps.install-dependencies.outputs.lib_path }} - shell: bash -eux {0} - run: | - DYLD_LIBRARY_PATH=${{ env.LD_LIBRARY_PATH }} python -m pytest -m "not data" tests --cov=./ --cov-report=xml - python -m coverage report +jobs: + # Run CI including downstream packages on self-hosted runners + downstream-ci: + name: downstream-ci + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci.yml@main + with: + covjsonkit: ecmwf/covjsonkit@${{ github.event.pull_request.head.sha || github.sha }} + python_qa: true + secrets: inherit - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: coverage.xml - deploy: - needs: test - if: ${{ github.event_name == 'release' }} - name: Upload to Pypi - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: "__token__" - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python setup.py sdist - twine upload dist/* + # Build downstream packages on HPC + downstream-ci-hpc: + name: downstream-ci-hpc + if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }} + uses: ecmwf-actions/downstream-ci/.github/workflows/downstream-ci-hpc.yml@main + with: + covjsonkit: ecmwf/covjsonkit@${{ github.event.pull_request.head.sha || github.sha }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/label-public-pr.yml b/.github/workflows/label-public-pr.yml new file mode 100644 index 0000000..59b2bfa --- /dev/null +++ b/.github/workflows/label-public-pr.yml @@ -0,0 +1,10 @@ +# Manage labels of pull requests that originate from forks +name: label-public-pr + +on: + pull_request_target: + types: [opened, synchronize] + +jobs: + label: + uses: ecmwf-actions/reusable-workflows/.github/workflows/label-pr.yml@v2 diff --git a/README.md b/README.md index fcf70a9..bc612e8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,34 @@ | :warning: This project is BETA and will be experimental for the foreseeable future. Interfaces and functionality are likely to change. DO NOT use this software in any project/software that is operational. | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +
+
+
+
+
+
+
+
+ Concept • + Installation • + Example • + Testing +
+ +## Concept + +Covjsonkit is an ECMWF library for encoding and decoding coverageJSON files/objects of meteorlogical features such as vertical profiles and time series. * Encodes and decodes CoverageJSON objects * Convert CoverageJSON files to and from xarray +* Works in conjunction with ECMWFs Polytope feature extraction library Current features implemented: @@ -14,4 +36,54 @@ Current features implemented: * Vertical Profile * Bounding Box * Frame -* Path \ No newline at end of file +* Path +* Wkt Polygons +* Shapefiles + +## Installation + +Install the covjsonkit with Python 3 (>=3.7) from GitHub directly with the command + + python3 -m pip install git+ssh://git@github.com/ecmwf/covjsonkit.git@develop + +or from PyPI with the command + + python3 -m pip install covjsonkit + +## Example + +The library consists of an encoder and a decoder element. The decoder can be used to decode valid coverageJSON files that can be then be edited and accessed via the api. It can also be used to convert to ther formats such as xarray. + +### Decoder + +```Python +from covjsonkit.api import Covjsonkit + +decoder = Covjsonkit().decode(coverage.covjson) + +print(decoder.type) +print(decoder.parameters) +print(decoder.get_referencing()) + +ds = decoder.to_xarray() +``` + + +### Encoder + +The following example encodes data output from the polytope feature extraction library assuming polytope_output is a valid output from polytope. + +```Python +from covjsonkit.api import Covjsonkit + +encoder = Covjsonkit().encode("CoverageCollection", "BoundingBox") +res = encoder.from_polytope(polytope_output) +``` + +## Testing + +Python unit tests can be run with pytest: + + python -m pytest + +When a pull request is merged into develop or main a github actions CI pipeline is triggered to test formatting and unit tests. diff --git a/covjsonkit/decoder/BoundingBox.py b/covjsonkit/decoder/BoundingBox.py index 5385d33..0ed8b1c 100644 --- a/covjsonkit/decoder/BoundingBox.py +++ b/covjsonkit/decoder/BoundingBox.py @@ -39,7 +39,7 @@ def to_geopandas(self): pass def to_xarray(self): - dims = ["points"] + dims = ["number", "steps", "points"] dataarraydict = {} # Get coordinates @@ -49,6 +49,7 @@ def to_xarray(self): x.append(float(coord[0])) y.append(float(coord[1])) + """ # Get values for parameter in self.parameters: dataarray = xr.DataArray(self.get_values()[parameter][0], dims=dims) @@ -56,15 +57,52 @@ def to_xarray(self): dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"] dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"] dataarraydict[dataarray.attrs["long_name"]] = dataarray + """ + + values = {} + for parameter in self.parameters: + values[parameter] = [] + + numbers = [] + steps = [] + for coverage in self.coverages: + numbers.append(coverage["mars:metadata"]["number"]) + steps.append(coverage["mars:metadata"]["step"]) + for parameter in self.parameters: + values[parameter].append(coverage["ranges"][parameter]["values"]) + + numbers = list(set(numbers)) + steps = list(set(steps)) + + new_values = {} + for parameter in self.parameters: + new_values[parameter] = [] + for i, num in enumerate(numbers): + new_values[parameter].append([]) + for j, step in enumerate(steps): + new_values[parameter][i].append(values[parameter][i * len(steps) + j]) + + for parameter in self.parameters: + dataarray = xr.DataArray(new_values[parameter], dims=dims) + dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] + dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"] + dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"] + dataarraydict[dataarray.attrs["long_name"]] = dataarray ds = xr.Dataset( dataarraydict, - coords=dict(points=(["points"], list(range(0, len(x)))), x=(["points"], x), y=(["points"], y)), + coords=dict( + number=(["number"], numbers), + steps=(["steps"], steps), + points=(["points"], list(range(0, len(x)))), + x=(["points"], x), + y=(["points"], y), + ), ) for mars_metadata in self.mars_metadata[0]: ds.attrs[mars_metadata] = self.mars_metadata[0][mars_metadata] # Add date attribute - ds.attrs["date"] = self.get_coordinates()["t"]["values"] + ds.attrs["date"] = self.get_coordinates()["t"]["values"][0] return ds diff --git a/covjsonkit/decoder/decoder.py b/covjsonkit/decoder/decoder.py index 985424f..e1b63ab 100644 --- a/covjsonkit/decoder/decoder.py +++ b/covjsonkit/decoder/decoder.py @@ -28,6 +28,7 @@ def __init__(self, covjson): self.parameters = self.get_parameters() self.coordinates = self.get_referencing() self.mars_metadata = self.get_mars_metadata() + self.coverages = self.coverage.coverages def get_type(self): return self.covjson["type"] diff --git a/covjsonkit/encoder/BoundingBox.py b/covjsonkit/encoder/BoundingBox.py index 0aecb42..fc050df 100644 --- a/covjsonkit/encoder/BoundingBox.py +++ b/covjsonkit/encoder/BoundingBox.py @@ -1,8 +1,3 @@ -import json - -import pandas as pd -from covjson_pydantic.coverage import Coverage - from .encoder import Encoder @@ -10,6 +5,7 @@ class BoundingBox(Encoder): def __init__(self, type, domaintype): super().__init__(type, domaintype) self.covjson["domainType"] = "MultiPoint" + self.covjson["coverages"] = [] def add_coverage(self, mars_metadata, coords, values): new_coverage = {} @@ -20,8 +16,9 @@ def add_coverage(self, mars_metadata, coords, values): self.add_mars_metadata(new_coverage, mars_metadata) self.add_domain(new_coverage, coords) self.add_range(new_coverage, values) - cov = Coverage.model_validate_json(json.dumps(new_coverage)) - self.pydantic_coverage.coverages.append(cov) + self.covjson["coverages"].append(new_coverage) + # cov = Coverage.model_validate_json(json.dumps(new_coverage)) + # self.pydantic_coverage.coverages.append(json.dumps(new_coverage)) def add_domain(self, coverage, coords): coverage["domain"]["type"] = "Domain" @@ -30,7 +27,9 @@ def add_domain(self, coverage, coords): coverage["domain"]["axes"]["t"]["values"] = coords["t"] coverage["domain"]["axes"]["composite"] = {} coverage["domain"]["axes"]["composite"]["dataType"] = "tuple" - coverage["domain"]["axes"]["composite"]["coordinates"] = self.pydantic_coverage.referencing[0].coordinates + coverage["domain"]["axes"]["composite"]["coordinates"] = self.covjson["referencing"][0][ + "coordinates" + ] # self.pydantic_coverage.referencing[0].coordinates coverage["domain"]["axes"]["composite"]["values"] = coords["composite"] def add_range(self, coverage, values): @@ -80,28 +79,18 @@ def from_xarray(self, dataset): return self.covjson def from_polytope(self, result): - ancestors = [val.get_ancestors() for val in result.leaves] - values = [val.result for val in result.leaves] - - columns = [] - df_dict = {} - # Create empty dataframe - for feature in ancestors[0]: - columns.append(str(feature).split("=")[0]) - df_dict[str(feature).split("=")[0]] = [] - - # populate dataframe - for ancestor in ancestors: - for feature in ancestor: - df_dict[str(feature).split("=")[0]].append(str(feature).split("=")[1]) - values = [val.result for val in result.leaves] - df_dict["values"] = values - df = pd.DataFrame(df_dict) - params = df["param"].unique() + coords = {} + # coords['composite'] = [] + mars_metadata = {} + range_dict = {} + lat = 0 + param = 0 + number = [0] + step = 0 + dates = [0] - for param in params: - self.add_parameter(param) + self.walk_tree(result, lat, coords, mars_metadata, param, range_dict, number, step, dates) self.add_reference( { @@ -113,26 +102,22 @@ def from_polytope(self, result): } ) - mars_metadata = {} - mars_metadata["class"] = df["class"].unique()[0] - mars_metadata["expver"] = df["expver"].unique()[0] - mars_metadata["levtype"] = df["levtype"].unique()[0] - mars_metadata["type"] = df["type"].unique()[0] - mars_metadata["domain"] = df["domain"].unique()[0] - mars_metadata["stream"] = df["stream"].unique()[0] - - range_dict = {} - coords = {} - coords["composite"] = [] - coords["t"] = [df["date"].unique()[0] + "Z"] - - for param in params: - df_param = df[df["param"] == param] - range_dict[param] = df_param["values"].values.tolist() - - df_param = df[df["param"] == params[0]] - for row in df_param.iterrows(): - coords["composite"].append([row[1]["latitude"], row[1]["longitude"]]) - - self.add_coverage(mars_metadata, coords, range_dict) - return json.loads(self.get_json()) + for date in range_dict.keys(): + for num in range_dict[date].keys(): + val_dict = {} + for step in range_dict[date][num][self.parameters[0]].keys(): + val_dict[step] = {} + for para in range_dict[date][num].keys(): + for step in range_dict[date][num][para].keys(): + val_dict[step][para] = range_dict[date][num][para][step] + for step in val_dict.keys(): + mm = mars_metadata.copy() + mm["number"] = num + mm["step"] = step + self.add_coverage(mm, coords[date], val_dict[step]) + + # self.add_coverage(mars_metadata, coords, range_dict) + # return self.covjson + # with open('data.json', 'w') as f: + # json.dump(self.covjson, f) + return self.covjson diff --git a/covjsonkit/encoder/Frame.py b/covjsonkit/encoder/Frame.py index 0d01fed..8d0525c 100644 --- a/covjsonkit/encoder/Frame.py +++ b/covjsonkit/encoder/Frame.py @@ -1,8 +1,3 @@ -import json - -import pandas as pd -from covjson_pydantic.coverage import Coverage - from .encoder import Encoder @@ -10,6 +5,7 @@ class Frame(Encoder): def __init__(self, type, domaintype): super().__init__(type, domaintype) self.covjson["domainType"] = "MultiPoint" + self.covjson["coverages"] = [] def add_coverage(self, mars_metadata, coords, values): new_coverage = {} @@ -20,8 +16,9 @@ def add_coverage(self, mars_metadata, coords, values): self.add_mars_metadata(new_coverage, mars_metadata) self.add_domain(new_coverage, coords) self.add_range(new_coverage, values) - cov = Coverage.model_validate_json(json.dumps(new_coverage)) - self.pydantic_coverage.coverages.append(cov) + # cov = Coverage.model_validate_json(json.dumps(new_coverage)) + # self.pydantic_coverage.coverages.append(cov) + self.covjson["coverages"].append(new_coverage) def add_domain(self, coverage, coords): coverage["domain"]["type"] = "Domain" @@ -30,7 +27,9 @@ def add_domain(self, coverage, coords): coverage["domain"]["axes"]["t"]["values"] = coords["t"] coverage["domain"]["axes"]["composite"] = {} coverage["domain"]["axes"]["composite"]["dataType"] = "tuple" - coverage["domain"]["axes"]["composite"]["coordinates"] = self.pydantic_coverage.referencing[0].coordinates + coverage["domain"]["axes"]["composite"]["coordinates"] = self.covjson["referencing"][0][ + "coordinates" + ] # self.pydantic_coverage.referencing[0].coordinates coverage["domain"]["axes"]["composite"]["values"] = coords["composite"] def add_range(self, coverage, values): @@ -80,28 +79,18 @@ def from_xarray(self, dataset): return self.covjson def from_polytope(self, result): - ancestors = [val.get_ancestors() for val in result.leaves] - values = [val.result for val in result.leaves] - - columns = [] - df_dict = {} - # Create empty dataframe - for feature in ancestors[0]: - columns.append(str(feature).split("=")[0]) - df_dict[str(feature).split("=")[0]] = [] - - # populate dataframe - for ancestor in ancestors: - for feature in ancestor: - df_dict[str(feature).split("=")[0]].append(str(feature).split("=")[1]) - values = [val.result for val in result.leaves] - df_dict["values"] = values - df = pd.DataFrame(df_dict) - params = df["param"].unique() + coords = {} + # coords['composite'] = [] + mars_metadata = {} + range_dict = {} + lat = 0 + param = 0 + number = [0] + step = 0 + dates = [0] - for param in params: - self.add_parameter(param) + self.walk_tree(result, lat, coords, mars_metadata, param, range_dict, number, step, dates) self.add_reference( { @@ -113,26 +102,22 @@ def from_polytope(self, result): } ) - mars_metadata = {} - mars_metadata["class"] = df["class"].unique()[0] - mars_metadata["expver"] = df["expver"].unique()[0] - mars_metadata["levtype"] = df["levtype"].unique()[0] - mars_metadata["type"] = df["type"].unique()[0] - mars_metadata["domain"] = df["domain"].unique()[0] - mars_metadata["stream"] = df["stream"].unique()[0] - - range_dict = {} - coords = {} - coords["composite"] = [] - coords["t"] = [df["date"].unique()[0] + "Z"] - - for param in params: - df_param = df[df["param"] == param] - range_dict[param] = df_param["values"].values.tolist() - - df_param = df[df["param"] == params[0]] - for row in df_param.iterrows(): - coords["composite"].append([row[1]["latitude"], row[1]["longitude"]]) - - self.add_coverage(mars_metadata, coords, range_dict) - return json.loads(self.get_json()) + for date in range_dict.keys(): + for num in range_dict[date].keys(): + val_dict = {} + for step in range_dict[date][num][self.parameters[0]].keys(): + val_dict[step] = {} + for para in range_dict[date][num].keys(): + for step in range_dict[date][num][para].keys(): + val_dict[step][para] = range_dict[date][num][para][step] + for step in val_dict.keys(): + mm = mars_metadata.copy() + mm["number"] = num + mm["step"] = step + self.add_coverage(mm, coords[date], val_dict[step]) + + # self.add_coverage(mars_metadata, coords, range_dict) + # return self.covjson + # with open('data.json', 'w') as f: + # json.dump(self.covjson, f) + return self.covjson diff --git a/covjsonkit/encoder/Shapefile.py b/covjsonkit/encoder/Shapefile.py index f24bd3b..1cfdc4c 100644 --- a/covjsonkit/encoder/Shapefile.py +++ b/covjsonkit/encoder/Shapefile.py @@ -1,8 +1,3 @@ -import json - -import pandas as pd -from covjson_pydantic.coverage import Coverage - from .encoder import Encoder @@ -10,6 +5,7 @@ class Shapefile(Encoder): def __init__(self, type, domaintype): super().__init__(type, domaintype) self.covjson["domainType"] = "MultiPoint" + self.covjson["coverages"] = [] def add_coverage(self, mars_metadata, coords, values): new_coverage = {} @@ -20,8 +16,9 @@ def add_coverage(self, mars_metadata, coords, values): self.add_mars_metadata(new_coverage, mars_metadata) self.add_domain(new_coverage, coords) self.add_range(new_coverage, values) - cov = Coverage.model_validate_json(json.dumps(new_coverage)) - self.pydantic_coverage.coverages.append(cov) + self.covjson["coverages"].append(new_coverage) + # cov = Coverage.model_validate_json(json.dumps(new_coverage)) + # self.pydantic_coverage.coverages.append(cov) def add_domain(self, coverage, coords): coverage["domain"]["type"] = "Domain" @@ -30,7 +27,9 @@ def add_domain(self, coverage, coords): coverage["domain"]["axes"]["t"]["values"] = coords["t"] coverage["domain"]["axes"]["composite"] = {} coverage["domain"]["axes"]["composite"]["dataType"] = "tuple" - coverage["domain"]["axes"]["composite"]["coordinates"] = self.pydantic_coverage.referencing[0].coordinates + coverage["domain"]["axes"]["composite"]["coordinates"] = self.covjson["referencing"][0][ + "coordinates" + ] # self.pydantic_coverage.referencing[0].coordinates coverage["domain"]["axes"]["composite"]["values"] = coords["composite"] def add_range(self, coverage, values): @@ -80,28 +79,18 @@ def from_xarray(self, dataset): return self.covjson def from_polytope(self, result): - ancestors = [val.get_ancestors() for val in result.leaves] - values = [val.result for val in result.leaves] - - columns = [] - df_dict = {} - # Create empty dataframe - for feature in ancestors[0]: - columns.append(str(feature).split("=")[0]) - df_dict[str(feature).split("=")[0]] = [] - - # populate dataframe - for ancestor in ancestors: - for feature in ancestor: - df_dict[str(feature).split("=")[0]].append(str(feature).split("=")[1]) - values = [val.result for val in result.leaves] - df_dict["values"] = values - df = pd.DataFrame(df_dict) - params = df["param"].unique() + coords = {} + # coords['composite'] = [] + mars_metadata = {} + range_dict = {} + lat = 0 + param = 0 + number = [0] + step = 0 + dates = [0] - for param in params: - self.add_parameter(param) + self.walk_tree(result, lat, coords, mars_metadata, param, range_dict, number, step, dates) self.add_reference( { @@ -113,26 +102,22 @@ def from_polytope(self, result): } ) - mars_metadata = {} - mars_metadata["class"] = df["class"].unique()[0] - mars_metadata["expver"] = df["expver"].unique()[0] - mars_metadata["levtype"] = df["levtype"].unique()[0] - mars_metadata["type"] = df["type"].unique()[0] - mars_metadata["domain"] = df["domain"].unique()[0] - mars_metadata["stream"] = df["stream"].unique()[0] - - range_dict = {} - coords = {} - coords["composite"] = [] - coords["t"] = [df["date"].unique()[0] + "Z"] - - for param in params: - df_param = df[df["param"] == param] - range_dict[param] = df_param["values"].values.tolist() - - df_param = df[df["param"] == params[0]] - for row in df_param.iterrows(): - coords["composite"].append([row[1]["latitude"], row[1]["longitude"]]) - - self.add_coverage(mars_metadata, coords, range_dict) - return json.loads(self.get_json()) + for date in range_dict.keys(): + for num in range_dict[date].keys(): + val_dict = {} + for step in range_dict[date][num][self.parameters[0]].keys(): + val_dict[step] = {} + for para in range_dict[date][num].keys(): + for step in range_dict[date][num][para].keys(): + val_dict[step][para] = range_dict[date][num][para][step] + for step in val_dict.keys(): + mm = mars_metadata.copy() + mm["number"] = num + mm["step"] = step + self.add_coverage(mm, coords[date], val_dict[step]) + + # self.add_coverage(mars_metadata, coords, range_dict) + # return self.covjson + # with open('data.json', 'w') as f: + # json.dump(self.covjson, f) + return self.covjson diff --git a/covjsonkit/encoder/TimeSeries.py b/covjsonkit/encoder/TimeSeries.py index 21f2a46..566a576 100644 --- a/covjsonkit/encoder/TimeSeries.py +++ b/covjsonkit/encoder/TimeSeries.py @@ -1,8 +1,6 @@ -import json from datetime import datetime, timedelta import pandas as pd -from covjson_pydantic.coverage import Coverage from .encoder import Encoder @@ -10,6 +8,8 @@ class TimeSeries(Encoder): def __init__(self, type, domaintype): super().__init__(type, domaintype) + self.covjson["domainType"] = "PointSeries" + self.covjson["coverages"] = [] def add_coverage(self, mars_metadata, coords, values): new_coverage = {} @@ -20,8 +20,9 @@ def add_coverage(self, mars_metadata, coords, values): self.add_mars_metadata(new_coverage, mars_metadata) self.add_domain(new_coverage, coords) self.add_range(new_coverage, values) - cov = Coverage.model_validate_json(json.dumps(new_coverage)) - self.pydantic_coverage.coverages.append(cov) + self.covjson["coverages"].append(new_coverage) + # cov = Coverage.model_validate_json(json.dumps(new_coverage)) + # self.pydantic_coverage.coverages.append(cov) def add_domain(self, coverage, coords): coverage["domain"]["type"] = "Domain" @@ -82,6 +83,7 @@ def from_xarray(self, dataset): return self.covjson def from_polytope(self, result): + """ ancestors = [val.get_ancestors() for val in result.leaves] values = [val.result for val in result.leaves] @@ -160,3 +162,108 @@ def from_polytope(self, result): self.add_coverage(new_metadata, coords, range_dict) return json.loads(self.get_json()) + """ + + coords = {} + coords["x"] = [] + coords["y"] = [] + coords["z"] = [] + coords["t"] = [] + mars_metadata = {} + range_dict = {} + lat = 0 + param = 0 + number = 0 + step = 0 + long = 0 + + self.func(result, lat, long, coords, mars_metadata, param, range_dict, number, step) + # print(range_dict) + + self.add_reference( + { + "coordinates": ["x", "y", "z"], + "system": { + "type": "GeographicCRS", + "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + }, + } + ) + + for param in range_dict[1].keys(): + self.add_parameter(param) + + for step in range_dict[1][self.parameters[0]].keys(): + date_format = "%Y%m%dT%H%M%S" + date = pd.Timestamp(coords["t"][0]).strftime(date_format) + start_time = datetime.strptime(date, date_format) + # add current date to list by converting it to iso format + stamp = start_time + timedelta(hours=int(step)) + coords["t"].append(stamp.isoformat() + "Z") + + val_dict = {} + for num in range_dict.keys(): + val_dict[num] = {} + for para in range_dict[1].keys(): + val_dict[num][para] = [] + for para in range_dict[num].keys(): + # for step in range_dict[num][para].keys(): + for step in range_dict[num][para]: + val_dict[num][para].extend(range_dict[num][para][step]) + mm = mars_metadata.copy() + mm["number"] = num + self.add_coverage(mm, coords, val_dict[num]) + + return self.covjson + + def func(self, tree, lat, long, coords, mars_metadata, param, range_dict, number, step): + if len(tree.children) != 0: + # recurse while we are not a leaf + for c in tree.children: + if ( + c.axis.name != "latitude" + and c.axis.name != "longitude" + and c.axis.name != "param" + and c.axis.name != "step" + and c.axis.name != "date" + ): + mars_metadata[c.axis.name] = c.values[0] + if c.axis.name == "latitude": + lat = c.values[0] + if c.axis.name == "param": + param = c.values + for num in range_dict: + for para in param: + range_dict[num][para] = {} + if c.axis.name == "date": + coords["t"] = [str(c.values[0]) + "Z"] + mars_metadata[c.axis.name] = str(c.values[0]) + "Z" + if c.axis.name == "number": + number = c.values + for num in number: + range_dict[num] = {} + if c.axis.name == "step": + step = c.values + for num in number: + for para in param: + for s in step: + range_dict[num][para][s] = [] + + self.func(c, lat, long, coords, mars_metadata, param, range_dict, number, step) + else: + vals = len(tree.values) + tree.values = [float(val) for val in tree.values] + tree.result = [float(val) for val in tree.result] + num_intervals = int(len(tree.result) / len(number)) + para_intervals = int(num_intervals / len(param)) + + coords["x"] = [lat] + coords["y"] = [long] + coords["z"] = ["sfc"] + + for num in range_dict: + for i, para in enumerate(range_dict[num]): + for s in range_dict[num][para]: + start = ((int(num) - 1) * num_intervals) + (vals * int(s)) + ((i * para_intervals)) + end = ((int(num) - 1) * num_intervals) + ((vals) * int(s + 1)) + ((i) * (para_intervals)) + range_dict[num][para][s].extend(tree.result[start:end]) diff --git a/covjsonkit/encoder/Wkt.py b/covjsonkit/encoder/Wkt.py index d0e0de2..cd7386e 100644 --- a/covjsonkit/encoder/Wkt.py +++ b/covjsonkit/encoder/Wkt.py @@ -1,8 +1,3 @@ -import json - -import pandas as pd -from covjson_pydantic.coverage import Coverage - from .encoder import Encoder @@ -10,6 +5,7 @@ class Wkt(Encoder): def __init__(self, type, domaintype): super().__init__(type, domaintype) self.covjson["domainType"] = "MultiPoint" + self.covjson["coverages"] = [] def add_coverage(self, mars_metadata, coords, values): new_coverage = {} @@ -20,8 +16,9 @@ def add_coverage(self, mars_metadata, coords, values): self.add_mars_metadata(new_coverage, mars_metadata) self.add_domain(new_coverage, coords) self.add_range(new_coverage, values) - cov = Coverage.model_validate_json(json.dumps(new_coverage)) - self.pydantic_coverage.coverages.append(cov) + self.covjson["coverages"].append(new_coverage) + # cov = Coverage.model_validate_json(json.dumps(new_coverage)) + # self.pydantic_coverage.coverages.append(cov) def add_domain(self, coverage, coords): coverage["domain"]["type"] = "Domain" @@ -30,7 +27,9 @@ def add_domain(self, coverage, coords): coverage["domain"]["axes"]["t"]["values"] = coords["t"] coverage["domain"]["axes"]["composite"] = {} coverage["domain"]["axes"]["composite"]["dataType"] = "tuple" - coverage["domain"]["axes"]["composite"]["coordinates"] = self.pydantic_coverage.referencing[0].coordinates + coverage["domain"]["axes"]["composite"]["coordinates"] = self.covjson["referencing"][0][ + "coordinates" + ] # self.pydantic_coverage.referencing[0].coordinates coverage["domain"]["axes"]["composite"]["values"] = coords["composite"] def add_range(self, coverage, values): @@ -80,28 +79,18 @@ def from_xarray(self, dataset): return self.covjson def from_polytope(self, result): - ancestors = [val.get_ancestors() for val in result.leaves] - values = [val.result for val in result.leaves] - - columns = [] - df_dict = {} - # Create empty dataframe - for feature in ancestors[0]: - columns.append(str(feature).split("=")[0]) - df_dict[str(feature).split("=")[0]] = [] - - # populate dataframe - for ancestor in ancestors: - for feature in ancestor: - df_dict[str(feature).split("=")[0]].append(str(feature).split("=")[1]) - values = [val.result for val in result.leaves] - df_dict["values"] = values - df = pd.DataFrame(df_dict) - params = df["param"].unique() + coords = {} + # coords['composite'] = [] + mars_metadata = {} + range_dict = {} + lat = 0 + param = 0 + number = [0] + step = 0 + dates = [0] - for param in params: - self.add_parameter(param) + self.walk_tree(result, lat, coords, mars_metadata, param, range_dict, number, step, dates) self.add_reference( { @@ -113,26 +102,22 @@ def from_polytope(self, result): } ) - mars_metadata = {} - mars_metadata["class"] = df["class"].unique()[0] - mars_metadata["expver"] = df["expver"].unique()[0] - mars_metadata["levtype"] = df["levtype"].unique()[0] - mars_metadata["type"] = df["type"].unique()[0] - mars_metadata["domain"] = df["domain"].unique()[0] - mars_metadata["stream"] = df["stream"].unique()[0] - - range_dict = {} - coords = {} - coords["composite"] = [] - coords["t"] = [df["date"].unique()[0] + "Z"] - - for param in params: - df_param = df[df["param"] == param] - range_dict[param] = df_param["values"].values.tolist() - - df_param = df[df["param"] == params[0]] - for row in df_param.iterrows(): - coords["composite"].append([row[1]["latitude"], row[1]["longitude"]]) - - self.add_coverage(mars_metadata, coords, range_dict) - return json.loads(self.get_json()) + for date in range_dict.keys(): + for num in range_dict[date].keys(): + val_dict = {} + for step in range_dict[date][num][self.parameters[0]].keys(): + val_dict[step] = {} + for para in range_dict[date][num].keys(): + for step in range_dict[date][num][para].keys(): + val_dict[step][para] = range_dict[date][num][para][step] + for step in val_dict.keys(): + mm = mars_metadata.copy() + mm["number"] = num + mm["step"] = step + self.add_coverage(mm, coords[date], val_dict[step]) + + # self.add_coverage(mars_metadata, coords, range_dict) + # return self.covjson + # with open('data.json', 'w') as f: + # json.dump(self.covjson, f) + return self.covjson diff --git a/covjsonkit/encoder/encoder.py b/covjsonkit/encoder/encoder.py index 68d2e5f..f1f857e 100644 --- a/covjsonkit/encoder/encoder.py +++ b/covjsonkit/encoder/encoder.py @@ -1,22 +1,25 @@ -import json from abc import ABC, abstractmethod +import orjson from covjson_pydantic.coverage import CoverageCollection from covjson_pydantic.domain import DomainType -from covjson_pydantic.parameter import Parameter -from covjson_pydantic.reference_system import ReferenceSystemConnectionObject -from covjsonkit.param_db import get_param_from_db, get_unit_from_db +from covjsonkit.param_db import get_param_ids, get_params, get_units class Encoder(ABC): def __init__(self, type, domaintype): self.covjson = {} + self.covjson["type"] = "CoverageCollection" self.type = type self.referencing = [] + self.units = get_units() + self.params = get_params() + self.param_ids = get_param_ids() + domaintype = domaintype.lower() if domaintype == "pointseries": @@ -37,24 +40,13 @@ def __init__(self, type, domaintype): self.pydantic_coverage = CoverageCollection( type=type, coverages=[], domainType=self.domaintype, parameters={}, referencing=[] ) - # self.covjson = self.pydantic_coverage.model_dump_json(exclude_none=True) self.parameters = [] - # self.covjson["type"] = self.type - # self.covjson["domainType"] = domaintype - # self.covjson["coverages"] = [] - # self.covjson["parameters"] = {} - # self.covjson["referencing"] = [] - - # if type == "Coverage": - # self.coverage = Coverage(self.covjson) - # elif type == "CoverageCollection": - # self.coverage = CoverageCollection(self.covjson) - # else: - # raise TypeError("Type must be Coverage or CoverageCollection") def add_parameter(self, param): - param_dict = get_param_from_db(param) - unit = get_unit_from_db(param_dict["unit_id"]) + # param_dict = get_param_from_db(param) + # unit = get_unit_from_db(param_dict["unit_id"]) + param_dict = self.params[str(param)] + unit = self.units[str(param_dict["unit_id"])] parameter = { "type": "Parameter", "description": {"en": param_dict["description"]}, @@ -64,31 +56,121 @@ def add_parameter(self, param): "label": {"en": param_dict["name"]}, }, } - self.pydantic_coverage.parameters[param_dict["shortname"]] = Parameter.model_validate_json( - json.dumps(parameter) - ) + # self.pydantic_coverage.parameters[param_dict["shortname"]] = Parameter.model_validate_json( + # json.dumps(parameter) + # ) + if "parameters" not in self.covjson: + self.covjson["parameters"] = {} + self.covjson["parameters"][param_dict["shortname"]] = parameter + else: + self.covjson["parameters"][param_dict["shortname"]] = parameter self.parameters.append(param) def add_reference(self, reference): - self.pydantic_coverage.referencing.append( - ReferenceSystemConnectionObject.model_validate_json(json.dumps(reference)) - ) + # self.pydantic_coverage.referencing.append( + # ReferenceSystemConnectionObject.model_validate_json(json.dumps(reference)) + # ) # self.pydantic_coverage.referencing.append(reference) - for ref in reference["coordinates"]: - if ref not in self.referencing: - self.referencing.append(ref) + # for ref in reference["coordinates"]: + # if ref not in self.referencing: + # self.referencing.append(ref) + self.covjson["referencing"] = [reference] def convert_param_id_to_param(self, paramid): try: param = int(paramid) except BaseException: return paramid - param_dict = get_param_from_db(int(param)) + # param_dict = get_param_from_db(int(param)) + param_dict = self.params[str(param)] return param_dict["shortname"] def get_json(self): - self.covjson = self.pydantic_coverage.model_dump_json(exclude_none=True, indent=4) - return self.covjson + # self.covjson = self.pydantic_coverage.model_dump_json(exclude_none=True, indent=4) + return orjson.dumps(self.covjson) + + def walk_tree(self, tree, lat, coords, mars_metadata, param, range_dict, number, step, dates): + if len(tree.children) != 0: + # recurse while we are not a leaf + for c in tree.children: + if ( + c.axis.name != "latitude" + and c.axis.name != "longitude" + and c.axis.name != "param" + and c.axis.name != "date" + ): + mars_metadata[c.axis.name] = c.values[0] + if c.axis.name == "latitude": + lat = c.values[0] + if c.axis.name == "param": + param = c.values + for date in range_dict.keys(): + if range_dict[date] == {}: + range_dict[date] = {0: {}} + for num in number: + for para in param: + if para not in range_dict[date][num]: + range_dict[date][num][para] = {} + self.add_parameter(para) + if c.axis.name == "date": + dates = [str(date) + "Z" for date in c.values] + for date in dates: + coords[date] = {} + coords[date]["composite"] = [] + coords[date]["t"] = [date] + if date not in range_dict: + range_dict[date] = {} + if c.axis.name == "number": + number = c.values + for date in dates: + for num in number: + range_dict[date][num] = {} + if c.axis.name == "step": + step = c.values + for date in dates: + for num in number: + for para in param: + for s in step: + range_dict[date][num][para][s] = [] + + self.walk_tree(c, lat, coords, mars_metadata, param, range_dict, number, step, dates) + else: + # vals = len(tree.values) + tree.values = [float(val) for val in tree.values] + if all(val is None for val in tree.result): + range_dict.pop(dates[0], None) + else: + tree.result = [float(val) if val is not None else val for val in tree.result] + num_len = len(tree.result) / len(number) + para_len = num_len / len(param) + step_len = para_len / len(step) + + for date in dates: + for val in tree.values: + coords[date]["composite"].append([lat, val]) + + # print(lat) + # print(number) + # print(dates) + # print(param) + # print(step) + # print(tree.values) + # print(para_len) + # print(step_len) + # print(tree.result) + + for i, num in enumerate(number): + for j, para in enumerate(param): + for k, s in enumerate(step): + range_dict[dates[0]][num][para][s].extend( + tree.result[ + int(i * num_len) + + int(j * para_len) + + int(k * step_len) : int(i * num_len) + + int(j * para_len) + + int((k + 1) * step_len) + ] + ) @abstractmethod def add_coverage(self, mars_metadata, coords, values): diff --git a/covjsonkit/param_db.py b/covjsonkit/param_db.py index 85e1f53..2b0ee6d 100644 --- a/covjsonkit/param_db.py +++ b/covjsonkit/param_db.py @@ -38,3 +38,24 @@ def get_unit_from_db(unit_id): with open(unit_path) as f: units = json.load(f) return units[str(unit_id)] + + +def get_param_ids(): + param_id_path = os.path.join(dirname(__file__), "data/param_id.json") + with open(param_id_path) as f: + param_ids = json.load(f) + return param_ids + + +def get_params(): + param_path = os.path.join(dirname(__file__), "data/param.json") + with open(param_path) as f: + params = json.load(f) + return params + + +def get_units(): + unit_path = os.path.join(dirname(__file__), "data/unit.json") + with open(unit_path) as f: + units = json.load(f) + return units diff --git a/covjsonkit/version.py b/covjsonkit/version.py index 9b36b86..b2f0155 100644 --- a/covjsonkit/version.py +++ b/covjsonkit/version.py @@ -1 +1 @@ -__version__ = "0.0.10" +__version__ = "0.0.11" diff --git a/docs/images/ECMWF_logo.svg.png b/docs/images/ECMWF_logo.svg.png new file mode 100644 index 0000000..69266e1 Binary files /dev/null and b/docs/images/ECMWF_logo.svg.png differ diff --git a/docs/images/Logo_Destination_Earth_Colours.png b/docs/images/Logo_Destination_Earth_Colours.png new file mode 100644 index 0000000..f4ab398 Binary files /dev/null and b/docs/images/Logo_Destination_Earth_Colours.png differ diff --git a/requirements.txt b/requirements.txt index 7e93d6f..58b5d3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ pandas +orjson datetime polytope +numpy xarray==2022.12.0 -pandas==1.5.2 +pandas==2.2.0 covjson-pydantic diff --git a/tests/test_encoder_bounding_box.py b/tests/test_encoder_bounding_box.py index 51a94ab..43c3100 100644 --- a/tests/test_encoder_bounding_box.py +++ b/tests/test_encoder_bounding_box.py @@ -1,5 +1,4 @@ from covjson_pydantic.coverage import CoverageCollection -from covjson_pydantic.domain import DomainType from covjsonkit.api import Covjsonkit @@ -144,11 +143,12 @@ def test_CoverageCollection(self): def test_standard_Coverage(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "BoundingBox") - covjson = CoverageCollection( - type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] - ) + # covjson = CoverageCollection( + # type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] + # ) - assert encoder_obj.get_json() == covjson.model_dump_json(exclude_none=True, indent=4) + covjson = {"type": "CoverageCollection", "domainType": "MultiPoint", "coverages": []} + assert encoder_obj.covjson == covjson def test_add_parameter(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "BoundingBox") diff --git a/tests/test_encoder_frame.py b/tests/test_encoder_frame.py index f916171..9a2b1ed 100644 --- a/tests/test_encoder_frame.py +++ b/tests/test_encoder_frame.py @@ -1,5 +1,4 @@ from covjson_pydantic.coverage import CoverageCollection -from covjson_pydantic.domain import DomainType from covjsonkit.api import Covjsonkit @@ -144,11 +143,12 @@ def test_CoverageCollection(self): def test_standard_Coverage(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "Frame") - covjson = CoverageCollection( - type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] - ) + # covjson = CoverageCollection( + # type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] + # ) - assert encoder_obj.get_json() == covjson.model_dump_json(exclude_none=True, indent=4) + covjson = {"type": "CoverageCollection", "domainType": "MultiPoint", "coverages": []} + assert encoder_obj.covjson == covjson def test_add_parameter(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "Frame") diff --git a/tests/test_encoder_shapefile.py b/tests/test_encoder_shapefile.py index 851c7cc..d7e575c 100644 --- a/tests/test_encoder_shapefile.py +++ b/tests/test_encoder_shapefile.py @@ -1,5 +1,4 @@ from covjson_pydantic.coverage import CoverageCollection -from covjson_pydantic.domain import DomainType from covjsonkit.api import Covjsonkit @@ -144,11 +143,12 @@ def test_CoverageCollection(self): def test_standard_Coverage(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "shapefile") - covjson = CoverageCollection( - type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] - ) + # covjson = CoverageCollection( + # type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] + # ) - assert encoder_obj.get_json() == covjson.model_dump_json(exclude_none=True, indent=4) + covjson = {"type": "CoverageCollection", "domainType": "MultiPoint", "coverages": []} + assert encoder_obj.covjson == covjson def test_add_parameter(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "shapefile") diff --git a/tests/test_encoder_time_series.py b/tests/test_encoder_time_series.py index edd3b4e..74096d3 100644 --- a/tests/test_encoder_time_series.py +++ b/tests/test_encoder_time_series.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from covjson_pydantic.coverage import CoverageCollection -from covjson_pydantic.domain import DomainType from covjsonkit.api import Covjsonkit @@ -155,11 +154,12 @@ def test_CoverageCollection(self): def test_standard_Coverage(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "PointSeries") - covjson = CoverageCollection( - type="CoverageCollection", coverages=[], domainType=DomainType.point_series, parameters={}, referencing=[] - ) + # covjson = CoverageCollection( + # type="CoverageCollection", coverages=[], domainType=DomainType.point_series, parameters={}, referencing=[] + # ) - assert encoder_obj.get_json() == covjson.model_dump_json(exclude_none=True, indent=4) + covjson = {"type": "CoverageCollection", "domainType": "PointSeries", "coverages": []} + assert encoder_obj.covjson == covjson def test_add_parameter(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "PointSeries") diff --git a/tests/test_encoder_wkt.py b/tests/test_encoder_wkt.py index f5f68dc..6fafd26 100644 --- a/tests/test_encoder_wkt.py +++ b/tests/test_encoder_wkt.py @@ -1,5 +1,4 @@ from covjson_pydantic.coverage import CoverageCollection -from covjson_pydantic.domain import DomainType from covjsonkit.api import Covjsonkit @@ -144,11 +143,12 @@ def test_CoverageCollection(self): def test_standard_Coverage(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "wkt") - covjson = CoverageCollection( - type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] - ) + # covjson = CoverageCollection( + # type="CoverageCollection", coverages=[], domainType=DomainType.multi_point, parameters={}, referencing=[] + # ) - assert encoder_obj.get_json() == covjson.model_dump_json(exclude_none=True, indent=4) + covjson = {"type": "CoverageCollection", "domainType": "MultiPoint", "coverages": []} + assert encoder_obj.covjson == covjson def test_add_parameter(self): encoder_obj = Covjsonkit().encode("CoverageCollection", "wkt")