# GeoTIFF.py -- Pamhyr # Copyright (C) 2024-2025 INRAE # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -*- coding: utf-8 -*- import os import struct import logging from functools import reduce from tools import trace, timer from Model.Tools.PamhyrDB import SQLSubModel from Model.Except import NotImplementedMethodeError from Model.Scenario import Scenario try: import rasterio import rasterio.control import rasterio.crs import rasterio.sample import rasterio.vrt import rasterio._features from rasterio.io import MemoryFile _rasterio_loaded = True except Exception as e: print(f"Module 'rasterio' is not available: {e}") _rasterio_loaded = False logger = logging.getLogger() class GeoTIFF(SQLSubModel): _sub_classes = [] def __init__(self, id: int = -1, enabled=True, name="", description="", path="", coordinates=None, status=None, owner_scenario=-1): super(GeoTIFF, self).__init__( id=id, status=status, owner_scenario=owner_scenario ) self._enabled = enabled self._name = f"GeoTIFF #{self._pamhyr_id}" if name == "" else name self._description = description self._file_bytes = b'' if coordinates is None: self._coordinates = { "bottom": 0.0, "top": 0.0, "left": 0.0, "right": 0.0, } else: self._coordinates = coordinates self._file_name = "" if path != "": self.read_file(path) self._memfile = None def __getitem__(self, key): value = None if key == "enabled": value = self._enabled elif key == "name": value = self.name elif key == "description": value = self.description elif key == "file_name": value = self.file_name elif key == "coordinates": value = self.coordinates elif key == "coordinates_bottom": value = self.coordinates["bottom"] elif key == "coordinates_top": value = self.coordinates["top"] elif key == "coordinates_left": value = self.coordinates["left"] elif key == "coordinates_right": value = self.coordinates["right"] elif key == "memfile": value = self.memfile return value def __setitem__(self, key, value): if key == "enabled": self.enabled = value elif key == "name": self.name = value elif key == "description": self.description = value elif key == "file_name": if self._file_name != value: self.read_file(value) elif key == "coordinates": self.coordinates = value elif key == "coordinates_bottom" or key == "bottom": self.coordinates["bottom"] = value elif key == "coordinates_top" or key == "top": self.coordinates["top"] = value elif key == "coordinates_left" or key == "left": self.coordinates["left"] = value elif key == "coordinates_right" or key == "right": self.coordinates["right"] = value self.modified() @property def enabled(self): return self._enabled @enabled.setter def enabled(self, enabled): self._enabled = enabled self.modified() def is_enabled(self): return self._enabled @property def name(self): return self._name @name.setter def name(self, name): self._name = name self.modified() @property def description(self): return self._description @description.setter def description(self, description): self._description = description self.modified() @property def file_name(self): return self._file_name @file_name.setter def file_name(self, file_name): self._file_name = file_name self.modified() @property def coordinates(self): return self._coordinates @coordinates.setter def coordinates(self, coordinates): self._coordinates = coordinates self.modified() @property def coord_bottom(self): if self._coordinates is None: return 0.0 return self._coordinates["bottom"] @property def coord_top(self): if self._coordinates is None: return 0.0 return self._coordinates["top"] @property def coord_left(self): if self._coordinates is None: return 0.0 return self._coordinates["left"] @property def coord_right(self): if self._coordinates is None: return 0.0 return self._coordinates["right"] @property def memfile(self): if not _rasterio_loaded: return None if self._file_bytes == b'': return None if self._memfile is None: self._memfile = MemoryFile() self._memfile.write(self._file_bytes) return self._memfile def read_file(self, path): logger.debug(f"Read GeoTIFF file at : '{path}'") self._file_name = path self._file_bytes = b'' self._memfile = None nbytes = 0 with open(path, "rb") as f: while True: data = f.read(4096) if not data: break nbytes += len(data) self._file_bytes += data logger.debug(f"Read GeoTIFF: {nbytes} bytes readed") def write_file(self, path): with open(path, "w+b") as f: f.write(self._file_bytes) @classmethod def _db_create(cls, execute, ext=""): execute(f""" CREATE TABLE geotiff{ext} ( {cls.create_db_add_pamhyr_id()}, enabled BOOLEAN NOT NULL, deleted BOOLEAN NOT NULL DEFAULT FALSE, name TEXT NOT NULL, description TEXT NOT NULL, file_name TEXT NOT NULL, file_bytes BLOB NOT NULL, coordinates_bottom REAL NOT NULL, coordinates_top REAL NOT NULL, coordinates_left REAL NOT NULL, coordinates_right REAL NOT NULL, {Scenario.create_db_add_scenario()}, {Scenario.create_db_add_scenario_fk()}, PRIMARY KEY(pamhyr_id, scenario) ) """) return cls._create_submodel(execute) @classmethod def _db_update(cls, execute, version, data=None): major, minor, release = version.strip().split(".") if major == "0" and int(minor) < 2: cls._db_create(execute) if major == "0" and int(minor) == 2: if int(release) < 3: cls._db_create(execute) return True @classmethod def _db_load(cls, execute, data=None): new = [] scenario = data["scenario"] loaded = data['loaded_pid'] if scenario is None: return new table = execute( "SELECT pamhyr_id, enabled, deleted, " + "name, description, file_name, file_bytes, " + "coordinates_bottom, coordinates_top, " + "coordinates_left, coordinates_right, " + "scenario " + "FROM geotiff " + f"WHERE scenario = {scenario.id} " + f"AND pamhyr_id NOT IN ({', '.join(map(str, loaded))})" ) if table is not None: for row in table: it = iter(row) id = next(it) enabled = (next(it) == 1) deleted = (next(it) == 1) name = next(it) description = next(it) file_name = next(it) file_bytes = next(it) coordinates_bottom = next(it) coordinates_top = next(it) coordinates_left = next(it) coordinates_right = next(it) owner_scenario = next(it) f = cls( id=id, enabled=enabled, name=name, description=description, coordinates={ "bottom": coordinates_bottom, "top": coordinates_top, "left": coordinates_left, "right": coordinates_right, }, status=data['status'], owner_scenario=owner_scenario ) if deleted: f.set_as_deleted() f._file_bytes = file_bytes loaded.add(id) new.append(f) data["scenario"] = scenario.parent new += cls._db_load(execute, data) data["scenario"] = scenario return new def _db_save(self, execute, data=None): if not self.must_be_saved(): return True execute( "INSERT INTO geotiff (" + "pamhyr_id, enabled, deleted, " + "name, description, file_name, file_bytes, " + "coordinates_bottom, coordinates_top, " + "coordinates_left, coordinates_right, " + "scenario) " + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", self._pamhyr_id, self._enabled, self.is_deleted(), self.name, self.description, self.file_name, self._file_bytes, self.coordinates['bottom'], self.coordinates['top'], self.coordinates['left'], self.coordinates['right'], self._status.scenario_id, ) return True