From ccf87dc69baac284d25b37bb5a68c47d572b1631 Mon Sep 17 00:00:00 2001 From: Sam <78538841+spwoodcock@users.noreply.github.com> Date: Tue, 30 Jul 2024 16:58:53 +0000 Subject: [PATCH] fix: generating pmtiles with different file extensions, update XLSForm ordering (#278) * refactor: update logging for OdkDataset property creation * refactor: move digitization verification questions to end of xlsforms * fix: handle multiple file extension types for pmtile generation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * refactor: improve basemapper.dlthread logic readability * fix: invalid logic setting imagery source via suffix * refactor: replace dlthread with modular functions * refactor: do not pass 'xy' param between funcs, use provider config * fix: do not handle xy within pmtile generation (download standardises to zxy dirs) * refactor: use imagery providers config for customTMS setup * refactor: pass zoom levels and image format through to mbtile generation * refactor: typo in OpenAerialMap provider * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- osm_fieldwork/OdkCentralAsync.py | 7 +- osm_fieldwork/basemapper.py | 297 +++++++++++++++------------ osm_fieldwork/imagery.yaml | 3 +- osm_fieldwork/sqlite.py | 8 +- osm_fieldwork/xlsforms/buildings.xls | Bin 109056 -> 109568 bytes osm_fieldwork/xlsforms/health.xls | Bin 55808 -> 55808 bytes tests/test_entities.py | 1 + 7 files changed, 169 insertions(+), 147 deletions(-) diff --git a/osm_fieldwork/OdkCentralAsync.py b/osm_fieldwork/OdkCentralAsync.py index ac0dd6b5c..86353ceeb 100755 --- a/osm_fieldwork/OdkCentralAsync.py +++ b/osm_fieldwork/OdkCentralAsync.py @@ -315,7 +315,6 @@ async def createDataset( success = await gather(*properties_tasks, return_exceptions=True) # type: ignore if not success: log.warning(f"No properties were uploaded for ODK project ({projectId}) dataset name ({datasetName})") - log.info(f"Successfully created properties for dataset ({datasetName})") except aiohttp.ClientError as e: msg = f"Failed to create properties: {e}" log.error(msg) @@ -350,12 +349,12 @@ async def createDatasetProperty( } try: - log.debug(f"Creating property of dataset {datasetName}") + log.debug(f"Creating property ({field_name}) for dataset {datasetName}") async with self.session.post(url, ssl=self.verify, json=payload) as response: response_data = await response.json() if response.status not in (200, 201): - log.debug(f"Failed to create properties: {response.status}, message='{response_data}'") - log.debug(f"Successfully created properties for dataset {datasetName}") + log.warning(f"Failed to create properties: {response.status}, message='{response_data}'") + log.debug(f"Successfully created property ({field_name}) for dataset {datasetName}") return response_data except aiohttp.ClientError as e: msg = f"Failed to create properties: {e}" diff --git a/osm_fieldwork/basemapper.py b/osm_fieldwork/basemapper.py index e6c907581..e2466afd9 100755 --- a/osm_fieldwork/basemapper.py +++ b/osm_fieldwork/basemapper.py @@ -23,14 +23,13 @@ import concurrent.futures import logging import os -import queue import re import shutil import sys import threading from io import BytesIO from pathlib import Path -from typing import Tuple, Union +from typing import Optional, Tuple, Union import geojson import mercantile @@ -163,71 +162,93 @@ def make_bbox(self) -> BoundingBox: raise ValueError(msg) from None -def dlthread( - dest: str, - mirrors: list, - tiles: list, - xy: bool, -): +def format_url(site: dict, tile: tuple) -> Optional[str]: + """Format the URL for the given site and tile. + + Args: + site (dict): The site configuration with URL and source type. + tile (tuple): The tile coordinates (x, y, z). + + Returns: + str: The formatted URL or None if the source is unsupported. + """ + source_url = site["url"] + if site.get("xy"): + # z/x/y format download, move to z/y/x structure on disk + url_path = f"{tile[2]}/{tile[0]}/{tile[1]}" + else: + # z/y/x format download, keep the same on disk + url_path = f"{tile[2]}/{tile[1]}/{tile[0]}" + + match site["source"]: + # NOTE we use % syntax to replace the placeholder %s + case "esri": + return source_url % url_path + case "bing": + bingkey = mercantile.quadkey(tile) + return source_url % bingkey + case "topo": + # FIXME does this work an intended? + return source_url % f"{tile[2]}/{tile[1]}/{tile[0]}" + case "google": + return source_url % f"x={tile[0]}&s=&y={tile[1]}&z={tile[2]}" + case "oam": + return source_url % url_path + case "custom": + return source_url % url_path + case _: + log.error(f"Unsupported source: {site['source']}") + return None + + +def download_tile(dest: str, tile: tuple, mirrors: list[dict]) -> None: + """Download a single tile from the given list of mirrors. + + Args: + dest (str): The destination directory. + tile (tuple): The tile coordinates (x, y, z). + mirrors (list): The list of mirrors to get imagery. + """ + for site in mirrors: + download_url = format_url(site, tile) + if download_url: + filespec = f"{tile[2]}/{tile[1]}/{tile[0]}.{site['suffix']}" + outfile = Path(dest) / filespec + if not outfile.exists(): + try: + log.debug(f"Attempting URL download: {download_url}") + dl = SmartDL(download_url, dest=str(outfile), connect_default_logger=False) + dl.start() + return + except Exception as e: + log.error(e) + log.error(f"Couldn't download file for {filespec}") + else: + log.debug(f"{outfile} exists!") + else: + continue + + +def dlthread(dest: str, mirrors: list[dict], tiles: list[tuple]) -> None: """Thread to handle downloads for Queue. Args: - dest (str): The filespec of the tile cache - mirrors (list): The list of mirrors to get imagery - tiles (list): The list of tiles to download - xy (bool): Whether to swap the X & Y fields in the TMS URL + dest (str): The filespec of the tile cache. + mirrors (list): The list of mirrors to get imagery. + tiles (list): The list of tiles to download. """ if len(tiles) == 0: # epdb.st() return - # counter = -1 - - # start = datetime.now() - - # totaltime = 0.0 - log.info("Downloading %d tiles in thread %d to %s" % (len(tiles), threading.get_ident(), dest)) - for tile in tiles: - filespec = f"{tile[2]}/{tile[1]}/{tile[0]}" - for site in mirrors: - if site["source"] != "topo": - filespec += "." + site["suffix"] - url = site["url"] - if site["source"] == "bing": - bingkey = mercantile.quadkey(tile) - remote = url % bingkey - elif site["source"] == "google": - path = f"x={tile[0]}&s=&y={tile[1]}&z={tile[2]}" - remote = url % path - elif site["source"] == "custom": - if not xy: - # z/y/x format - path = f"{tile[2]}/{tile[1]}/{tile[0]}" - else: - # z/x/y format - path = f"{tile[2]}/{tile[0]}/{tile[1]}" - remote = url % path - else: - remote = url % filespec - print("Getting file from: %s" % remote) - # Create the subdirectories as pySmartDL doesn't do it for us - Path(dest).mkdir(parents=True, exist_ok=True) - dl = None - try: - if site["source"] == "topo": - filespec += "." + site["suffix"] - outfile = dest + "/" + filespec - if not Path(outfile).exists(): - dl = SmartDL(remote, dest=outfile, connect_default_logger=False) - dl.start() - else: - log.debug("%s exists!" % (outfile)) - except Exception as e: - log.error(e) - if dl: - log.error(f"Couldn't download {filespec}: {dl.get_errors()}") - else: - log.error(f"Couldn't download {filespec}") + # Create the subdirectories as pySmartDL doesn't do it for us + Path(dest).mkdir(parents=True, exist_ok=True) + + log.info(f"Downloading {len(tiles)} tiles in thread {threading.get_ident()} to {dest}") + + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: + futures = [executor.submit(download_tile, dest, tile, mirrors) for tile in tiles] + concurrent.futures.wait(futures) class BaseMapper(object): @@ -238,7 +259,6 @@ def __init__( boundary: Union[str, BytesIO], base: str, source: str, - xy: bool, ): """Create an tile basemap for ODK Collect. @@ -247,7 +267,6 @@ def __init__( The GeoJSON can contain multiple geometries. base (str): The base directory to cache map tile in source (str): The upstream data source for map tiles - xy (bool): Whether to swap the X & Y fields in the TMS URL Returns: (BaseMapper): An instance of this class @@ -259,7 +278,6 @@ def __init__( # sources for imagery self.source = source self.sources = dict() - self.xy = xy path = xlsforms_path.replace("xlsforms", "imagery.yaml") self.yaml = YamlFile(path) @@ -274,7 +292,7 @@ def __init__( src[k1] = v1 self.sources[k] = src - def customTMS(self, url: str, name: str = "custom", source: str = "custom", suffix: str = "jpg"): + def customTMS(self, url: str, is_oam: bool = False, is_xy: bool = False): """Add a custom TMS URL to the list of sources. The url must end in %s to be replaced with the tile xyz values. @@ -287,29 +305,38 @@ def customTMS(self, url: str, name: str = "custom", source: str = "custom", suff The method will replace {z}/{x}/{y}.jpg with %s Args: - name (str): The name to display url (str): The URL string - suffix (str): The suffix, png or jpg - source (str): The source value to use as an index + source (str): The provier source, for setting attribution + is_xy (bool): Swap the x and y for the provider --> 'zxy' """ # Remove any file extensions if present and update the 'suffix' parameter + # NOTE the file extension gets added again later for the download URL if url.endswith(".jpg"): - source = "jpg" suffix = "jpg" url = url[:-4] # Remove the last 4 characters (".jpg") elif url.endswith(".png"): - source = "png" suffix = "png" url = url[:-4] # Remove the last 4 characters (".png") + else: + # FIXME handle other formats for custom TMS + suffix = "jpg" # Replace "{z}/{x}/{y}" with "%s" url = re.sub(r"/{[xyz]+\}", "", url) url = url + r"/%s" - tms_params = {"name": name, "url": url, "suffix": suffix, "source": source} - log.debug(f"Setting custom TMS with params: {tms_params}") - self.sources["custom"] = tms_params - self.source = "custom" + if is_oam: + # Override dummy OAM URL + source = "oam" + self.sources[source]["url"] = url + else: + source = "custom" + tms_params = {"name": source, "url": url, "suffix": suffix, "source": source, "xy": is_xy} + log.debug(f"Setting custom TMS with params: {tms_params}") + self.sources[source] = tms_params + + # Select the source + self.source = source def getFormat(self): """Get the image format of the map tiles. @@ -319,47 +346,37 @@ def getFormat(self): """ return self.sources[self.source]["suffix"] - def getTiles( - self, - zoom: int = None, - ): - """Get a list of tiles for the specifed zoom level. + def getTiles(self, zoom: int) -> int: + """Get a list of tiles for the specified zoom level. Args: - zoom (int): The Zoom level of the desired map tiles + zoom (int): The Zoom level of the desired map tiles. Returns: - (int): The total number of map tiles downloaded + int: The total number of map tiles downloaded. """ info = get_cpu_info() cores = info["count"] self.tiles = list(mercantile.tiles(self.bbox[0], self.bbox[1], self.bbox[2], self.bbox[3], zoom)) total = len(self.tiles) - log.info("%d tiles for zoom level %d" % (len(self.tiles), zoom)) - chunk = round(len(self.tiles) / cores) - queue.Queue(maxsize=cores) - log.info("%d threads, %d tiles" % (cores, total)) + log.info(f"{total} tiles for zoom level {zoom}") mirrors = [self.sources[self.source]] + chunk_size = max(1, round(total / cores)) - if len(self.tiles) < chunk or chunk == 0: - dlthread(self.base, mirrors, self.tiles, self.xy) + if total < chunk_size or chunk_size == 0: + dlthread(self.base, mirrors, self.tiles) else: with concurrent.futures.ThreadPoolExecutor(max_workers=cores) as executor: - # results = [] - block = 0 - while block <= len(self.tiles): - executor.submit(dlthread, self.base, mirrors, self.tiles[block : block + chunk], self.xy) - # result = executor.submit(dlthread, self.base, mirrors, self.tiles[block : block + chunk], self.xy) - # results.append(result) - log.debug("Dispatching Block %d:%d" % (block, block + chunk)) - block += chunk - executor.shutdown() - # log.info("Had %r errors downloading %d tiles for data for %r" % (self.errors, len(tiles), Path(self.base).name)) - # for result in results: - # print(result.result()) - return len(self.tiles) + futures = [] + for i in range(0, total, chunk_size): + chunk = self.tiles[i : i + chunk_size] + futures.append(executor.submit(dlthread, self.base, mirrors, chunk)) + log.debug(f"Dispatching Block {i}:{i + chunk_size}") + concurrent.futures.wait(futures) + + return total def tileExists( self, @@ -382,15 +399,15 @@ def tileExists( return False -def tileid_from_xyz_dir_path(filepath: Union[Path, str], is_xy: bool = False) -> int: - """Helper function to get the tile id from a tile in xyz directory structure. +def tileid_from_zyx_dir_path(filepath: Union[Path, str]) -> int: + """Helper function to get the tile id from a tile in xyz (zyx) directory structure. TMS typically has structure z/y/x.png - If the --xy flag is used during creation, then z/x/y is used. + If the --xy flag was used previously, the TMS was downloaed into + directories of z/y/x structure from their z/x/y URL. Args: filepath (Union[Path, str]): The path to tile image within the xyz directory. - is_xy (bool): If the X/Y are swapped in the xyz URL. Returns: int: The globally defined tile id from the xyz definition. @@ -405,12 +422,8 @@ def tileid_from_xyz_dir_path(filepath: Union[Path, str], is_xy: bool = False) -> log.error(msg) raise ValueError(msg) from e - if is_xy: - y = final_tile - z, x = map(int, tile_image_path[:-1]) - else: - x = final_tile - z, y = map(int, tile_image_path[:-1]) + x = final_tile + z, y = map(int, tile_image_path[:-1]) return zxy_to_tileid(z, x, y) @@ -419,8 +432,9 @@ def tile_dir_to_pmtiles( outfile: str, tile_dir: str | Path, bbox: tuple, + image_format: str, + zoom_levels: list[int], attribution: str, - is_xy=False, ): """Write PMTiles archive from tiles in the specified directory. @@ -429,44 +443,48 @@ def tile_dir_to_pmtiles( tile_dir (str | Path): The directory containing the tile images. bbox (tuple): Bounding box in format (min_lon, min_lat, max_lon, max_lat). attribution (str): Attribution string to include in PMTile archive. - is_xy (bool): If the X/Y are swapped in the xyz URL. Returns: None """ tile_dir = Path(tile_dir) - # Get tile image format from the first file encountered + # Abort if no files are present first_file = next((file for file in tile_dir.rglob("*.*") if file.is_file()), None) - if not first_file: err = "No tile files found in the specified directory. Aborting PMTile creation." log.error(err) raise ValueError(err) - # FIXME passing as PMTileType[tile_format] does not work - # tile_format = first_file.suffix.upper() - - # Get zoom levels from dirs - zoom_levels = sorted([int(x.stem) for x in tile_dir.glob("*") if x.is_dir()]) + tile_format = image_format.upper() + # NOTE JPEG exception / flexible extension (.jpg, .jpeg) + if tile_format == "JPG": + tile_format = "JPEG" + log.debug(f"PMTile determind internal file format: {tile_format}") + possible_tile_formats = [f".{e.name.lower()}" for e in PMTileType] + possible_tile_formats.append(".jpg") + possible_tile_formats.remove(".unknown") with open(outfile, "wb") as pmtile_file: writer = PMTileWriter(pmtile_file) for tile_path in tile_dir.rglob("*"): - if tile_path.is_file() and tile_path.suffix in [".jpg", ".jpeg"]: - tile_id = tileid_from_xyz_dir_path(tile_path, is_xy) + if tile_path.is_file() and tile_path.suffix.lower() in possible_tile_formats: + tile_id = tileid_from_zyx_dir_path(tile_path) with open(tile_path, "rb") as tile: writer.write_tile(tile_id, tile.read()) min_lon, min_lat, max_lon, max_lat = bbox + log.debug( + f"Writing PMTiles file with min_zoom ({zoom_levels[0]}) " + f"max_zoom ({zoom_levels[-1]}) bbox ({bbox}) tile_compression None" + ) # Write PMTile metadata writer.finalize( header={ - # "tile_type": TileType[tile_format.lstrip(".")], - "tile_type": PMTileType.PNG, + "tile_type": PMTileType[tile_format], "tile_compression": PMTileCompression.NONE, "min_zoom": zoom_levels[0], "max_zoom": zoom_levels[-1], @@ -504,7 +522,7 @@ def create_basemap_file( (e.g., "12-17") or comma-separated levels (e.g., "12,13,14"). outdir (str, optional): Output directory name for tile cache. source (str, optional): Imagery source, one of - ["esri", "bing", "topo", "google", "oam"] (default is "esri"). + ["esri", "bing", "topo", "google", "oam", "custom"] (default is "esri"). append (bool, optional): Whether to append to an existing file Returns: @@ -517,7 +535,6 @@ def create_basemap_file( f"zooms={zooms} | " f"outdir={outdir} | " f"source={source} | " - f"xy={xy} | " f"tms={tms}" ) @@ -548,29 +565,36 @@ def create_basemap_file( else: base = Path(outdir).absolute() - source = "custom" if tms else source + # Source / TMS validation + if not source and not tms: + err = "You need to specify a source!" + log.error(err) + raise ValueError(err) + if source == "oam" and not tms: + err = "A TMS URL must be provided for OpenAerialMap!" + log.error(err) + raise ValueError(err) + # A custom TMS provider + if source != "oam" and tms: + source = "custom" + tiledir = base / f"{source}tiles" # Make tile download directory tiledir.mkdir(parents=True, exist_ok=True) # Convert to string for other methods tiledir = str(tiledir) - if not source and not tms: - err = "You need to specify a source!" - log.error(err) - raise ValueError(err) - - basemap = BaseMapper(boundary, tiledir, source, xy) + basemap = BaseMapper(boundary, tiledir, source) if tms: # Add TMS URL to sources for download - basemap.customTMS(tms) + basemap.customTMS(tms, True if source == "oam" else False, xy) # Args parsed, main code: tiles = list() - for level in zoom_levels: + for zoom_level in zoom_levels: # Download the tile directory - basemap.getTiles(level) + basemap.getTiles(zoom_level) tiles += basemap.tiles if not outfile: @@ -578,7 +602,8 @@ def create_basemap_file( return suffix = Path(outfile).suffix.lower() - log.debug(f"Basemap output format: {suffix}") + image_format = basemap.sources[source].get("suffix", "jpg") + log.debug(f"Basemap output format: {suffix} | Image format: {image_format}") if any(substring in suffix for substring in ["sqlite", "mbtiles"]): outf = DataFile(outfile, basemap.getFormat(), append) @@ -586,10 +611,10 @@ def create_basemap_file( outf.addBounds(basemap.bbox) outf.addZoomLevels(zoom_levels) # Create output database and specify image format, png, jpg, or tif - outf.writeTiles(tiles, tiledir) + outf.writeTiles(tiles, tiledir, image_format) elif suffix == ".pmtiles": - tile_dir_to_pmtiles(outfile, tiledir, basemap.bbox, source, xy) + tile_dir_to_pmtiles(outfile, tiledir, basemap.bbox, image_format, zoom_levels, source) else: msg = f"Format {suffix} not supported" diff --git a/osm_fieldwork/imagery.yaml b/osm_fieldwork/imagery.yaml index 149d1c688..25e101750 100644 --- a/osm_fieldwork/imagery.yaml +++ b/osm_fieldwork/imagery.yaml @@ -18,7 +18,8 @@ sources: - xy: False - oam: - - name: OpenArialMap + - name: OpenAerialMap + # Note this is an example URL! It will change per project - url: https://tiles.openaerialmap.org/63962d0d9f665400075759be/0/63962d0d9f665400075759bf/%s - suffix: png - xy: True diff --git a/osm_fieldwork/sqlite.py b/osm_fieldwork/sqlite.py index 977ab59a3..1a774141a 100755 --- a/osm_fieldwork/sqlite.py +++ b/osm_fieldwork/sqlite.py @@ -203,11 +203,7 @@ def createDB( self.db.commit() logging.info("Created database file %s" % dbname) - def writeTiles( - self, - tiles: list, - base: str = "./", - ): + def writeTiles(self, tiles: list, base: str = "./", image_format: str = "jpg"): """Write map tiles into the to the map tile cache. Args: @@ -215,7 +211,7 @@ def writeTiles( base (str): The default local to write tiles to disk """ for tile in tiles: - xyz = MapTile(tile=tile) + xyz = MapTile(tile=tile, suffix=image_format) xyz.readImage(base) # xyz.dump() self.writeTile(xyz) diff --git a/osm_fieldwork/xlsforms/buildings.xls b/osm_fieldwork/xlsforms/buildings.xls index b21b5420f85ee079fb81823442069c8aa8053249..6551bc312e224cefbca04703f7501754995b451a 100644 GIT binary patch delta 38750 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 J?gLMZhN_V(3A7Q=~kqBB+!Iiv>{-5fu?pQIRJ4 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 delta 11834 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 LRQDg=?-TfcJOCJY diff --git a/tests/test_entities.py b/tests/test_entities.py index a9cee7f07..887aa8dc3 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -85,6 +85,7 @@ async def test_create_invalid_dataset(odk_dataset): with pytest.raises(ValueError): await dataset.createDataset(odk_id, dataset_name, properties=[1, 2]) + async def test_bulk_create_entity_count(odk_dataset_cleanup): """Test bulk creation of Entities.""" odk_id, dataset_name, entity_uuid, dataset = odk_dataset_cleanup