tyler 0.3.14

Create tiles from 3D city objects encoded as CityJSONFeatures.
"""Split a directory of cityjson files to cityjsonfeature files, one file per feature.
One main cityjson file (metadata.city.json) is written for all the features and the
'transform' property is computed from the extent of all files.

Copyright 2023 Balázs Dukai, Ravi Peters

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from pathlib import Path
from sys import argv
from os import cpu_count
from concurrent.futures import ProcessPoolExecutor, as_completed
import json

from cjio import cityjson

# zwaartepunt in Nederland
TRANSLATE = [171800.0, 472700.0, 0.0]
IMPORTANT_DIGITS = 3

input_dir = Path(argv[1]).resolve()
output_dir = Path(argv[2]).resolve()
max_workers = int(argv[3]) if int(argv[3]) <= cpu_count() else cpu_count()

if input_dir.is_dir():
    cityjson_path_list = [cityjson_path for cityjson_path in input_dir.iterdir()]
else:
    cityjson_path_list = [input_dir, ]
if len(cityjson_path_list) <= max_workers:
    max_workers = len(cityjson_path_list)
print(f"Using maximum {max_workers} workers")


# --- Compute translation properties from the extent

# Compute the extent of all the files
def bbox_from_file(path: Path):
    with path.open(mode='r', encoding='utf-8-sig') as f:
        cm = cityjson.reader(file=f, ignore_duplicate_keys=False)
        return cm.get_bbox()


futures = []

# Init the extent
with cityjson_path_list[0].open(mode='r', encoding='utf-8-sig') as f:
    cm = cityjson.reader(file=f, ignore_duplicate_keys=False)
    extent = cm.calculate_bbox()

with ProcessPoolExecutor(max_workers=max_workers) as executor:
    for cj_path in cityjson_path_list:
        futures.append(executor.submit(bbox_from_file, cj_path))
    for i, future in enumerate(as_completed(futures)):
        minx, miny, minz, maxx, maxy, maxz = future.result()
        if minx < extent[0]:
            extent[0] = minx
        if miny < extent[1]:
            extent[1] = miny
        if minz < extent[2]:
            extent[2] = minz
        if maxx > extent[3]:
            extent[3] = maxx
        if maxy > extent[4]:
            extent[4] = maxx
        if maxz > extent[5]:
            extent[5] = maxz
del cm, i, future, futures, executor

# The features are centered around the center of the extent
dx = extent[3] - extent[0]
dy = extent[4] - extent[1]
dz = extent[5] - extent[2]
center = [extent[0] + dx * 0.5, extent[1] + dy * 0.5, extent[2] + dz * 0.5]
translate = TRANSLATE
print(f"Computed translation property: {translate}")


# --- Write the metadata file
with cityjson_path_list[0].open(mode='r', encoding='utf-8-sig') as f:
    cm = cityjson.reader(file=f, ignore_duplicate_keys=False)
    cm.upgrade_version("1.1", digit=IMPORTANT_DIGITS)
    cm.decompress()
    cm.compress(important_digits=IMPORTANT_DIGITS, translate=translate)
    outfile = output_dir / "metadata.city.json"
    with outfile.open("w") as fo:
        fo.write(cm.cityjson_for_features())
    print(f"Written {outfile}")
del cm, outfile

# --- Split to features
def file_to_feature_files(filepath: Path, out_dir: Path):
    with filepath.open(mode='r', encoding='utf-8-sig') as f:
        cm = cityjson.reader(file=f, ignore_duplicate_keys=False)
        cm.upgrade_version("1.1", digit=IMPORTANT_DIGITS)
        cm.decompress()
        cm.compress(important_digits=IMPORTANT_DIGITS, translate=translate)

        fail = []
        # e.g: 'gb2' in /home/cjio/gb2.city.json
        old_filename = filepath.name.replace("".join(filepath.suffixes), "")
        # e.g: '/home/cjio/gb2' in /home/cjio/gb2.city.json
        filedir = out_dir / old_filename
        filedir.mkdir(exist_ok=True)
        for feature in cm.generate_features():
            feature_id = feature.j['id']
            new_filename = f"{feature_id}.city.jsonl"
            filepath = filedir / new_filename
            try:
                with open(filepath, "w") as fout:
                    fout.write(json.dumps(feature.j, separators=(',', ':')))
            except IOError as e:
                print(f"Invalid output file: {filepath}\n{e}")
                fail.append(feature_id)
            except BaseException as e:
                print(e)
                fail.append(feature_id)
        return fail


failed = []
futures = []
with ProcessPoolExecutor(max_workers=max_workers) as executor:
    for cj_path in cityjson_path_list:
        futures.append(executor.submit(file_to_feature_files, cj_path, output_dir))
    for i, future in enumerate(as_completed(futures)):
        failed.extend(future.result())
del i, future, futures, executor

print(f"Failed to export the CityObjects: {failed}")