Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/cjio metadata #56

Merged
merged 27 commits into from
Jul 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
810a514
Add skeleton for updating the metadata
hugoledoux Jun 2, 2020
23471c9
Add initial metadata functionality
Athelena Jun 26, 2020
9cfac39
Add --overwrite option in update_metadata command
Athelena Jul 9, 2020
4bd6e75
Fix datasetReferenceDate formatting in metadata computation
Athelena Jul 9, 2020
001507b
Treat datasetReferenceDate like citymodelIdentifier in update_metadata
Athelena Jul 9, 2020
df66ed5
Fix issue with discovery of textures/materials in update_metadata
Athelena Jul 9, 2020
822fd40
Remove the update_bbox command
Athelena Jul 24, 2020
a1a5482
Improve calculate_bbox function
Athelena Jul 24, 2020
c57667a
Fix small things in metadata
Athelena Jul 24, 2020
a3d25b3
Add docstring in calculate_bbox
Athelena Jul 24, 2020
c05299d
Rename metadata functions and update docstrings
Athelena Jul 24, 2020
d1c2375
Add get_metadata command
Athelena Jul 24, 2020
23f3e90
Add function to add lineage items in CityJSON
Athelena Jul 24, 2020
9798026
Add lineage creation in subset by bbox
Athelena Jul 24, 2020
af15139
Add lineage item for every subset command
Athelena Jul 24, 2020
169c471
Fix issue with integer coords in remove_duplicate_vertices
Athelena Jul 27, 2020
0658325
Merge branch 'develop' into feature/cjio-metadata
Athelena Jul 27, 2020
79faa17
Fix issue with calculate_bbox calculation
Athelena Jul 27, 2020
d0366c7
Add lineage in extract_lod command
Athelena Jul 27, 2020
0216335
Add date to lineage item creation
Athelena Jul 27, 2020
46c2cbf
Update fileIdentifier in metadata when saving a CityJSON file
Athelena Jul 27, 2020
22d0854
Force metadata overwrite in all commands with lineage
Athelena Jul 27, 2020
6bd1286
Add metadata and lineage creation in partition command
Athelena Jul 27, 2020
51b020b
Add lineage and metadata creation in merge command
Athelena Jul 27, 2020
19dffec
Cleanup code regarding lineage/metadata creation in commands
Athelena Jul 27, 2020
93d08c7
Make subset by ids to inherit metadata (fixes #55)
Athelena Jul 27, 2020
e984e5a
Make all subset commands recompute the citymodelIdentifier
Athelena Jul 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,6 @@ ENV/

# KDE
.directory

# VS Code
.vscode
201 changes: 178 additions & 23 deletions cjio/cityjson.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from io import StringIO
from sys import platform
from click import progressbar
from datetime import datetime, date

MODULE_NUMPY_AVAILABLE = True
MODULE_PYPROJ_AVAILABLE = True
Expand All @@ -39,6 +40,7 @@
from cjio import validation, subset, geom_help, convert, models
from cjio.errors import InvalidOperation
from cjio.utils import print_cmd_warning
from cjio.metadata import generate_metadata


CITYJSON_VERSIONS_SUPPORTED = ['0.6', '0.8', '0.9', '1.0']
Expand Down Expand Up @@ -210,17 +212,20 @@ def __init__(self, file=None, j=None, ignore_duplicate_keys=False):
if file is not None:
self.read(file, ignore_duplicate_keys)
self.path = os.path.abspath(file.name)
self.reference_date = datetime.fromtimestamp(os.path.getmtime(file.name)).strftime('%Y-%m-%d')
self.cityobjects = {}
elif j is not None:
self.j = j
self.cityobjects = {}
self.reference_date = datetime.now()
else: #-- create an empty one
self.j = {}
self.j["type"] = "CityJSON"
self.j["version"] = CITYJSON_VERSIONS_SUPPORTED[-1]
self.j["CityObjects"] = {}
self.j["vertices"] = []
self.cityobjects = {}
self.reference_date = datetime.now()


def __repr__(self):
Expand Down Expand Up @@ -604,19 +609,17 @@ def get_bbox(self):


def calculate_bbox(self):
bbox = [9e9, 9e9, 9e9, -9e9, -9e9, -9e9]
for v in self.j["vertices"]:
for i in range(3):
if v[i] < bbox[i]:
bbox[i] = v[i]
for i in range(3):
if v[i] > bbox[i+3]:
bbox[i+3] = v[i]
"""
Calculate the bbox of the CityJSON.
"""
if len(self.j["vertices"]) == 0:
return [0, 0, 0, 0, 0, 0]
x, y, z = zip(*self.j["vertices"])
bbox = [min(x), min(y), min(z), max(x), max(y), max(z)]
if "transform" in self.j:
for i in range(3):
bbox[i] = (bbox[i] * self.j["transform"]["scale"][i]) + self.j["transform"]["translate"][i]
for i in range(3):
bbox[i+3] = (bbox[i+3] * self.j["transform"]["scale"][i]) + self.j["transform"]["translate"][i]
s = self.j["transform"]["scale"]
t = self.j["transform"]["translate"]
bbox = [a * b + c for a, b, c in zip(bbox, (s + s), (t + t))]
return bbox


Expand Down Expand Up @@ -723,6 +726,44 @@ def recusionvisit(a, vs):
return None


def get_identifier(self):
"""
Returns the identifier of this file.

If there is one in metadata, it will be returned. Otherwise, the filename will.
"""
if "metadata" in self.j:
if "citymodelIdentifier" in self.j["metadata"]:
cm_id = self.j["metadata"]["citymodelIdentifier"]

if cm_id:
template = "{cm_id} ({file_id})"
else:
template = "{file_id}"

if "metadata" in self.j:
if "fileIdentifier" in self.j["metadata"]:
return template.format(cm_id=cm_id, file_id=self.j["metadata"]["fileIdentifier"])

if self.path:
return os.path.basename(self.path)

return "unknown"


def get_title(self):
"""
Returns the description of this file from metadata.

If there is none, the identifier will be returned, instead.
"""

if "metadata" in self.j:
if "datasetTitle" in self.j["metadata"]:
return self.j["metadata"]["datasetTitle"]

return self.get_identifier()

def get_subset_bbox(self, bbox, exclude=False):
# print ('get_subset_bbox')
#-- new sliced CityJSON object
Expand Down Expand Up @@ -764,9 +805,14 @@ def get_subset_bbox(self, bbox, exclude=False):
cm2.j["appearance"] = {}
subset.process_appearance(self.j, cm2.j)
#-- metadata
if ("metadata" in self.j):
cm2.j["metadata"] = self.j["metadata"]
cm2.update_bbox()
try:
cm2.j["metadata"] = copy.deepcopy(self.j["metadata"])
cm2.update_metadata(overwrite=True, new_uuid=True)
fids = [fid for fid in cm2.j["CityObjects"]]
cm2.add_lineage_item("Subset of {} by bounding box {}".format(self.get_identifier(), bbox), features=fids)
except:
pass

return cm2


Expand Down Expand Up @@ -813,7 +859,12 @@ def get_subset_random(self, number=1, exclude=False):
sallkeys = set(self.j["CityObjects"].keys())
re = sallkeys ^ re
re = list(re)
return self.get_subset_ids(re)
cm = self.get_subset_ids(re)
try:
cm.j["metadata"]["lineage"][-1]["processStep"]["description"] = "Random subset of {}".format(self.get_identifier())
except:
pass
return cm


def get_subset_ids(self, lsIDs, exclude=False):
Expand Down Expand Up @@ -841,9 +892,13 @@ def get_subset_ids(self, lsIDs, exclude=False):
cm2.j["appearance"] = {}
subset.process_appearance(self.j, cm2.j)
#-- metadata
if ("metadata" in self.j):
cm2.j["metadata"] = self.j["metadata"]
cm2.update_bbox()
try:
cm2.j["metadata"] = copy.deepcopy(self.j["metadata"])
cm2.update_metadata(overwrite=True, new_uuid=True)
fids = [fid for fid in cm2.j["CityObjects"]]
cm2.add_lineage_item("Subset of {} based on user specified IDs".format(self.get_identifier()), features=fids)
except:
pass
return cm2


Expand Down Expand Up @@ -883,9 +938,12 @@ def get_subset_cotype(self, cotype, exclude=False):
cm2.j["appearance"] = {}
subset.process_appearance(self.j, cm2.j)
#-- metadata
if ("metadata" in self.j):
cm2.j["metadata"] = self.j["metadata"]
cm2.update_bbox()
try:
cm2.j["metadata"] = copy.deepcopy(self.j["metadata"])
cm2.update_metadata(overwrite=True, new_uuid=True)
cm2.add_lineage_item("Subset of {} by object type {}".format(self.get_identifier(), cotype))
except:
pass
return cm2


Expand Down Expand Up @@ -1133,6 +1191,9 @@ def update_face(a, oldnewids):


def remove_duplicate_vertices(self, precision=3):
if "transform" in self.j:
precision = 0

def update_geom_indices(a, newids):
for i, each in enumerate(a):
if isinstance(each, list):
Expand Down Expand Up @@ -1243,6 +1304,12 @@ def update_texture_indices(a, toffset, voffset):
a[i] = each + voffset
#-- decompress current CM
self.decompress()

#-- metadata
try:
self.update_metadata(overwrite=True)
except:
pass
for cm in lsCMs:
#-- decompress
cm.decompress()
Expand Down Expand Up @@ -1340,6 +1407,16 @@ def update_texture_indices(a, toffset, voffset):
if 'texture' in g:
for m in g['texture']:
update_texture_indices(g['texture'][m]['values'], toffset, voffset)
#-- metadata
try:
fids = [fid for fid in cm.j["CityObjects"]]
src = {
"description": cm.get_title(),
"sourceReferenceSystem": "urn:ogc:def:crs:EPSG::{}".format(cm.get_epsg()) if cm.get_epsg() else None
}
self.add_lineage_item("Merge {} into {}".format(cm.get_identifier(), self.get_identifier()), features=fids, source=[src])
except:
pass
# self.remove_duplicate_vertices()
# self.remove_orphan_vertices()
return True
Expand Down Expand Up @@ -1553,7 +1630,14 @@ def extract_lod(self, thelod):
for each in re:
self.j['CityObjects'][co]['geometry'].remove(each)
self.remove_duplicate_vertices()
self.remove_orphan_vertices()
self.remove_orphan_vertices()
#-- metadata
try:
self.update_metadata(overwrite=True)
fids = [fid for fid in self.j["CityObjects"]]
self.add_lineage_item("Extract LoD{} from {}".format(thelod, self.get_identifier()), features=fids)
except:
pass


def translate(self, values, minimum_xyz):
Expand All @@ -1576,3 +1660,74 @@ def translate(self, values, minimum_xyz):
self.set_epsg(None)
self.update_bbox()
return bbox

def has_metadata(self):
"""
Returns whether metadata exist in this CityJSON file or not
"""
return "metadata" in self.j

def get_metadata(self):
"""
Returns the "metadata" property of this CityJSON file

Raises a KeyError exception if metadata is missing
"""
if not "metadata" in self.j:
raise KeyError("Metadata is missing")
return self.j["metadata"]

def compute_metadata(self, overwrite=False, new_uuid=False):
"""
Returns the metadata of this CityJSON file
"""
return generate_metadata(self.j, self.path, self.reference_date, overwrite, new_uuid)

def update_metadata(self, overwrite=False, new_uuid=False):
"""
Computes and updates the "metadata" property of this CityJSON file
"""
self.update_bbox()

metadata, errors = self.compute_metadata(overwrite, new_uuid)

self.j["metadata"] = metadata

return (True, errors)

def add_lineage_item(self, description: str, features: list = None, source: list = None, processor: dict = None):
"""
Adds a lineage item in metadata.

description --- A string with the description of the process
features (optional) --- A list of object ids that are affected by it
source (optional) --- A list of sources. Every source is a dict with
the respective info (description, sourceReferenceSystem etc.)
processor (optional) --- A dict with contact information for the
person that conducted the processing
"""

new_item = {
"processStep": {
"description": description,
"stepDateTime": str(date.today())
}
}

if isinstance(features, list):
new_item["featureIDs"] = features

if isinstance(source, list):
new_item["source"] = source

if isinstance(processor, dict):
new_item["processStep"]["processor"] = processor

if not self.has_metadata():
self.j["metadata"] = {}

if not "lineage" in self.j["metadata"]:
self.j["metadata"]["lineage"] = []

self.j["metadata"]["lineage"].append(new_item)

Loading