geomind-ai 1.0.0__py3-none-any.whl → 1.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geomind/__init__.py +11 -11
- geomind/agent.py +441 -445
- geomind/cli.py +125 -125
- geomind/config.py +55 -55
- geomind/tools/__init__.py +27 -27
- geomind/tools/geocoding.py +108 -108
- geomind/tools/processing.py +351 -349
- geomind/tools/stac_search.py +234 -231
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/METADATA +78 -85
- geomind_ai-1.0.2.dist-info/RECORD +14 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/licenses/LICENSE +21 -21
- geomind_ai-1.0.0.dist-info/RECORD +0 -14
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/WHEEL +0 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/entry_points.txt +0 -0
- {geomind_ai-1.0.0.dist-info → geomind_ai-1.0.2.dist-info}/top_level.txt +0 -0
geomind/cli.py
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Command-line interface for GeoMind.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
import argparse
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
from .agent import GeoMindAgent
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main():
|
|
13
|
-
"""Main CLI entry point for the geomind package."""
|
|
14
|
-
parser = argparse.ArgumentParser(
|
|
15
|
-
description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
|
|
16
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
-
epilog="""
|
|
18
|
-
Examples:
|
|
19
|
-
# Interactive mode
|
|
20
|
-
geomind
|
|
21
|
-
|
|
22
|
-
# Single query
|
|
23
|
-
geomind --query "Find recent imagery of Paris"
|
|
24
|
-
|
|
25
|
-
# With custom model
|
|
26
|
-
geomind --model "anthropic/claude-3.5-sonnet"
|
|
27
|
-
|
|
28
|
-
# With API key
|
|
29
|
-
geomind --api-key "your-openrouter-api-key"
|
|
30
|
-
|
|
31
|
-
Environment Variables:
|
|
32
|
-
OPENROUTER_API_KEY Your OpenRouter API key
|
|
33
|
-
OPENROUTER_MODEL Model to use (default: xiaomi/mimo-v2-flash:free)
|
|
34
|
-
OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
|
|
35
|
-
""",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
parser.add_argument(
|
|
39
|
-
"--query",
|
|
40
|
-
"-q",
|
|
41
|
-
type=str,
|
|
42
|
-
help="Single query to run (if not provided, starts interactive mode)",
|
|
43
|
-
)
|
|
44
|
-
parser.add_argument(
|
|
45
|
-
"--model",
|
|
46
|
-
"-m",
|
|
47
|
-
type=str,
|
|
48
|
-
help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
|
|
49
|
-
)
|
|
50
|
-
parser.add_argument(
|
|
51
|
-
"--api-key",
|
|
52
|
-
"-k",
|
|
53
|
-
type=str,
|
|
54
|
-
help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
|
|
55
|
-
)
|
|
56
|
-
parser.add_argument(
|
|
57
|
-
"--version", "-v", action="store_true", help="Show version and exit"
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
args = parser.parse_args()
|
|
61
|
-
|
|
62
|
-
if args.version:
|
|
63
|
-
from . import __version__
|
|
64
|
-
|
|
65
|
-
print(f"GeoMind version {__version__}")
|
|
66
|
-
sys.exit(0)
|
|
67
|
-
|
|
68
|
-
# Start interactive or single-query mode
|
|
69
|
-
try:
|
|
70
|
-
if args.query:
|
|
71
|
-
# Single query mode
|
|
72
|
-
agent = GeoMindAgent(model=args.model, api_key=args.api_key)
|
|
73
|
-
agent.chat(args.query)
|
|
74
|
-
else:
|
|
75
|
-
# Interactive mode
|
|
76
|
-
run_interactive(model=args.model, api_key=args.api_key)
|
|
77
|
-
except ValueError as e:
|
|
78
|
-
print(f"\n❌ Error: {e}")
|
|
79
|
-
sys.exit(1)
|
|
80
|
-
except KeyboardInterrupt:
|
|
81
|
-
print("\n\n👋 Goodbye!")
|
|
82
|
-
sys.exit(0)
|
|
83
|
-
except Exception as e:
|
|
84
|
-
print(f"\n❌ Unexpected error: {e}")
|
|
85
|
-
sys.exit(1)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
89
|
-
"""Run interactive CLI mode."""
|
|
90
|
-
print("=" * 60)
|
|
91
|
-
print("🌍 GeoMind - Geospatial AI Agent")
|
|
92
|
-
print("=" * 60)
|
|
93
|
-
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
94
|
-
print("Type 'quit' or 'exit' to end the session")
|
|
95
|
-
print("Type 'reset' to start a new conversation")
|
|
96
|
-
print("=" * 60)
|
|
97
|
-
|
|
98
|
-
agent = GeoMindAgent(model=model, api_key=api_key)
|
|
99
|
-
|
|
100
|
-
while True:
|
|
101
|
-
try:
|
|
102
|
-
user_input = input("\n💬 You: ").strip()
|
|
103
|
-
|
|
104
|
-
if not user_input:
|
|
105
|
-
continue
|
|
106
|
-
|
|
107
|
-
if user_input.lower() in ["quit", "exit", "q"]:
|
|
108
|
-
print("\n👋 Goodbye!")
|
|
109
|
-
break
|
|
110
|
-
|
|
111
|
-
if user_input.lower() == "reset":
|
|
112
|
-
agent.reset()
|
|
113
|
-
continue
|
|
114
|
-
|
|
115
|
-
agent.chat(user_input)
|
|
116
|
-
|
|
117
|
-
except KeyboardInterrupt:
|
|
118
|
-
print("\n\n👋 Goodbye!")
|
|
119
|
-
break
|
|
120
|
-
except Exception as e:
|
|
121
|
-
print(f"\n❌ Error: {e}")
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if __name__ == "__main__":
|
|
125
|
-
main()
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for GeoMind.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import argparse
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from .agent import GeoMindAgent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
"""Main CLI entry point for the geomind package."""
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="GeoMind - AI agent for geospatial analysis with Sentinel-2 imagery",
|
|
16
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
17
|
+
epilog="""
|
|
18
|
+
Examples:
|
|
19
|
+
# Interactive mode
|
|
20
|
+
geomind
|
|
21
|
+
|
|
22
|
+
# Single query
|
|
23
|
+
geomind --query "Find recent imagery of Paris"
|
|
24
|
+
|
|
25
|
+
# With custom model
|
|
26
|
+
geomind --model "anthropic/claude-3.5-sonnet"
|
|
27
|
+
|
|
28
|
+
# With API key
|
|
29
|
+
geomind --api-key "your-openrouter-api-key"
|
|
30
|
+
|
|
31
|
+
Environment Variables:
|
|
32
|
+
OPENROUTER_API_KEY Your OpenRouter API key
|
|
33
|
+
OPENROUTER_MODEL Model to use (default: xiaomi/mimo-v2-flash:free)
|
|
34
|
+
OPENROUTER_API_URL API endpoint (default: https://openrouter.ai/api/v1)
|
|
35
|
+
""",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--query",
|
|
40
|
+
"-q",
|
|
41
|
+
type=str,
|
|
42
|
+
help="Single query to run (if not provided, starts interactive mode)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--model",
|
|
46
|
+
"-m",
|
|
47
|
+
type=str,
|
|
48
|
+
help="Model name to use (e.g., 'anthropic/claude-3.5-sonnet')",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--api-key",
|
|
52
|
+
"-k",
|
|
53
|
+
type=str,
|
|
54
|
+
help="OpenRouter API key (or set OPENROUTER_API_KEY env variable)",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--version", "-v", action="store_true", help="Show version and exit"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
if args.version:
|
|
63
|
+
from . import __version__
|
|
64
|
+
|
|
65
|
+
print(f"GeoMind version {__version__}")
|
|
66
|
+
sys.exit(0)
|
|
67
|
+
|
|
68
|
+
# Start interactive or single-query mode
|
|
69
|
+
try:
|
|
70
|
+
if args.query:
|
|
71
|
+
# Single query mode
|
|
72
|
+
agent = GeoMindAgent(model=args.model, api_key=args.api_key)
|
|
73
|
+
agent.chat(args.query)
|
|
74
|
+
else:
|
|
75
|
+
# Interactive mode
|
|
76
|
+
run_interactive(model=args.model, api_key=args.api_key)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
print(f"\n❌ Error: {e}")
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
except KeyboardInterrupt:
|
|
81
|
+
print("\n\n👋 Goodbye!")
|
|
82
|
+
sys.exit(0)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
print(f"\n❌ Unexpected error: {e}")
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def run_interactive(model: Optional[str] = None, api_key: Optional[str] = None):
|
|
89
|
+
"""Run interactive CLI mode."""
|
|
90
|
+
print("=" * 60)
|
|
91
|
+
print("🌍 GeoMind - Geospatial AI Agent")
|
|
92
|
+
print("=" * 60)
|
|
93
|
+
print("Powered by OpenRouter | Sentinel-2 Imagery")
|
|
94
|
+
print("Type 'quit' or 'exit' to end the session")
|
|
95
|
+
print("Type 'reset' to start a new conversation")
|
|
96
|
+
print("=" * 60)
|
|
97
|
+
|
|
98
|
+
agent = GeoMindAgent(model=model, api_key=api_key)
|
|
99
|
+
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
user_input = input("\n💬 You: ").strip()
|
|
103
|
+
|
|
104
|
+
if not user_input:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if user_input.lower() in ["quit", "exit", "q"]:
|
|
108
|
+
print("\n👋 Goodbye!")
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
if user_input.lower() == "reset":
|
|
112
|
+
agent.reset()
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
agent.chat(user_input)
|
|
116
|
+
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
print("\n\n👋 Goodbye!")
|
|
119
|
+
break
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(f"\n❌ Error: {e}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|
geomind/config.py
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Configuration settings for GeoMind agent.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from dotenv import load_dotenv
|
|
8
|
-
|
|
9
|
-
# Load environment variables from .env file
|
|
10
|
-
load_dotenv()
|
|
11
|
-
|
|
12
|
-
# STAC API Configuration
|
|
13
|
-
STAC_API_URL = "https://stac.core.eopf.eodc.eu"
|
|
14
|
-
STAC_COLLECTION = "sentinel-2-l2a"
|
|
15
|
-
|
|
16
|
-
# Sentinel-2 Band Configuration
|
|
17
|
-
SENTINEL2_BANDS = {
|
|
18
|
-
"B01": {"name": "Coastal aerosol", "wavelength": 0.443, "resolution": 60},
|
|
19
|
-
"B02": {"name": "Blue", "wavelength": 0.490, "resolution": 10},
|
|
20
|
-
"B03": {"name": "Green", "wavelength": 0.560, "resolution": 10},
|
|
21
|
-
"B04": {"name": "Red", "wavelength": 0.665, "resolution": 10},
|
|
22
|
-
"B05": {"name": "Red Edge 1", "wavelength": 0.704, "resolution": 20},
|
|
23
|
-
"B06": {"name": "Red Edge 2", "wavelength": 0.740, "resolution": 20},
|
|
24
|
-
"B07": {"name": "Red Edge 3", "wavelength": 0.783, "resolution": 20},
|
|
25
|
-
"B08": {"name": "NIR", "wavelength": 0.842, "resolution": 10},
|
|
26
|
-
"B8A": {"name": "NIR Narrow", "wavelength": 0.865, "resolution": 20},
|
|
27
|
-
"B09": {"name": "Water Vapour", "wavelength": 0.945, "resolution": 60},
|
|
28
|
-
"B11": {"name": "SWIR 1", "wavelength": 1.610, "resolution": 20},
|
|
29
|
-
"B12": {"name": "SWIR 2", "wavelength": 2.190, "resolution": 20},
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
# Reflectance scale and offset (from STAC metadata)
|
|
33
|
-
REFLECTANCE_SCALE = 0.0001
|
|
34
|
-
REFLECTANCE_OFFSET = -0.1
|
|
35
|
-
|
|
36
|
-
# RGB Band Mapping
|
|
37
|
-
RGB_BANDS = {"red": "b04", "green": "b03", "blue": "b02"}
|
|
38
|
-
|
|
39
|
-
# Default search parameters
|
|
40
|
-
DEFAULT_MAX_CLOUD_COVER = 20 # percent
|
|
41
|
-
DEFAULT_BUFFER_KM = 10 # km buffer around point for bbox
|
|
42
|
-
DEFAULT_MAX_ITEMS = 10
|
|
43
|
-
|
|
44
|
-
# Output directory for saved images
|
|
45
|
-
OUTPUT_DIR = Path("outputs")
|
|
46
|
-
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
47
|
-
|
|
48
|
-
# Geocoding configuration
|
|
49
|
-
GEOCODER_USER_AGENT = "geomind_agent_v0.1"
|
|
50
|
-
|
|
51
|
-
# OpenRouter API Configuration
|
|
52
|
-
# OpenRouter provides access to multiple AI models via API
|
|
53
|
-
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
|
54
|
-
OPENROUTER_API_URL = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1")
|
|
55
|
-
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "xiaomi/mimo-v2-flash:free")
|
|
1
|
+
"""
|
|
2
|
+
Configuration settings for GeoMind agent.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
# Load environment variables from .env file
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
# STAC API Configuration
|
|
13
|
+
STAC_API_URL = "https://stac.core.eopf.eodc.eu"
|
|
14
|
+
STAC_COLLECTION = "sentinel-2-l2a"
|
|
15
|
+
|
|
16
|
+
# Sentinel-2 Band Configuration
|
|
17
|
+
SENTINEL2_BANDS = {
|
|
18
|
+
"B01": {"name": "Coastal aerosol", "wavelength": 0.443, "resolution": 60},
|
|
19
|
+
"B02": {"name": "Blue", "wavelength": 0.490, "resolution": 10},
|
|
20
|
+
"B03": {"name": "Green", "wavelength": 0.560, "resolution": 10},
|
|
21
|
+
"B04": {"name": "Red", "wavelength": 0.665, "resolution": 10},
|
|
22
|
+
"B05": {"name": "Red Edge 1", "wavelength": 0.704, "resolution": 20},
|
|
23
|
+
"B06": {"name": "Red Edge 2", "wavelength": 0.740, "resolution": 20},
|
|
24
|
+
"B07": {"name": "Red Edge 3", "wavelength": 0.783, "resolution": 20},
|
|
25
|
+
"B08": {"name": "NIR", "wavelength": 0.842, "resolution": 10},
|
|
26
|
+
"B8A": {"name": "NIR Narrow", "wavelength": 0.865, "resolution": 20},
|
|
27
|
+
"B09": {"name": "Water Vapour", "wavelength": 0.945, "resolution": 60},
|
|
28
|
+
"B11": {"name": "SWIR 1", "wavelength": 1.610, "resolution": 20},
|
|
29
|
+
"B12": {"name": "SWIR 2", "wavelength": 2.190, "resolution": 20},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Reflectance scale and offset (from STAC metadata)
|
|
33
|
+
REFLECTANCE_SCALE = 0.0001
|
|
34
|
+
REFLECTANCE_OFFSET = -0.1
|
|
35
|
+
|
|
36
|
+
# RGB Band Mapping
|
|
37
|
+
RGB_BANDS = {"red": "b04", "green": "b03", "blue": "b02"}
|
|
38
|
+
|
|
39
|
+
# Default search parameters
|
|
40
|
+
DEFAULT_MAX_CLOUD_COVER = 20 # percent
|
|
41
|
+
DEFAULT_BUFFER_KM = 10 # km buffer around point for bbox
|
|
42
|
+
DEFAULT_MAX_ITEMS = 10
|
|
43
|
+
|
|
44
|
+
# Output directory for saved images
|
|
45
|
+
OUTPUT_DIR = Path("outputs")
|
|
46
|
+
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Geocoding configuration
|
|
49
|
+
GEOCODER_USER_AGENT = "geomind_agent_v0.1"
|
|
50
|
+
|
|
51
|
+
# OpenRouter API Configuration
|
|
52
|
+
# OpenRouter provides access to multiple AI models via API
|
|
53
|
+
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
|
|
54
|
+
OPENROUTER_API_URL = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1")
|
|
55
|
+
OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "xiaomi/mimo-v2-flash:free")
|
geomind/tools/__init__.py
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
"""
|
|
2
|
-
GeoMind Tools - Functions available to the AI agent.
|
|
3
|
-
|
|
4
|
-
This module contains all the tools that the agent can call:
|
|
5
|
-
- Geocoding: Convert place names to coordinates
|
|
6
|
-
- STAC Search: Query the Sentinel-2 catalog
|
|
7
|
-
- Processing: Load and process imagery data
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from .geocoding import geocode_location, get_bbox_from_location
|
|
11
|
-
from .stac_search import search_imagery, get_item_details, list_recent_imagery
|
|
12
|
-
from .processing import (
|
|
13
|
-
create_rgb_composite,
|
|
14
|
-
calculate_ndvi,
|
|
15
|
-
get_band_statistics,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
__all__ = [
|
|
19
|
-
"geocode_location",
|
|
20
|
-
"get_bbox_from_location",
|
|
21
|
-
"search_imagery",
|
|
22
|
-
"get_item_details",
|
|
23
|
-
"list_recent_imagery",
|
|
24
|
-
"create_rgb_composite",
|
|
25
|
-
"calculate_ndvi",
|
|
26
|
-
"get_band_statistics",
|
|
27
|
-
]
|
|
1
|
+
"""
|
|
2
|
+
GeoMind Tools - Functions available to the AI agent.
|
|
3
|
+
|
|
4
|
+
This module contains all the tools that the agent can call:
|
|
5
|
+
- Geocoding: Convert place names to coordinates
|
|
6
|
+
- STAC Search: Query the Sentinel-2 catalog
|
|
7
|
+
- Processing: Load and process imagery data
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .geocoding import geocode_location, get_bbox_from_location
|
|
11
|
+
from .stac_search import search_imagery, get_item_details, list_recent_imagery
|
|
12
|
+
from .processing import (
|
|
13
|
+
create_rgb_composite,
|
|
14
|
+
calculate_ndvi,
|
|
15
|
+
get_band_statistics,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"geocode_location",
|
|
20
|
+
"get_bbox_from_location",
|
|
21
|
+
"search_imagery",
|
|
22
|
+
"get_item_details",
|
|
23
|
+
"list_recent_imagery",
|
|
24
|
+
"create_rgb_composite",
|
|
25
|
+
"calculate_ndvi",
|
|
26
|
+
"get_band_statistics",
|
|
27
|
+
]
|
geomind/tools/geocoding.py
CHANGED
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Geocoding tools for converting place names to coordinates.
|
|
3
|
-
|
|
4
|
-
Uses OpenStreetMap's Nominatim service via geopy.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Optional
|
|
8
|
-
from geopy.geocoders import Nominatim
|
|
9
|
-
from geopy.extra.rate_limiter import RateLimiter
|
|
10
|
-
|
|
11
|
-
from ..config import GEOCODER_USER_AGENT, DEFAULT_BUFFER_KM
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def geocode_location(place_name: str) -> dict:
|
|
15
|
-
"""
|
|
16
|
-
Convert a place name to geographic coordinates.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
place_name: Name of the location (e.g., "New York", "Paris, France")
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
Dictionary with latitude, longitude, and full address
|
|
23
|
-
|
|
24
|
-
Example:
|
|
25
|
-
>>> geocode_location("Central Park, New York")
|
|
26
|
-
{'latitude': 40.7828, 'longitude': -73.9653, 'address': '...'}
|
|
27
|
-
"""
|
|
28
|
-
geolocator = Nominatim(user_agent=GEOCODER_USER_AGENT, timeout=10)
|
|
29
|
-
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
|
|
30
|
-
|
|
31
|
-
location = geocode(place_name)
|
|
32
|
-
|
|
33
|
-
if location is None:
|
|
34
|
-
return {
|
|
35
|
-
"success": False,
|
|
36
|
-
"error": f"Could not find location: {place_name}",
|
|
37
|
-
"latitude": None,
|
|
38
|
-
"longitude": None,
|
|
39
|
-
"address": None,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
"success": True,
|
|
44
|
-
"latitude": location.latitude,
|
|
45
|
-
"longitude": location.longitude,
|
|
46
|
-
"address": location.address,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def get_bbox_from_location(place_name: str, buffer_km: Optional[float] = None) -> dict:
|
|
51
|
-
"""
|
|
52
|
-
Convert a place name to a bounding box suitable for STAC queries.
|
|
53
|
-
|
|
54
|
-
Creates a square bounding box centered on the location with the
|
|
55
|
-
specified buffer distance.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
place_name: Name of the location (e.g., "San Francisco")
|
|
59
|
-
buffer_km: Buffer distance in kilometers (default: 10km)
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
Dictionary with bbox [min_lon, min_lat, max_lon, max_lat] and center point
|
|
63
|
-
|
|
64
|
-
Example:
|
|
65
|
-
>>> get_bbox_from_location("London", buffer_km=5)
|
|
66
|
-
{'bbox': [-0.17, 51.46, -0.08, 51.55], 'center': {...}}
|
|
67
|
-
"""
|
|
68
|
-
if buffer_km is None:
|
|
69
|
-
buffer_km = DEFAULT_BUFFER_KM
|
|
70
|
-
|
|
71
|
-
# Get coordinates
|
|
72
|
-
location_result = geocode_location(place_name)
|
|
73
|
-
|
|
74
|
-
if not location_result["success"]:
|
|
75
|
-
return {
|
|
76
|
-
"success": False,
|
|
77
|
-
"error": location_result["error"],
|
|
78
|
-
"bbox": None,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
lat = location_result["latitude"]
|
|
82
|
-
lon = location_result["longitude"]
|
|
83
|
-
|
|
84
|
-
# Calculate approximate degree offset
|
|
85
|
-
# 1 degree latitude ≈ 111 km
|
|
86
|
-
# 1 degree longitude ≈ 111 * cos(latitude) km
|
|
87
|
-
import math
|
|
88
|
-
|
|
89
|
-
lat_offset = buffer_km / 111.0
|
|
90
|
-
lon_offset = buffer_km / (111.0 * math.cos(math.radians(lat)))
|
|
91
|
-
|
|
92
|
-
bbox = [
|
|
93
|
-
lon - lon_offset, # min_lon (west)
|
|
94
|
-
lat - lat_offset, # min_lat (south)
|
|
95
|
-
lon + lon_offset, # max_lon (east)
|
|
96
|
-
lat + lat_offset, # max_lat (north)
|
|
97
|
-
]
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
"success": True,
|
|
101
|
-
"bbox": bbox,
|
|
102
|
-
"center": {
|
|
103
|
-
"latitude": lat,
|
|
104
|
-
"longitude": lon,
|
|
105
|
-
},
|
|
106
|
-
"address": location_result["address"],
|
|
107
|
-
"buffer_km": buffer_km,
|
|
108
|
-
}
|
|
1
|
+
"""
|
|
2
|
+
Geocoding tools for converting place names to coordinates.
|
|
3
|
+
|
|
4
|
+
Uses OpenStreetMap's Nominatim service via geopy.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from geopy.geocoders import Nominatim
|
|
9
|
+
from geopy.extra.rate_limiter import RateLimiter
|
|
10
|
+
|
|
11
|
+
from ..config import GEOCODER_USER_AGENT, DEFAULT_BUFFER_KM
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def geocode_location(place_name: str) -> dict:
|
|
15
|
+
"""
|
|
16
|
+
Convert a place name to geographic coordinates.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
place_name: Name of the location (e.g., "New York", "Paris, France")
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Dictionary with latitude, longitude, and full address
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> geocode_location("Central Park, New York")
|
|
26
|
+
{'latitude': 40.7828, 'longitude': -73.9653, 'address': '...'}
|
|
27
|
+
"""
|
|
28
|
+
geolocator = Nominatim(user_agent=GEOCODER_USER_AGENT, timeout=10)
|
|
29
|
+
geocode = RateLimiter(geolocator.geocode, min_delay_seconds=1)
|
|
30
|
+
|
|
31
|
+
location = geocode(place_name)
|
|
32
|
+
|
|
33
|
+
if location is None:
|
|
34
|
+
return {
|
|
35
|
+
"success": False,
|
|
36
|
+
"error": f"Could not find location: {place_name}",
|
|
37
|
+
"latitude": None,
|
|
38
|
+
"longitude": None,
|
|
39
|
+
"address": None,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
"success": True,
|
|
44
|
+
"latitude": location.latitude,
|
|
45
|
+
"longitude": location.longitude,
|
|
46
|
+
"address": location.address,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_bbox_from_location(place_name: str, buffer_km: Optional[float] = None) -> dict:
|
|
51
|
+
"""
|
|
52
|
+
Convert a place name to a bounding box suitable for STAC queries.
|
|
53
|
+
|
|
54
|
+
Creates a square bounding box centered on the location with the
|
|
55
|
+
specified buffer distance.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
place_name: Name of the location (e.g., "San Francisco")
|
|
59
|
+
buffer_km: Buffer distance in kilometers (default: 10km)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Dictionary with bbox [min_lon, min_lat, max_lon, max_lat] and center point
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> get_bbox_from_location("London", buffer_km=5)
|
|
66
|
+
{'bbox': [-0.17, 51.46, -0.08, 51.55], 'center': {...}}
|
|
67
|
+
"""
|
|
68
|
+
if buffer_km is None:
|
|
69
|
+
buffer_km = DEFAULT_BUFFER_KM
|
|
70
|
+
|
|
71
|
+
# Get coordinates
|
|
72
|
+
location_result = geocode_location(place_name)
|
|
73
|
+
|
|
74
|
+
if not location_result["success"]:
|
|
75
|
+
return {
|
|
76
|
+
"success": False,
|
|
77
|
+
"error": location_result["error"],
|
|
78
|
+
"bbox": None,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lat = location_result["latitude"]
|
|
82
|
+
lon = location_result["longitude"]
|
|
83
|
+
|
|
84
|
+
# Calculate approximate degree offset
|
|
85
|
+
# 1 degree latitude ≈ 111 km
|
|
86
|
+
# 1 degree longitude ≈ 111 * cos(latitude) km
|
|
87
|
+
import math
|
|
88
|
+
|
|
89
|
+
lat_offset = buffer_km / 111.0
|
|
90
|
+
lon_offset = buffer_km / (111.0 * math.cos(math.radians(lat)))
|
|
91
|
+
|
|
92
|
+
bbox = [
|
|
93
|
+
lon - lon_offset, # min_lon (west)
|
|
94
|
+
lat - lat_offset, # min_lat (south)
|
|
95
|
+
lon + lon_offset, # max_lon (east)
|
|
96
|
+
lat + lat_offset, # max_lat (north)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"success": True,
|
|
101
|
+
"bbox": bbox,
|
|
102
|
+
"center": {
|
|
103
|
+
"latitude": lat,
|
|
104
|
+
"longitude": lon,
|
|
105
|
+
},
|
|
106
|
+
"address": location_result["address"],
|
|
107
|
+
"buffer_km": buffer_km,
|
|
108
|
+
}
|