Skip to content

Commit af50819

Browse files
giswqsCopilot
andauthored
Add support for creating local TiTiler endpoint (#1256)
* Add support for creating local TiTiler endpoint * Remove jupyter-duckdb * Update pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update endpoint * Update mkdocs.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 2703e69 commit af50819

8 files changed

Lines changed: 273 additions & 2 deletions

File tree

docs/maplibre/local_titiler.ipynb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=maplibre/local_titiler.ipynb)\n",
8+
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/maplibre/local_titiler.ipynb)\n",
9+
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
10+
"\n",
11+
"**Start a TiTiler server**\n",
12+
"\n",
13+
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"# %pip install \"leafmap[titiler]\""
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"import leafmap.maplibregl as leafmap"
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": null,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"leafmap.run_titiler(show_logs=False)"
41+
]
42+
},
43+
{
44+
"cell_type": "code",
45+
"execution_count": null,
46+
"metadata": {},
47+
"outputs": [],
48+
"source": [
49+
"url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_test_naip.tif\""
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"m = leafmap.Map()\n",
59+
"m.add_cog_layer(url)\n",
60+
"m"
61+
]
62+
}
63+
],
64+
"metadata": {
65+
"kernelspec": {
66+
"display_name": "tiler",
67+
"language": "python",
68+
"name": "python3"
69+
},
70+
"language_info": {
71+
"codemirror_mode": {
72+
"name": "ipython",
73+
"version": 3
74+
},
75+
"file_extension": ".py",
76+
"mimetype": "text/x-python",
77+
"name": "python",
78+
"nbconvert_exporter": "python",
79+
"pygments_lexer": "ipython3",
80+
"version": "3.12.12"
81+
}
82+
},
83+
"nbformat": 4,
84+
"nbformat_minor": 4
85+
}

docs/maplibre/overview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,12 @@ View local raster datasets with localtileserver
483483

484484
[![](https://i.imgur.com/Q9sQLCP.png)](https://leafmap.org/maplibre/local_raster)
485485

486+
## Start a TiTiler server
487+
488+
Start a local TiTiler server for visualizing COGs.
489+
490+
[![](https://github.com/user-attachments/assets/73d77340-93d5-47a7-8c1a-373d2c154110)](https://leafmap.org/maplibre/local_titiler)
491+
486492
## Locate the user
487493

488494
Geolocate the user and then track their current location on the map using the GeolocateControl.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"[![image](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://demo.leafmap.org/lab/index.html?path=notebooks/109_local_titiler.ipynb)\n",
8+
"[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/leafmap/blob/master/docs/notebooks/109_local_titiler.ipynb)\n",
9+
"[![image](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/opengeos/leafmap/HEAD)\n",
10+
"\n",
11+
"**Start a TiTiler server**\n",
12+
"\n",
13+
"Uncomment the following line to install [leafmap](https://leafmap.org) if needed."
14+
]
15+
},
16+
{
17+
"cell_type": "code",
18+
"execution_count": null,
19+
"metadata": {},
20+
"outputs": [],
21+
"source": [
22+
"# %pip install \"leafmap[titiler]\""
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"metadata": {},
29+
"outputs": [],
30+
"source": [
31+
"import leafmap"
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": null,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"leafmap.run_titiler(show_logs=False)"
41+
]
42+
},
43+
{
44+
"cell_type": "code",
45+
"execution_count": null,
46+
"metadata": {},
47+
"outputs": [],
48+
"source": [
49+
"url = \"https://huggingface.co/datasets/giswqs/geospatial/resolve/main/las_vegas_test_naip.tif\""
50+
]
51+
},
52+
{
53+
"cell_type": "code",
54+
"execution_count": null,
55+
"metadata": {},
56+
"outputs": [],
57+
"source": [
58+
"m = leafmap.Map()\n",
59+
"m.add_cog_layer(url)\n",
60+
"m"
61+
]
62+
}
63+
],
64+
"metadata": {
65+
"kernelspec": {
66+
"display_name": "tiler",
67+
"language": "python",
68+
"name": "python3"
69+
},
70+
"language_info": {
71+
"codemirror_mode": {
72+
"name": "ipython",
73+
"version": 3
74+
},
75+
"file_extension": ".py",
76+
"mimetype": "text/x-python",
77+
"name": "python",
78+
"nbconvert_exporter": "python",
79+
"pygments_lexer": "ipython3",
80+
"version": "3.12.12"
81+
}
82+
},
83+
"nbformat": 4,
84+
"nbformat_minor": 4
85+
}

leafmap/foliumap.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
read_netcdf,
5858
read_raster,
5959
read_rasters,
60+
run_titiler,
6061
save_colorbar,
6162
search_qms,
6263
search_xyz_services,

leafmap/maplibregl.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
random_string,
6363
read_geojson,
6464
read_vector,
65+
run_titiler,
6566
sort_files,
6667
stac_assets,
6768
start_duckdb_tile_server,

leafmap/stac.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ def check_titiler_endpoint(titiler_endpoint: Optional[str] = None) -> Any:
118118
Returns:
119119
The titiler endpoint.
120120
"""
121-
if titiler_endpoint is None:
121+
if titiler_endpoint is not None and titiler_endpoint.lower() == "local":
122+
titiler_endpoint = run_titiler(show_logs=False)
123+
elif titiler_endpoint is None:
122124
if os.environ.get("TITILER_ENDPOINT") is not None:
123125
titiler_endpoint = os.environ.get("TITILER_ENDPOINT")
124126

@@ -2514,3 +2516,91 @@ def get_cog_link_from_stac_item(item_url: str) -> str:
25142516
except Exception as e:
25152517
print(f"Failed to retrieve STAC item: {e}")
25162518
return None
2519+
2520+
2521+
def run_titiler(
2522+
show_logs: bool = False,
2523+
start_port: int = 8000,
2524+
max_port: int = 8100,
2525+
return_titiler_endpoint: bool = False,
2526+
):
2527+
"""Run TiTiler as a background service on an available port.
2528+
2529+
Args:
2530+
show_logs (bool): If True, stream logs to the notebook output.
2531+
start_port (int): First port to try.
2532+
max_port (int): Last port to try (exclusive).
2533+
return_titiler_endpoint (bool): If True, return the titiler endpoint. Defaults to False.
2534+
2535+
Returns:
2536+
tuple: (endpoint, port, process)
2537+
"""
2538+
2539+
import subprocess
2540+
import socket
2541+
import atexit
2542+
import signal
2543+
import time
2544+
import threading
2545+
2546+
def find_free_port(start, end):
2547+
for port in range(start, end):
2548+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
2549+
try:
2550+
s.bind(("127.0.0.1", port))
2551+
return port
2552+
except OSError:
2553+
continue
2554+
raise RuntimeError(f"No free port found between {start} and {end}")
2555+
2556+
port = find_free_port(start_port, max_port)
2557+
2558+
cmd = [
2559+
"uvicorn",
2560+
"titiler.application.main:app",
2561+
"--host",
2562+
"127.0.0.1",
2563+
"--port",
2564+
str(port),
2565+
"--log-level",
2566+
"info",
2567+
]
2568+
2569+
proc = subprocess.Popen(
2570+
cmd,
2571+
stdout=subprocess.PIPE if show_logs else subprocess.DEVNULL,
2572+
stderr=subprocess.STDOUT,
2573+
)
2574+
2575+
# Optionally stream logs in a background thread
2576+
if show_logs:
2577+
2578+
def stream_logs():
2579+
for line in iter(proc.stdout.readline, b""):
2580+
print(line.decode().rstrip())
2581+
2582+
threading.Thread(target=stream_logs, daemon=True).start()
2583+
2584+
# Wait a bit for startup
2585+
time.sleep(2)
2586+
endpoint = f"http://127.0.0.1:{port}"
2587+
print(f"🚀 TiTiler is running at {endpoint}")
2588+
2589+
# Register cleanup on kernel shutdown
2590+
def stop_titiler():
2591+
if proc.poll() is None:
2592+
print("🛑 Stopping TiTiler...")
2593+
proc.send_signal(signal.SIGINT)
2594+
try:
2595+
proc.wait(timeout=5)
2596+
except subprocess.TimeoutExpired:
2597+
proc.kill()
2598+
2599+
atexit.register(stop_titiler)
2600+
2601+
os.environ["TITILER_ENDPOINT"] = endpoint
2602+
os.environ["TITILER_PORT"] = str(port)
2603+
os.environ["TITILER_PROCESS"] = str(proc)
2604+
2605+
if return_titiler_endpoint:
2606+
return endpoint, port, proc

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ nav:
261261
- maplibre/live_update_feature.ipynb
262262
- maplibre/local_geojson.ipynb
263263
- maplibre/local_raster.ipynb
264+
- maplibre/local_titiler.ipynb
264265
- maplibre/locate_user.ipynb
265266
- maplibre/map_tiles.ipynb
266267
- maplibre/mapillary.ipynb
@@ -402,3 +403,4 @@ nav:
402403
- notebooks/106_aws_s3.ipynb
403404
- notebooks/107_copernicus.ipynb
404405
- notebooks/108_add_geotiff.ipynb
406+
- notebooks/109_local_titiler.ipynb

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ authors = [{name = "Qiusheng Wu", email = "giswqs@gmail.com"}]
3030
[project.optional-dependencies]
3131
backends = ["bokeh", "keplergl", "maplibre", "pydeck", "plotly"]
3232
lidar = ["geopandas","ipygany", "ipyvtklink", "laspy", "panel", "pyntcloud[LAS]", "pyvista[all]"]
33-
raster = ["localtileserver>=0.10.6", "jupyter-server-proxy", "rio-cogeo", "rioxarray", "netcdf4", "d2spy", "h5netcdf", "h5py", "opera-utils", "rioxarray", "psutil"]
33+
raster = ["localtileserver>=0.10.6", "jupyter-server-proxy", "rio-cogeo", "rioxarray", "netcdf4", "d2spy", "h5netcdf", "h5py", "opera-utils", "psutil", "titiler", "uvicorn"]
3434
viewer = ["localtileserver", "fiona"]
3535
usgs = ["pynhd", "py3dep"]
3636
sql = ["psycopg2", "sqlalchemy"]
@@ -41,6 +41,7 @@ ai = ["geopandas", "osmnx", "localtileserver>=0.10.4", "rastervision", "pytorch-
4141
maplibre = ["anywidget", "geopandas", "fiona", "h3", "ipyvuetify", "localtileserver", "mapclassify", "maplibre>=0.3.1", "pmtiles", "rioxarray", "xarray"]
4242
gdal = ["gdal", "pyproj"]
4343
duckdb = ["duckdb", "duckdb-engine", "jupysql", "flask", "flask-cors", "jupyter-server-proxy"]
44+
titiler = ["titiler", "uvicorn"]
4445

4546

4647
[project.scripts]

0 commit comments

Comments
 (0)