···11-(** MLTessera - Main library interface
22-33- This module provides the high-level interface for downloading
44- tessera tiles within bounding boxes.
55-*)
66-77-open Eio
88-99-let src = Logs.Src.create "mltessera" ~doc:"MLTessera main"
1010-module Log = (val Logs.src_log src : Logs.LOG)
1111-1212-module Coordinates = Coordinates
1313-module Registry_manager = Registry_manager
1414-module Tile_downloader = Tile_downloader
1515-1616-type t = {
1717- sw : Switch.t;
1818- env : Eio_unix.Stdenv.base;
1919- registry_manager : Registry_manager.t;
2020- downloader : Tile_downloader.t;
2121-}
2222-2323-type data_type = [`Embeddings | `Landmasks]
2424-2525-(** Initialize MLTessera *)
2626-let create ~sw ~env ?base_url ?cache_dir () =
2727- Log.info (fun m -> m "Initializing MLTessera...");
2828-2929- (* Create registry manager *)
3030- let registry_manager = Registry_manager.create ~sw ~env in
3131-3232- (* Create tile downloader - base_url is deprecated, using default URLs *)
3333- let downloader = match base_url with
3434- | Some _url ->
3535- Log.warn (fun m -> m "base_url parameter is deprecated, using default URLs");
3636- Tile_downloader.create ~sw ~env ?cache_dir ()
3737- | None ->
3838- Tile_downloader.create ~sw ~env ?cache_dir ()
3939- in
4040-4141- { sw; env; registry_manager; downloader }
4242-4343-(** Download tiles within a bounding box *)
4444-let download_bbox t ~data_type ~year ~bbox ?(concurrency = 4) () =
4545- Log.info (fun m -> m "Downloading %s tiles for %s in %s"
4646- (match data_type with `Embeddings -> "embeddings" | `Landmasks -> "landmasks")
4747- year (Coordinates.format_bbox bbox));
4848-4949- (* Calculate number of tiles *)
5050- let num_tiles = Coordinates.count_tiles_in_bbox bbox in
5151- Log.info (fun m -> m "Bounding box contains up to %d tiles" num_tiles);
5252-5353- (* Download tiles *)
5454- let results = Tile_downloader.download_bbox t.downloader
5555- ~registry_manager:t.registry_manager
5656- ~data_type ~year bbox ~concurrency () in
5757-5858- (* Count successes *)
5959- let successes = List.filter (fun (_, r) ->
6060- match r with Ok _ -> true | Error _ -> false
6161- ) results in
6262-6363- Log.info (fun m -> m "Downloaded %d/%d tiles successfully"
6464- (List.length successes) (List.length results));
6565-6666- results
6767-6868-(** Download tiles for a geographic region *)
6969-let download_region t ~data_type ~year ~region ?(concurrency = 4) () =
7070- match region with
7171- | `Bbox bbox -> download_bbox t ~data_type ~year ~bbox ~concurrency ()
7272- | `Country _country ->
7373- (* TODO: Implement country bbox lookup *)
7474- failwith "Country regions not yet implemented"
7575-7676-(** Get available years *)
7777-let get_available_years t =
7878- Registry_manager.get_available_years t.registry_manager
7979-8080-(** Update the tessera-manifests repository *)
8181-let update_manifests t =
8282- Log.info (fun m -> m "Updating tessera-manifests repository...");
8383- Registry_manager.update t.registry_manager;
8484- Log.info (fun m -> m "Repository updated")
8585-8686-(** Get cache statistics *)
8787-let get_cache_stats t =
8888- let (size, count) = Tile_downloader.cache_stats t.downloader in
8989- Log.info (fun m -> m "Cache contains %d tiles using %Ld bytes" count size);
9090- (size, count)
9191-9292-(** Clear the tile cache *)
9393-let clear_cache t =
9494- Log.info (fun m -> m "Clearing tile cache...");
9595- Tile_downloader.clear_cache t.downloader
9696-9797-(** Export downloaded tiles to a directory *)
9898-let export_tiles _t ~dest_dir results =
9999- Log.info (fun m -> m "Exporting %d tiles to %s"
100100- (List.length results) dest_dir);
101101-102102- (* Ensure destination directory exists *)
103103- (try Unix.mkdir dest_dir 0o755 with Unix.Unix_error _ -> ());
104104-105105- (* Copy successful downloads *)
106106- let exported = ref 0 in
107107- List.iter (fun (entry, result) ->
108108- match result with
109109- | Ok src_path ->
110110- let filename = Toru.Registry.filename entry in
111111- let dest_path = Filename.concat dest_dir (Filename.basename filename) in
112112-113113- (* Copy file *)
114114- let src = Path.native_exn src_path in
115115- let cmd = Printf.sprintf "cp %s %s"
116116- (Filename.quote src) (Filename.quote dest_path) in
117117-118118- if Sys.command cmd = 0 then begin
119119- incr exported;
120120- Log.debug (fun m -> m "Exported: %s" dest_path)
121121- end else
122122- Log.warn (fun m -> m "Failed to export: %s" filename)
123123- | Error _ -> ()
124124- ) results;
125125-126126- Log.info (fun m -> m "Exported %d tiles" !exported);
127127- !exported
-208
stack/mltessera/lib/registry_manager.ml
···11-(** Registry manager for tessera-manifests
22-33- This module manages the tessera-manifests git repository and
44- provides access to block-based registry files.
55-*)
66-77-open Eio
88-99-let src = Logs.Src.create "mltessera.registry" ~doc:"MLTessera registry manager"
1010-module Log = (val Logs.src_log src : Logs.LOG)
1111-1212-type t = {
1313- sw : Switch.t;
1414- env : Eio_unix.Stdenv.base;
1515- manifests_dir : Fs.dir_ty Path.t;
1616- cache_dir : Fs.dir_ty Path.t;
1717- registries : (string, Toru.Registry.t) Hashtbl.t;
1818-}
1919-2020-let tessera_manifests_url = "https://github.com/ucam-eo/tessera-manifests.git"
2121-2222-(** Get XDG-compliant directory for tessera manifests *)
2323-let get_manifests_dir ~sw:_ ~env =
2424- (* Use XDG_CACHE_HOME if set, otherwise default to ~/.cache *)
2525- let cache_home = match Sys.getenv_opt "XDG_CACHE_HOME" with
2626- | Some dir -> dir
2727- | None ->
2828- match Sys.getenv_opt "HOME" with
2929- | Some home -> Filename.concat home ".cache"
3030- | None -> "/tmp"
3131- in
3232-3333- let mltessera_cache = Filename.concat cache_home "mltessera" in
3434- let manifests_path = Filename.concat mltessera_cache "tessera-manifests" in
3535-3636- (* Ensure directories exist *)
3737- (try Unix.mkdir cache_home 0o755 with Unix.Unix_error _ -> ());
3838- (try Unix.mkdir mltessera_cache 0o755 with Unix.Unix_error _ -> ());
3939-4040- (* Return as Path.t *)
4141- Path.(Eio.Stdenv.fs env / manifests_path)
4242-4343-(** Clone or update the tessera-manifests repository *)
4444-let ensure_manifests_repo ~sw:_ ~env manifests_dir =
4545- let manifests_path = Path.native_exn manifests_dir in
4646-4747- if Sys.file_exists (Filename.concat manifests_path ".git") then begin
4848- Log.info (fun m -> m "Updating tessera-manifests repository...");
4949-5050- (* Pull latest changes *)
5151- let proc_mgr = env#process_mgr in
5252- try
5353- Process.run ~cwd:manifests_dir proc_mgr
5454- ["git"; "pull"; "--quiet"];
5555- Log.info (fun m -> m "Repository updated successfully")
5656- with exn ->
5757- Log.warn (fun m -> m "Failed to update repository: %s" (Printexc.to_string exn))
5858- end else begin
5959- Log.info (fun m -> m "Cloning tessera-manifests repository to %s..." manifests_path);
6060-6161- (* Ensure parent directory exists *)
6262- let manifests_parent = Filename.dirname manifests_path in
6363- (try Unix.mkdir manifests_parent 0o755 with Unix.Unix_error _ -> ());
6464-6565- (* Clone the repository *)
6666- let proc_mgr = env#process_mgr in
6767- let parent_path = Path.(Eio.Stdenv.fs env / manifests_parent) in
6868- try
6969- Process.run ~cwd:parent_path proc_mgr
7070- ["git"; "clone"; "--depth"; "1"; tessera_manifests_url];
7171- Log.info (fun m -> m "Repository cloned successfully")
7272- with exn ->
7373- failwith (Printf.sprintf "Failed to clone repository: %s" (Printexc.to_string exn))
7474- end
7575-7676-(** Create a registry manager *)
7777-let create ~sw ~env =
7878- let manifests_dir = get_manifests_dir ~sw ~env in
7979- let cache_dir = Path.(manifests_dir / ".cache") in
8080-8181- (* Ensure manifests repo is available *)
8282- ensure_manifests_repo ~sw ~env manifests_dir;
8383-8484- (* Create cache directory *)
8585- (try Path.mkdir ~perm:0o755 cache_dir with
8686- | Eio.Io _ -> ()
8787- | _ -> ());
8888-8989- {
9090- sw;
9191- env;
9292- manifests_dir;
9393- cache_dir;
9494- registries = Hashtbl.create 32;
9595- }
9696-9797-(** Load a registry file from the manifests repository *)
9898-let load_registry t ~subdir ~filename =
9999- let cache_key = Printf.sprintf "%s/%s" subdir filename in
100100-101101- (* Check cache first *)
102102- match Hashtbl.find_opt t.registries cache_key with
103103- | Some registry -> registry
104104- | None ->
105105- let registry_path = Path.(t.manifests_dir / "registry" / subdir / filename) in
106106-107107- if not (Path.is_file registry_path) then
108108- failwith (Printf.sprintf "Registry file not found: %s" (Path.native_exn registry_path));
109109-110110- Log.info (fun m -> m "Loading registry: %s/%s" subdir filename);
111111-112112- (* Load the registry *)
113113- let registry = Toru.Registry.load registry_path in
114114-115115- (* Debug: Show number of entries loaded *)
116116- let entries = Toru.Registry.entries registry in
117117- Log.info (fun m -> m "Loaded %d entries from %s/%s" (List.length entries) subdir filename);
118118-119119- (* Debug: Show first few entries if any *)
120120- (match entries with
121121- | e1 :: e2 :: e3 :: _ ->
122122- Log.debug (fun m -> m " Sample entries:");
123123- Log.debug (fun m -> m " %s" (Toru.Registry.filename e1));
124124- Log.debug (fun m -> m " %s" (Toru.Registry.filename e2));
125125- Log.debug (fun m -> m " %s" (Toru.Registry.filename e3))
126126- | _ -> ());
127127-128128- (* Cache it *)
129129- Hashtbl.add t.registries cache_key registry;
130130- registry
131131-132132-(** Load embeddings registry for a specific block and year *)
133133-let load_embeddings_registry t ~year block =
134134- let filename = Coordinates.embeddings_registry_filename ~year block in
135135- load_registry t ~subdir:"embeddings" ~filename
136136-137137-(** Load landmasks registry for a specific block *)
138138-let load_landmasks_registry t block =
139139- let filename = Coordinates.landmasks_registry_filename block in
140140- load_registry t ~subdir:"landmasks" ~filename
141141-142142-(** Get all available years for embeddings *)
143143-let get_available_years t =
144144- let embeddings_dir = Path.(t.manifests_dir / "registry" / "embeddings") in
145145-146146- if not (Path.is_directory embeddings_dir) then []
147147- else
148148- let years = ref [] in
149149- let files = Path.read_dir embeddings_dir in
150150- List.iter (fun name ->
151151- (* Extract year from filename like "embeddings_2024_lon0_lat0.txt" *)
152152- if String.starts_with ~prefix:"embeddings_" name then
153153- match String.split_on_char '_' name with
154154- | _ :: year :: _ ->
155155- if not (List.mem year !years) then
156156- years := year :: !years
157157- | _ -> ()
158158- ) files;
159159- List.sort String.compare !years
160160-161161-(** Load all registries for blocks intersecting a bounding box *)
162162-let load_bbox_registries t ~data_type ~year bbox =
163163- let blocks = Coordinates.blocks_from_bbox bbox in
164164-165165- Log.info (fun m -> m "Loading %d registry blocks for bbox %s"
166166- (List.length blocks) (Coordinates.format_bbox bbox));
167167-168168- List.map (fun block ->
169169- let registry = match data_type with
170170- | `Embeddings -> load_embeddings_registry t ~year block
171171- | `Landmasks -> load_landmasks_registry t block
172172- in
173173- (block, registry)
174174- ) blocks
175175-176176-(** Find a tile entry across multiple registries *)
177177-let find_tile_entry registries filename =
178178- List.find_map (fun (_block, registry) ->
179179- Toru.Registry.find filename registry
180180- ) registries
181181-182182-(** Get all tile entries within a bounding box *)
183183-let get_bbox_tiles t ~data_type ~year bbox =
184184- (* Load relevant registries *)
185185- let registries = load_bbox_registries t ~data_type ~year bbox in
186186-187187- (* Get all tiles in bbox *)
188188- let tiles = Coordinates.tiles_from_bbox bbox in
189189-190190- (* Map tiles to registry entries *)
191191- List.filter_map (fun tile ->
192192- let filename = match data_type with
193193- | `Embeddings -> Coordinates.embeddings_tile_filename ~year tile
194194- | `Landmasks -> Coordinates.landmasks_tile_filename tile
195195- in
196196-197197- match find_tile_entry registries filename with
198198- | Some entry -> Some (tile, entry)
199199- | None ->
200200- Log.warn (fun m -> m "File not found in registry: %s" filename);
201201- None
202202- ) tiles
203203-204204-(** Update the manifests repository *)
205205-let update t =
206206- ensure_manifests_repo ~sw:t.sw ~env:t.env t.manifests_dir;
207207- (* Clear cache after update *)
208208- Hashtbl.clear t.registries
-163
stack/mltessera/lib/tile_downloader.ml
···11-(** Tile downloader using Toru
22-33- This module downloads tessera tiles from dl.geotessera.org using
44- the Toru library for HTTP downloading and caching.
55-*)
66-77-open Eio
88-99-let src = Logs.Src.create "mltessera.downloader" ~doc:"MLTessera tile downloader"
1010-module Log = (val Logs.src_log src : Logs.LOG)
1111-1212-type t = {
1313- toru : Toru.t;
1414- embeddings_base_url : string;
1515- landmasks_base_url : string;
1616-}
1717-1818-let default_embeddings_base_url = "https://dl.geotessera.org/v1/global_0.1_degree_representation/"
1919-let default_landmasks_base_url = "https://dl.geotessera.org/v1/global_0.1_degree_tiff_all/"
2020-2121-(** Create a tile downloader *)
2222-let create ~sw ~env ?(embeddings_base_url = default_embeddings_base_url)
2323- ?(landmasks_base_url = default_landmasks_base_url) ?(cache_dir = None) () =
2424- (* Get cache directory *)
2525- let cache_path = match cache_dir with
2626- | Some dir -> dir
2727- | None ->
2828- (* Use XDG_CACHE_HOME if set, otherwise default to ~/.cache *)
2929- let cache_home = match Sys.getenv_opt "XDG_CACHE_HOME" with
3030- | Some dir -> dir
3131- | None ->
3232- match Sys.getenv_opt "HOME" with
3333- | Some home -> Filename.concat home ".cache"
3434- | None -> "/tmp"
3535- in
3636- let mltessera_cache = Filename.concat cache_home "mltessera" in
3737- let tiles_cache = Filename.concat mltessera_cache "tiles" in
3838-3939- (* Ensure directories exist *)
4040- (try Unix.mkdir cache_home 0o755 with Unix.Unix_error _ -> ());
4141- (try Unix.mkdir mltessera_cache 0o755 with Unix.Unix_error _ -> ());
4242- (try Unix.mkdir tiles_cache 0o755 with Unix.Unix_error _ -> ());
4343-4444- tiles_cache
4545- in
4646-4747- (* Create Toru instance with embeddings base URL as default *)
4848- let toru = Toru.create ~sw ~env
4949- ~base_url:embeddings_base_url
5050- ~cache_path
5151- ()
5252- in
5353-5454- { toru; embeddings_base_url; landmasks_base_url }
5555-5656-(** Download a single tile *)
5757-let download_tile t ~data_type entry =
5858- let filename = Toru.Registry.filename entry in
5959-6060- Log.info (fun m -> m "Downloading tile: %s" filename);
6161-6262- (* Update Toru with the correct base URL for this data type *)
6363- let base_url = match data_type with
6464- | `Embeddings -> t.embeddings_base_url
6565- | `Landmasks -> t.landmasks_base_url
6666- in
6767- let toru_with_base = Toru.update_base_url t.toru base_url in
6868-6969- (* Add the entry to Toru's registry so it can be fetched *)
7070- let toru_with_entry = Toru.add_registry_entry toru_with_base entry in
7171-7272- match Toru.fetch toru_with_entry ~filename () with
7373- | Ok path ->
7474- Log.info (fun m -> m "Downloaded: %s -> %s" filename (Path.native_exn path));
7575- Ok path
7676- | Error msg ->
7777- Log.err (fun m -> m "Failed to download %s: %s" filename msg);
7878- Error msg
7979-8080-(** Download multiple tiles concurrently *)
8181-let download_tiles t ~data_type entries ?(concurrency = 4) () =
8282- let total = List.length entries in
8383- let completed = ref 0 in
8484- let failed = ref [] in
8585-8686- Log.info (fun m -> m "Downloading %d tiles with concurrency %d" total concurrency);
8787-8888- (* Create a pool for concurrent downloads *)
8989- let pool = Eio.Pool.create concurrency (fun () -> ()) in
9090-9191- (* Download each tile *)
9292- let results = List.map (fun entry ->
9393- Pool.use pool @@ fun () ->
9494- let result = download_tile t ~data_type entry in
9595-9696- (* Update progress *)
9797- incr completed;
9898- let pct = float_of_int !completed /. float_of_int total *. 100.0 in
9999- Log.info (fun m -> m "Progress: %d/%d (%.1f%%)" !completed total pct);
100100-101101- match result with
102102- | Ok path -> (entry, Ok path)
103103- | Error msg ->
104104- failed := Toru.Registry.filename entry :: !failed;
105105- (entry, Error msg)
106106- ) entries in
107107-108108- (* Report summary *)
109109- let successful = total - List.length !failed in
110110- Log.info (fun m -> m "Download complete: %d successful, %d failed"
111111- successful (List.length !failed));
112112-113113- if !failed <> [] then
114114- Log.warn (fun m -> m "Failed tiles: %s" (String.concat ", " !failed));
115115-116116- results
117117-118118-(** Download all tiles within a bounding box *)
119119-let download_bbox t ~registry_manager ~data_type ~year bbox ?(concurrency = 4) () =
120120- Log.info (fun m -> m "Downloading %s tiles for year %s in bbox %s"
121121- (match data_type with `Embeddings -> "embeddings" | `Landmasks -> "landmasks")
122122- year (Coordinates.format_bbox bbox));
123123-124124- (* Get tiles from registry *)
125125- let tile_entries = Registry_manager.get_bbox_tiles
126126- registry_manager ~data_type ~year bbox in
127127-128128- if tile_entries = [] then begin
129129- Log.warn (fun m -> m "No tiles found in bbox %s" (Coordinates.format_bbox bbox));
130130- []
131131- end else begin
132132- Log.info (fun m -> m "Found %d tiles to download" (List.length tile_entries));
133133-134134- (* Extract just the entries *)
135135- let entries = List.map snd tile_entries in
136136-137137- (* Download them *)
138138- download_tiles t ~data_type entries ~concurrency ()
139139- end
140140-141141-(** Get the local path for a tile if it exists in cache *)
142142-let get_cached_tile t entry =
143143- let filename = Toru.Registry.filename entry in
144144- let cache = Toru.cache t.toru in
145145- let path = Toru.Cache.file_path cache filename in
146146-147147- if Toru.Cache.exists cache filename then
148148- Some path
149149- else
150150- None
151151-152152-(** Clear the tile cache *)
153153-let clear_cache t =
154154- let cache = Toru.cache t.toru in
155155- Toru.Cache.clear cache;
156156- Log.info (fun m -> m "Tile cache cleared")
157157-158158-(** Get cache statistics *)
159159-let cache_stats t =
160160- let cache = Toru.cache t.toru in
161161- let size = Toru.Cache.size_bytes cache in
162162- let files = Toru.Cache.list_files cache in
163163- (size, List.length files)
-35
stack/mltessera/mltessera.opam
···11-# This file is generated by dune, edit dune-project instead
22-opam-version: "2.0"
33-synopsis: "OCaml library for downloading tessera tiles from geotessera.org"
44-description:
55- "MLTessera is an OCaml equivalent of the Python geotessera library. It manages tessera-manifests registry files and downloads geospatial tiles within bounding boxes using the Toru library for HTTP operations."
66-maintainer: ["maintainer@example.com"]
77-authors: ["MLTessera Authors"]
88-license: "MIT"
99-depends: [
1010- "ocaml"
1111- "dune" {>= "3.0"}
1212- "toru"
1313- "lonlat"
1414- "cmdliner" {>= "2.0.0"}
1515- "eio"
1616- "eio_main"
1717- "yojson"
1818- "logs"
1919- "fmt"
2020- "odoc" {with-doc}
2121-]
2222-build: [
2323- ["dune" "subst"] {dev}
2424- [
2525- "dune"
2626- "build"
2727- "-p"
2828- name
2929- "-j"
3030- jobs
3131- "@install"
3232- "@runtest" {with-test}
3333- "@doc" {with-doc}
3434- ]
3535-]
···11-open Mltessera
22-open Lonlat
33-44-let () =
55- (* Test tile generation for London coordinates *)
66- let coord = Coord.create ~lat:51.45 ~lon:(-0.15) in
77- let tile = Coordinates.tile_from_world coord in
88- let filename = Coordinates.embeddings_tile_filename ~year:"2024" tile in
99- Printf.printf "Generated filename: %s\n" filename;
1010- Printf.printf "Tile lon: %.2f, lat: %.2f\n" tile.tile_lon tile.tile_lat;
1111-1212- (* Test a few more tiles in the bbox *)
1313- let bbox = Bbox.create ~min_lat:51.4 ~min_lon:(-0.2) ~max_lat:51.6 ~max_lon:0.0 in
1414- let tiles = Coordinates.tiles_from_bbox bbox in
1515- Printf.printf "\nAll tiles in bbox:\n";
1616- List.iter (fun t ->
1717- let fname = Coordinates.embeddings_tile_filename ~year:"2024" t in
1818- Printf.printf " %.2f,%.2f -> %s\n" t.tile_lon t.tile_lat fname
1919- ) tiles
-11
stack/mltessera/test_tiles.ml
···11-open Mltessera
22-open Lonlat
33-44-let () =
55- let coord = Coord.create ~lat:51.45 ~lon:(-0.15) in
66- let tile = Coordinates.tile_from_world coord in
77- let filename = Coordinates.embeddings_tile_filename ~year:"2024" tile in
88- Printf.printf "Generated filename: %s\n" filename;
99-1010- (* Also show the tile coordinates *)
1111- Printf.printf "Tile lon: %.2f, lat: %.2f\n" tile.tile_lon tile.tile_lat