Last active
December 11, 2023 10:03
-
-
Save johnhw/74bb07ce9c6a03cd8c8c74aeb87a7311 to your computer and use it in GitHub Desktop.
Python script to convert XTide TCD database files to JSON. Requires libtcd to be installed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import ctypes | |
| import json | |
| import os | |
| # Load the libtcd shared library using ctypes | |
| # set the path here to the location of the libtcd.so file | |
| lib_path = "/usr/lib" | |
| libtcd = ctypes.CDLL(f"{lib_path}/libtcd.so") | |
| # Path where the xtide TCD files are located | |
| # (all tcd files in this directory will be processed) | |
| base_path = "/usr/share/xtide/" | |
| ## Define the C structures in Python | |
| ONELINER_LENGTH = 90 | |
| MONOLOGUE_LENGTH = 10000 | |
| MAX_CONSTITUENTS = 255 | |
| class TIDE_STATION_HEADER(ctypes.Structure): | |
| _fields_ = [ | |
| ("record_number", ctypes.c_int32), | |
| ("record_size", ctypes.c_uint32), | |
| ("record_type", ctypes.c_ubyte), | |
| ("latitude", ctypes.c_double), | |
| ("longitude", ctypes.c_double), | |
| ("reference_station", ctypes.c_int32), | |
| ("tzfile", ctypes.c_int16), | |
| ("name", ctypes.c_char * ONELINER_LENGTH) | |
| ] | |
| # Define the DB_HEADER_PUBLIC structure in Python | |
| class DB_HEADER_PUBLIC(ctypes.Structure): | |
| _fields_ = [ | |
| ("version", ctypes.c_char * ONELINER_LENGTH), | |
| ("major_rev", ctypes.c_uint32), | |
| ("minor_rev", ctypes.c_uint32), | |
| ("last_modified", ctypes.c_char * ONELINER_LENGTH), | |
| ("number_of_records", ctypes.c_uint32), | |
| ("start_year", ctypes.c_int32), | |
| ("number_of_years", ctypes.c_uint32), | |
| ("constituents", ctypes.c_uint32), | |
| ("level_unit_types", ctypes.c_uint32), | |
| ("dir_unit_types", ctypes.c_uint32), | |
| ("restriction_types", ctypes.c_uint32), | |
| ("datum_types", ctypes.c_uint32), | |
| ("countries", ctypes.c_uint32), | |
| ("tzfiles", ctypes.c_uint32), | |
| ("legaleses", ctypes.c_uint32), | |
| ("pedigree_types", ctypes.c_uint32), # Included for reading V1 files | |
| ] | |
| # Define the TIDE_RECORD structure in Python | |
| class TIDE_RECORD(ctypes.Structure): | |
| _fields_ = [ | |
| ("header", TIDE_STATION_HEADER), | |
| ("country", ctypes.c_int16), | |
| ("source", ctypes.c_char * ONELINER_LENGTH), | |
| ("restriction", ctypes.c_ubyte), | |
| ("comments", ctypes.c_char * MONOLOGUE_LENGTH), | |
| ("notes", ctypes.c_char * MONOLOGUE_LENGTH), | |
| ("legalese", ctypes.c_ubyte), | |
| ("station_id_context", ctypes.c_char * ONELINER_LENGTH), | |
| ("station_id", ctypes.c_char * ONELINER_LENGTH), | |
| ("date_imported", ctypes.c_uint32), | |
| ("xfields", ctypes.c_char * MONOLOGUE_LENGTH), | |
| ("direction_units", ctypes.c_ubyte), | |
| ("min_direction", ctypes.c_int32), | |
| ("max_direction", ctypes.c_int32), | |
| ("level_units", ctypes.c_ubyte), | |
| ("datum_offset", ctypes.c_float), | |
| ("datum", ctypes.c_int16), | |
| ("zone_offset", ctypes.c_int32), | |
| ("expiration_date", ctypes.c_uint32), | |
| ("months_on_station", ctypes.c_uint16), | |
| ("last_date_on_station", ctypes.c_uint32), | |
| ("confidence", ctypes.c_ubyte), | |
| ("amplitude", ctypes.c_float * MAX_CONSTITUENTS), | |
| ("epoch", ctypes.c_float * MAX_CONSTITUENTS), | |
| ("min_time_add", ctypes.c_int32), | |
| ("min_level_add", ctypes.c_float), | |
| ("min_level_multiply", ctypes.c_float), | |
| ("max_time_add", ctypes.c_int32), | |
| ("max_level_add", ctypes.c_float), | |
| ("max_level_multiply", ctypes.c_float), | |
| ("flood_begins", ctypes.c_int32), | |
| ("ebb_begins", ctypes.c_int32), | |
| ] | |
| ## Define the C function prototypes in Python | |
| libtcd.dump_tide_record.argtypes = [ctypes.POINTER(TIDE_RECORD)] | |
| libtcd.dump_tide_record.restype = None | |
| libtcd.get_country.argtypes = [ctypes.c_int32] | |
| libtcd.get_country.restype = ctypes.c_char_p | |
| libtcd.get_tzfile.argtypes = [ctypes.c_int32] | |
| libtcd.get_tzfile.restype = ctypes.c_char_p | |
| libtcd.get_level_units.argtypes = [ctypes.c_int32] | |
| libtcd.get_level_units.restype = ctypes.c_char_p | |
| libtcd.get_dir_units.argtypes = [ctypes.c_int32] | |
| libtcd.get_dir_units.restype = ctypes.c_char_p | |
| libtcd.get_restriction.argtypes = [ctypes.c_int32] | |
| libtcd.get_restriction.restype = ctypes.c_char_p | |
| libtcd.get_datum.argtypes = [ctypes.c_int32] | |
| libtcd.get_datum.restype = ctypes.c_char_p | |
| libtcd.get_legalese.argtypes = [ctypes.c_int32] | |
| libtcd.get_legalese.restype = ctypes.c_char_p | |
| libtcd.get_constituent.argtypes = [ctypes.c_int32] | |
| libtcd.get_constituent.restype = ctypes.c_char_p | |
| libtcd.get_station.argtypes = [ctypes.c_int32] | |
| libtcd.get_station.restype = ctypes.c_char_p | |
| libtcd.get_speed.argtypes = [ctypes.c_int32] | |
| libtcd.get_speed.restype = ctypes.c_double | |
| libtcd.get_equilibrium.argtypes = [ctypes.c_int32, ctypes.c_int32] | |
| libtcd.get_equilibrium.restype = ctypes.c_float | |
| libtcd.get_node_factor.argtypes = [ctypes.c_int32, ctypes.c_int32] | |
| libtcd.get_node_factor.restype = ctypes.c_float | |
| libtcd.get_equilibriums.argtypes = [ctypes.c_int32] | |
| libtcd.get_equilibriums.restype = ctypes.POINTER(ctypes.c_float) | |
| libtcd.get_node_factors.argtypes = [ctypes.c_int32] | |
| libtcd.get_node_factors.restype = ctypes.POINTER(ctypes.c_float) | |
| libtcd.get_time.argtypes = [ctypes.c_char_p] | |
| libtcd.get_time.restype = ctypes.c_int32 | |
| libtcd.ret_time.argtypes = [ctypes.c_int32] | |
| libtcd.ret_time.restype = ctypes.c_char_p | |
| libtcd.ret_time_neat.argtypes = [ctypes.c_int32] | |
| libtcd.ret_time_neat.restype = ctypes.c_char_p | |
| libtcd.ret_date.argtypes = [ctypes.c_uint32] | |
| libtcd.ret_date.restype = ctypes.c_char_p | |
| libtcd.search_station.argtypes = [ctypes.c_char_p] | |
| libtcd.search_station.restype = ctypes.c_int32 | |
| libtcd.find_station.argtypes = [ctypes.c_char_p] | |
| libtcd.find_station.restype = ctypes.c_int32 | |
| libtcd.find_tzfile.argtypes = [ctypes.c_char_p] | |
| libtcd.find_tzfile.restype = ctypes.c_int32 | |
| libtcd.find_country.argtypes = [ctypes.c_char_p] | |
| libtcd.find_country.restype = ctypes.c_int32 | |
| libtcd.find_level_units.argtypes = [ctypes.c_char_p] | |
| libtcd.find_level_units.restype = ctypes.c_int32 | |
| libtcd.find_dir_units.argtypes = [ctypes.c_char_p] | |
| libtcd.find_dir_units.restype = ctypes.c_int32 | |
| libtcd.find_restriction.argtypes = [ctypes.c_char_p] | |
| libtcd.find_restriction.restype = ctypes.c_int32 | |
| libtcd.find_datum.argtypes = [ctypes.c_char_p] | |
| libtcd.find_datum.restype = ctypes.c_int32 | |
| libtcd.find_constituent.argtypes = [ctypes.c_char_p] | |
| libtcd.find_constituent.restype = ctypes.c_int32 | |
| libtcd.find_legalese.argtypes = [ctypes.c_char_p] | |
| libtcd.find_legalese.restype = ctypes.c_int32 | |
| libtcd.add_restriction.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_restriction.restype = ctypes.c_int32 | |
| libtcd.add_tzfile.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_tzfile.restype = ctypes.c_int32 | |
| libtcd.add_country.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_country.restype = ctypes.c_int32 | |
| libtcd.add_datum.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_datum.restype = ctypes.c_int32 | |
| libtcd.add_legalese.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_legalese.restype = ctypes.c_int32 | |
| libtcd.find_or_add_restriction.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.find_or_add_restriction.restype = ctypes.c_int32 | |
| libtcd.find_or_add_tzfile.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.find_or_add_tzfile.restype = ctypes.c_int32 | |
| libtcd.find_or_add_country.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.find_or_add_country.restype = ctypes.c_int32 | |
| libtcd.find_or_add_datum.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.find_or_add_datum.restype = ctypes.c_int32 | |
| libtcd.find_or_add_legalese.argtypes = [ctypes.c_char_p, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.find_or_add_legalese.restype = ctypes.c_int32 | |
| libtcd.set_speed.argtypes = [ctypes.c_int32, ctypes.c_double] | |
| libtcd.set_speed.restype = None | |
| libtcd.set_equilibrium.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_float] | |
| libtcd.set_equilibrium.restype = None | |
| libtcd.set_node_factor.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.c_float] | |
| libtcd.set_node_factor.restype = None | |
| libtcd.open_tide_db.argtypes = [ctypes.c_char_p] | |
| libtcd.open_tide_db.restype = ctypes.c_bool | |
| libtcd.close_tide_db.argtypes = [] | |
| libtcd.close_tide_db.restype = None | |
| libtcd.create_tide_db.argtypes = [ctypes.c_char_p, ctypes.c_uint32, ctypes.POINTER(ctypes.c_char_p), | |
| ctypes.POINTER(ctypes.c_double), ctypes.c_int32, ctypes.c_uint32, | |
| ctypes.POINTER(ctypes.POINTER(ctypes.c_float)), | |
| ctypes.POINTER(ctypes.POINTER(ctypes.c_float))] | |
| libtcd.create_tide_db.restype = ctypes.c_bool | |
| libtcd.get_tide_db_header.argtypes = [] | |
| libtcd.get_tide_db_header.restype = DB_HEADER_PUBLIC | |
| libtcd.get_partial_tide_record.argtypes = [ctypes.c_int32, ctypes.POINTER(TIDE_STATION_HEADER)] | |
| libtcd.get_partial_tide_record.restype = ctypes.c_bool | |
| libtcd.get_next_partial_tide_record.argtypes = [ctypes.POINTER(TIDE_STATION_HEADER)] | |
| libtcd.get_next_partial_tide_record.restype = ctypes.c_int32 | |
| libtcd.get_nearest_partial_tide_record.argtypes = [ctypes.c_double, ctypes.c_double, ctypes.POINTER(TIDE_STATION_HEADER)] | |
| libtcd.get_nearest_partial_tide_record.restype = ctypes.c_int32 | |
| libtcd.read_tide_record.argtypes = [ctypes.c_int32, ctypes.POINTER(TIDE_RECORD)] | |
| libtcd.read_tide_record.restype = ctypes.c_int32 | |
| libtcd.read_next_tide_record.argtypes = [ctypes.POINTER(TIDE_RECORD)] | |
| libtcd.read_next_tide_record.restype = ctypes.c_int32 | |
| libtcd.add_tide_record.argtypes = [ctypes.POINTER(TIDE_RECORD), ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.add_tide_record.restype = ctypes.c_bool | |
| libtcd.update_tide_record.argtypes = [ctypes.c_int32, ctypes.POINTER(TIDE_RECORD), ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.update_tide_record.restype = ctypes.c_bool | |
| libtcd.delete_tide_record.argtypes = [ctypes.c_int32, ctypes.POINTER(DB_HEADER_PUBLIC)] | |
| libtcd.delete_tide_record.restype = ctypes.c_bool | |
| libtcd.infer_constituents.argtypes = [ctypes.POINTER(TIDE_RECORD)] | |
| libtcd.infer_constituents.restype = ctypes.c_bool | |
| def decode(by): | |
| try: | |
| return by.decode() | |
| except: | |
| return by.decode('latin-1') | |
| def open_and_generate_json(database_file, output_file): | |
| if not libtcd.open_tide_db(database_file.encode()): | |
| print(f"Failed to open the database: {database_file}") | |
| return | |
| try: | |
| # Get the database header | |
| db_header = libtcd.get_tide_db_header() | |
| assert db_header is not None | |
| except: | |
| print("Failed to get the database header") | |
| return | |
| data = { | |
| "database_header": { | |
| "version": decode(db_header.version), | |
| "major_revision": db_header.major_rev, | |
| "minor_revision": db_header.minor_rev, | |
| "last_modified": decode(db_header.last_modified), | |
| "number_of_records": db_header.number_of_records, | |
| "start_year": db_header.start_year, | |
| "number_of_years": db_header.number_of_years, | |
| "constituents": db_header.constituents, | |
| "level_unit_types": db_header.level_unit_types, | |
| "dir_unit_types": db_header.dir_unit_types, | |
| "restriction_types": db_header.restriction_types, | |
| "datum_types": db_header.datum_types, | |
| "countries": db_header.countries, | |
| "tzfiles": db_header.tzfiles, | |
| "legaleses": db_header.legaleses, | |
| "pedigree_types": db_header.pedigree_types | |
| }, | |
| "constituents": [], | |
| "tide_records": [] | |
| } | |
| for i in range(db_header.constituents): | |
| year_constituent_info = { | |
| "constituent_number": i + 1, | |
| "constituent_name": decode(libtcd.get_constituent(i)), | |
| "speed": libtcd.get_speed(i), | |
| "years":{} | |
| } | |
| # extract constituents for each year in valid range | |
| for year in range(data["database_header"]["number_of_years"]): | |
| year_constituent_info["years"][year + data["database_header"]["start_year"]] = { | |
| "equilibrium": libtcd.get_equilibrium(i, year), | |
| "node_factor": libtcd.get_node_factor(i, year) | |
| } | |
| data["constituents"].append(year_constituent_info) | |
| record_number = 0 | |
| tide_record = TIDE_RECORD() | |
| while libtcd.read_next_tide_record(ctypes.byref(tide_record)) != -1: | |
| record_info = { | |
| "record_number": tide_record.header.record_number, | |
| "record_size": tide_record.header.record_size, | |
| "record_type": tide_record.header.record_type, | |
| "latitude": tide_record.header.latitude, | |
| "longitude": tide_record.header.longitude, | |
| "reference_station": tide_record.header.reference_station, | |
| "tzfile": decode(libtcd.get_tzfile(tide_record.header.tzfile)), | |
| "name": decode(tide_record.header.name), | |
| "country": decode(libtcd.get_country(tide_record.country)), | |
| "source": decode(tide_record.source), | |
| "restriction": decode(libtcd.get_restriction(tide_record.restriction)), | |
| "comments": decode(tide_record.comments), | |
| "notes": decode(tide_record.notes), | |
| "legalese": decode(libtcd.get_legalese(tide_record.legalese)), | |
| "station_id_context": decode(tide_record.station_id_context), | |
| "station_id": decode(tide_record.station_id), | |
| "date_imported": decode(libtcd.ret_date(tide_record.date_imported)), | |
| "xfields": decode(tide_record.xfields), | |
| "direction_units": decode(libtcd.get_dir_units(tide_record.direction_units)), | |
| "min_direction": tide_record.min_direction, | |
| "max_direction": tide_record.max_direction, | |
| "level_units": decode(libtcd.get_level_units(tide_record.level_units)) | |
| } | |
| if tide_record.header.record_type == 1: | |
| record_info.update({ | |
| "datum_offset": tide_record.datum_offset, | |
| "datum": decode(libtcd.get_datum(tide_record.datum)), | |
| "zone_offset": tide_record.zone_offset, | |
| "expiration_date": decode(libtcd.ret_date(tide_record.expiration_date)), | |
| "months_on_station": tide_record.months_on_station, | |
| "last_date_on_station": decode(libtcd.ret_date(tide_record.last_date_on_station)), | |
| "confidence": tide_record.confidence, | |
| "amplitude": [float(value) for value in tide_record.amplitude[:db_header.constituents]], | |
| "epoch": [float(value) for value in tide_record.epoch[:db_header.constituents]] | |
| }) | |
| elif tide_record.header.record_type == 2: | |
| record_info.update({ | |
| "min_time_add": tide_record.min_time_add, | |
| "min_level_add": tide_record.min_level_add, | |
| "min_level_multiply": tide_record.min_level_multiply, | |
| "max_time_add": tide_record.max_time_add, | |
| "max_level_add": tide_record.max_level_add, | |
| "max_level_multiply": tide_record.max_level_multiply, | |
| "flood_begins": decode(libtcd.ret_time(tide_record.flood_begins)), | |
| "ebb_begins": decode(libtcd.ret_time(tide_record.ebb_begins)) | |
| }) | |
| data["tide_records"].append(record_info) | |
| record_number += 1 | |
| # Serialize the dictionary to a JSON file | |
| with open(output_file, 'w') as json_file: | |
| json.dump(data, json_file, indent=2) | |
| if __name__=="__main__": | |
| # Example usage | |
| os.listdir(base_path) | |
| for file in os.listdir(base_path): | |
| if file.endswith(".tcd"): | |
| print(f"Converting {file} to {file}.json") | |
| open_and_generate_json(f"{base_path}/{file}", f"{file}.json") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment