Spaces:
Running
Running
feat: initial clean project structure with Git LFS
Browse files- .gitattributes +1 -0
- .gitignore +7 -0
- LICENSE +21 -0
- README.md +287 -0
- app.py +580 -0
- cities_data.py +996 -0
- create_map_poster.py +822 -0
- fonts/GoudyOldStyle-Bold.ttf +0 -0
- fonts/GoudyOldStyle-Regular.ttf +0 -0
- fonts/HYWenRunSongYunU.ttf +3 -0
- pyproject.toml +45 -0
- requirements.txt +29 -0
- restart.sh +28 -0
- src/maptoposter/__init__.py +2 -0
- src/maptoposter/py.typed +0 -0
- themes/autumn.json +15 -0
- themes/blueprint.json +15 -0
- themes/contrast_zones.json +15 -0
- themes/copper_patina.json +15 -0
- themes/feature_based.json +15 -0
- themes/forest.json +15 -0
- themes/gradient_roads.json +15 -0
- themes/japanese_ink.json +15 -0
- themes/midnight_blue.json +15 -0
- themes/monochrome_blue.json +15 -0
- themes/neon_cyberpunk.json +15 -0
- themes/noir.json +15 -0
- themes/ocean.json +15 -0
- themes/pastel_dream.json +15 -0
- themes/sunset.json +15 -0
- themes/terracotta.json +15 -0
- themes/warm_beige.json +15 -0
- uv.lock +0 -0
.gitattributes
CHANGED
|
@@ -33,6 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 36 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.JPG filter=lfs diff=lfs merge=lfs -text
|
| 38 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
fonts/HYWenRunSongYunU.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
*.JPG filter=lfs diff=lfs merge=lfs -text
|
| 39 |
*.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cache/
|
| 2 |
+
posters/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.DS_Store
|
| 6 |
+
.env
|
| 7 |
+
*.log
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Ankur Gupta
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -11,3 +11,290 @@ license: mit
|
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 14 |
+
|
| 15 |
+
# City Map Poster Generator
|
| 16 |
+
|
| 17 |
+
Generate beautiful, minimalist map posters for any city in the world.
|
| 18 |
+
|
| 19 |
+
<img src="posters/singapore_neon_cyberpunk_20260108_184503.png" width="250">
|
| 20 |
+
<img src="posters/dubai_midnight_blue_20260108_174920.png" width="250">
|
| 21 |
+
|
| 22 |
+
## Examples
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
| Country | City | Theme | Poster |
|
| 26 |
+
|:------------:|:--------------:|:---------------:|:------:|
|
| 27 |
+
| USA | San Francisco | sunset | <img src="posters/san_francisco_sunset_20260108_184122.png" width="250"> |
|
| 28 |
+
| Spain | Barcelona | warm_beige | <img src="posters/barcelona_warm_beige_20260108_172924.png" width="250"> |
|
| 29 |
+
| Italy | Venice | blueprint | <img src="posters/venice_blueprint_20260108_165527.png" width="250"> |
|
| 30 |
+
| Japan | Tokyo | japanese_ink | <img src="posters/tokyo_japanese_ink_20260108_165830.png" width="250"> |
|
| 31 |
+
| India | Mumbai | contrast_zones | <img src="posters/mumbai_contrast_zones_20260108_170325.png" width="250"> |
|
| 32 |
+
| Morocco | Marrakech | terracotta | <img src="posters/marrakech_terracotta_20260108_180821.png" width="250"> |
|
| 33 |
+
| Singapore | Singapore | neon_cyberpunk | <img src="posters/singapore_neon_cyberpunk_20260108_184503.png" width="250"> |
|
| 34 |
+
| Australia | Melbourne | forest | <img src="posters/melbourne_forest_20260108_181459.png" width="250"> |
|
| 35 |
+
| UAE | Dubai | midnight_blue | <img src="posters/dubai_midnight_blue_20260108_174920.png" width="250"> |
|
| 36 |
+
|
| 37 |
+
## Installation
|
| 38 |
+
|
| 39 |
+
Recommended: Use `uv` to create a virtual environment and install dependencies:
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
uv venv
|
| 43 |
+
source .venv/bin/activate
|
| 44 |
+
uv pip install -r requirements.txt
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Or simply sync with uv:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
uv sync
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Web UI (Gradio)
|
| 54 |
+
|
| 55 |
+
Launch the visual interface for interactive poster generation:
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
uv run python app.py
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
Then open [http://localhost:7860](http://localhost:7860) in your browser.
|
| 62 |
+
|
| 63 |
+
**Features:**
|
| 64 |
+
- 🔍 City search or cascading selection (Country → Province → City)
|
| 65 |
+
- 🎨 17 themes with color preview
|
| 66 |
+
- ⚙️ Adjustable parameters (distance, size, format)
|
| 67 |
+
- 🖼️ Real-time preview and download
|
| 68 |
+
|
| 69 |
+
## Command Line Usage
|
| 70 |
+
|
| 71 |
+
If the virtual environment is activated:
|
| 72 |
+
```bash
|
| 73 |
+
python create_map_poster.py --city <city> --country <country> [options]
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Or without activating:
|
| 77 |
+
```bash
|
| 78 |
+
.venv/bin/python create_map_poster.py --city <city> --country <country> [options]
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
### Options
|
| 82 |
+
|
| 83 |
+
| Option | Short | Description | Default |
|
| 84 |
+
|--------|-------|-------------|---------|
|
| 85 |
+
| `--city` | `-c` | City name | required |
|
| 86 |
+
| `--country` | `-C` | Country name | required |
|
| 87 |
+
| `--theme` | `-t` | Theme name | feature_based |
|
| 88 |
+
| `--distance` | `-d` | Map radius in meters | 29000 |
|
| 89 |
+
| `--list-themes` | | List all available themes | |
|
| 90 |
+
|
| 91 |
+
### Examples
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# Iconic grid patterns
|
| 95 |
+
python create_map_poster.py -c "New York" -C "USA" -t noir -d 12000 # Manhattan grid
|
| 96 |
+
python create_map_poster.py -c "Barcelona" -C "Spain" -t warm_beige -d 8000 # Eixample district
|
| 97 |
+
|
| 98 |
+
# Waterfront & canals
|
| 99 |
+
python create_map_poster.py -c "Venice" -C "Italy" -t blueprint -d 4000 # Canal network
|
| 100 |
+
python create_map_poster.py -c "Amsterdam" -C "Netherlands" -t ocean -d 6000 # Concentric canals
|
| 101 |
+
python create_map_poster.py -c "Dubai" -C "UAE" -t midnight_blue -d 15000 # Palm & coastline
|
| 102 |
+
|
| 103 |
+
# Radial patterns
|
| 104 |
+
python create_map_poster.py -c "Paris" -C "France" -t pastel_dream -d 10000 # Haussmann boulevards
|
| 105 |
+
python create_map_poster.py -c "Moscow" -C "Russia" -t noir -d 12000 # Ring roads
|
| 106 |
+
|
| 107 |
+
# Organic old cities
|
| 108 |
+
python create_map_poster.py -c "Tokyo" -C "Japan" -t japanese_ink -d 15000 # Dense organic streets
|
| 109 |
+
python create_map_poster.py -c "Marrakech" -C "Morocco" -t terracotta -d 5000 # Medina maze
|
| 110 |
+
python create_map_poster.py -c "Rome" -C "Italy" -t warm_beige -d 8000 # Ancient layout
|
| 111 |
+
|
| 112 |
+
# Coastal cities
|
| 113 |
+
python create_map_poster.py -c "San Francisco" -C "USA" -t sunset -d 10000 # Peninsula grid
|
| 114 |
+
python create_map_poster.py -c "Sydney" -C "Australia" -t ocean -d 12000 # Harbor city
|
| 115 |
+
python create_map_poster.py -c "Mumbai" -C "India" -t contrast_zones -d 18000 # Coastal peninsula
|
| 116 |
+
|
| 117 |
+
# River cities
|
| 118 |
+
python create_map_poster.py -c "London" -C "UK" -t noir -d 15000 # Thames curves
|
| 119 |
+
python create_map_poster.py -c "Budapest" -C "Hungary" -t copper_patina -d 8000 # Danube split
|
| 120 |
+
|
| 121 |
+
# List available themes
|
| 122 |
+
python create_map_poster.py --list-themes
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Distance Guide
|
| 126 |
+
|
| 127 |
+
| Distance | Best for |
|
| 128 |
+
|----------|----------|
|
| 129 |
+
| 4000-6000m | Small/dense cities (Venice, Amsterdam center) |
|
| 130 |
+
| 8000-12000m | Medium cities, focused downtown (Paris, Barcelona) |
|
| 131 |
+
| 15000-20000m | Large metros, full city view (Tokyo, Mumbai) |
|
| 132 |
+
|
| 133 |
+
## Themes
|
| 134 |
+
|
| 135 |
+
17 themes available in `themes/` directory:
|
| 136 |
+
|
| 137 |
+
| Theme | Style |
|
| 138 |
+
|-------|-------|
|
| 139 |
+
| `feature_based` | Classic black & white with road hierarchy |
|
| 140 |
+
| `gradient_roads` | Smooth gradient shading |
|
| 141 |
+
| `contrast_zones` | High contrast urban density |
|
| 142 |
+
| `noir` | Pure black background, white roads |
|
| 143 |
+
| `midnight_blue` | Navy background with gold roads |
|
| 144 |
+
| `blueprint` | Architectural blueprint aesthetic |
|
| 145 |
+
| `neon_cyberpunk` | Dark with electric pink/cyan |
|
| 146 |
+
| `warm_beige` | Vintage sepia tones |
|
| 147 |
+
| `pastel_dream` | Soft muted pastels |
|
| 148 |
+
| `japanese_ink` | Minimalist ink wash style |
|
| 149 |
+
| `forest` | Deep greens and sage |
|
| 150 |
+
| `ocean` | Blues and teals for coastal cities |
|
| 151 |
+
| `terracotta` | Mediterranean warmth |
|
| 152 |
+
| `sunset` | Warm oranges and pinks |
|
| 153 |
+
| `autumn` | Seasonal burnt oranges and reds |
|
| 154 |
+
| `copper_patina` | Oxidized copper aesthetic |
|
| 155 |
+
| `monochrome_blue` | Single blue color family |
|
| 156 |
+
|
| 157 |
+
## Output
|
| 158 |
+
|
| 159 |
+
Posters are saved to `posters/` directory with format:
|
| 160 |
+
```
|
| 161 |
+
{city}_{theme}_{YYYYMMDD_HHMMSS}.png
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
## Adding Custom Themes
|
| 165 |
+
|
| 166 |
+
Create a JSON file in `themes/` directory:
|
| 167 |
+
|
| 168 |
+
```json
|
| 169 |
+
{
|
| 170 |
+
"name": "My Theme",
|
| 171 |
+
"description": "Description of the theme",
|
| 172 |
+
"bg": "#FFFFFF",
|
| 173 |
+
"text": "#000000",
|
| 174 |
+
"gradient_color": "#FFFFFF",
|
| 175 |
+
"water": "#C0C0C0",
|
| 176 |
+
"parks": "#F0F0F0",
|
| 177 |
+
"road_motorway": "#0A0A0A",
|
| 178 |
+
"road_primary": "#1A1A1A",
|
| 179 |
+
"road_secondary": "#2A2A2A",
|
| 180 |
+
"road_tertiary": "#3A3A3A",
|
| 181 |
+
"road_residential": "#4A4A4A",
|
| 182 |
+
"road_default": "#3A3A3A"
|
| 183 |
+
}
|
| 184 |
+
```
|
| 185 |
+
|
| 186 |
+
## Project Structure
|
| 187 |
+
|
| 188 |
+
```
|
| 189 |
+
map_poster/
|
| 190 |
+
├── create_map_poster.py # Main script
|
| 191 |
+
├── themes/ # Theme JSON files
|
| 192 |
+
├── fonts/ # Roboto font files
|
| 193 |
+
├── posters/ # Generated posters
|
| 194 |
+
└── README.md
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
## Hacker's Guide
|
| 198 |
+
|
| 199 |
+
Quick reference for contributors who want to extend or modify the script.
|
| 200 |
+
|
| 201 |
+
### Architecture Overview
|
| 202 |
+
|
| 203 |
+
```
|
| 204 |
+
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
|
| 205 |
+
│ CLI Parser │────▶│ Geocoding │────▶│ Data Fetching │
|
| 206 |
+
│ (argparse) │ │ (Nominatim) │ │ (OSMnx) │
|
| 207 |
+
└─────────────────┘ └──────────────┘ └─────────────────┘
|
| 208 |
+
│
|
| 209 |
+
┌──────────────┐ ▼
|
| 210 |
+
│ Output │◀────┌─────────────────┐
|
| 211 |
+
│ (matplotlib)│ │ Rendering │
|
| 212 |
+
└──────────────┘ │ (matplotlib) │
|
| 213 |
+
└─────────────────┘
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
### Key Functions
|
| 217 |
+
|
| 218 |
+
| Function | Purpose | Modify when... |
|
| 219 |
+
|----------|---------|----------------|
|
| 220 |
+
| `get_coordinates()` | City → lat/lon via Nominatim | Switching geocoding provider |
|
| 221 |
+
| `create_poster()` | Main rendering pipeline | Adding new map layers |
|
| 222 |
+
| `get_edge_colors_by_type()` | Road color by OSM highway tag | Changing road styling |
|
| 223 |
+
| `get_edge_widths_by_type()` | Road width by importance | Adjusting line weights |
|
| 224 |
+
| `create_gradient_fade()` | Top/bottom fade effect | Modifying gradient overlay |
|
| 225 |
+
| `load_theme()` | JSON theme → dict | Adding new theme properties |
|
| 226 |
+
|
| 227 |
+
### Rendering Layers (z-order)
|
| 228 |
+
|
| 229 |
+
```
|
| 230 |
+
z=11 Text labels (city, country, coords)
|
| 231 |
+
z=10 Gradient fades (top & bottom)
|
| 232 |
+
z=3 Roads (via ox.plot_graph)
|
| 233 |
+
z=2 Parks (green polygons)
|
| 234 |
+
z=1 Water (blue polygons)
|
| 235 |
+
z=0 Background color
|
| 236 |
+
```
|
| 237 |
+
|
| 238 |
+
### OSM Highway Types → Road Hierarchy
|
| 239 |
+
|
| 240 |
+
```python
|
| 241 |
+
# In get_edge_colors_by_type() and get_edge_widths_by_type()
|
| 242 |
+
motorway, motorway_link → Thickest (1.2), darkest
|
| 243 |
+
trunk, primary → Thick (1.0)
|
| 244 |
+
secondary → Medium (0.8)
|
| 245 |
+
tertiary → Thin (0.6)
|
| 246 |
+
residential, living_street → Thinnest (0.4), lightest
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### Adding New Features
|
| 250 |
+
|
| 251 |
+
**New map layer (e.g., railways):**
|
| 252 |
+
```python
|
| 253 |
+
# In create_poster(), after parks fetch:
|
| 254 |
+
try:
|
| 255 |
+
railways = ox.features_from_point(point, tags={'railway': 'rail'}, dist=dist)
|
| 256 |
+
except:
|
| 257 |
+
railways = None
|
| 258 |
+
|
| 259 |
+
# Then plot before roads:
|
| 260 |
+
if railways is not None and not railways.empty:
|
| 261 |
+
railways.plot(ax=ax, color=THEME['railway'], linewidth=0.5, zorder=2.5)
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
**New theme property:**
|
| 265 |
+
1. Add to theme JSON: `"railway": "#FF0000"`
|
| 266 |
+
2. Use in code: `THEME['railway']`
|
| 267 |
+
3. Add fallback in `load_theme()` default dict
|
| 268 |
+
|
| 269 |
+
### Typography Positioning
|
| 270 |
+
|
| 271 |
+
All text uses `transform=ax.transAxes` (0-1 normalized coordinates):
|
| 272 |
+
```
|
| 273 |
+
y=0.14 City name (spaced letters)
|
| 274 |
+
y=0.125 Decorative line
|
| 275 |
+
y=0.10 Country name
|
| 276 |
+
y=0.07 Coordinates
|
| 277 |
+
y=0.02 Attribution (bottom-right)
|
| 278 |
+
```
|
| 279 |
+
|
| 280 |
+
### Useful OSMnx Patterns
|
| 281 |
+
|
| 282 |
+
```python
|
| 283 |
+
# Get all buildings
|
| 284 |
+
buildings = ox.features_from_point(point, tags={'building': True}, dist=dist)
|
| 285 |
+
|
| 286 |
+
# Get specific amenities
|
| 287 |
+
cafes = ox.features_from_point(point, tags={'amenity': 'cafe'}, dist=dist)
|
| 288 |
+
|
| 289 |
+
# Different network types
|
| 290 |
+
G = ox.graph_from_point(point, dist=dist, network_type='drive') # roads only
|
| 291 |
+
G = ox.graph_from_point(point, dist=dist, network_type='bike') # bike paths
|
| 292 |
+
G = ox.graph_from_point(point, dist=dist, network_type='walk') # pedestrian
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
### Performance Tips
|
| 296 |
+
|
| 297 |
+
- Large `dist` values (>20km) = slow downloads + memory heavy
|
| 298 |
+
- Cache coordinates locally to avoid Nominatim rate limits
|
| 299 |
+
- Use `network_type='drive'` instead of `'all'` for faster renders
|
| 300 |
+
- Reduce `dpi` from 300 to 150 for quick previews
|
app.py
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Gradio Web Interface for City Map Poster Generator
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import tempfile
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
|
| 12 |
+
# Import from the main module
|
| 13 |
+
from cities_data import (
|
| 14 |
+
CITIES,
|
| 15 |
+
get_countries,
|
| 16 |
+
get_provinces,
|
| 17 |
+
get_cities,
|
| 18 |
+
get_city_full_name,
|
| 19 |
+
translate,
|
| 20 |
+
get_country_key
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# --- Constants ---
|
| 25 |
+
THEMES_DIR = "themes"
|
| 26 |
+
FONTS_DIR = "fonts"
|
| 27 |
+
POSTERS_DIR = "posters"
|
| 28 |
+
|
| 29 |
+
# Layer Constants
|
| 30 |
+
LAYERS_EN = ["Motorway", "Primary Roads", "Secondary Roads", "Water", "Parks"]
|
| 31 |
+
LAYERS_CN = ["高速公路", "主干道", "次干道", "水域", "公园"]
|
| 32 |
+
LAYER_KEYS = ["motorway", "primary", "secondary", "water", "parks"]
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def get_available_themes():
|
| 37 |
+
"""Scans the themes directory and returns a list of available theme names."""
|
| 38 |
+
if not os.path.exists(THEMES_DIR):
|
| 39 |
+
return []
|
| 40 |
+
|
| 41 |
+
themes = []
|
| 42 |
+
for file in sorted(os.listdir(THEMES_DIR)):
|
| 43 |
+
if file.endswith('.json'):
|
| 44 |
+
theme_name = file[:-5]
|
| 45 |
+
themes.append(theme_name)
|
| 46 |
+
return themes
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_theme_choices(lang='en'):
|
| 50 |
+
"""Return a list of (Display Name, Internal Name) tuples for themes."""
|
| 51 |
+
internal_names = get_available_themes()
|
| 52 |
+
choices = []
|
| 53 |
+
|
| 54 |
+
for internal_name in internal_names:
|
| 55 |
+
theme_info = load_theme_info(internal_name)
|
| 56 |
+
if theme_info:
|
| 57 |
+
proper_name = theme_info.get("name", internal_name)
|
| 58 |
+
display_name = translate(proper_name, lang)
|
| 59 |
+
choices.append((display_name, internal_name))
|
| 60 |
+
else:
|
| 61 |
+
choices.append((internal_name, internal_name))
|
| 62 |
+
|
| 63 |
+
return choices
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def load_theme_info(theme_name):
|
| 67 |
+
"""Load theme details for preview."""
|
| 68 |
+
theme_file = os.path.join(THEMES_DIR, f"{theme_name}.json")
|
| 69 |
+
if os.path.exists(theme_file):
|
| 70 |
+
with open(theme_file, 'r') as f:
|
| 71 |
+
return json.load(f)
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_theme_preview_html(theme_name, lang='en'):
|
| 76 |
+
"""Generate HTML preview for a theme."""
|
| 77 |
+
theme = load_theme_info(theme_name)
|
| 78 |
+
lang_code = 'en' if lang == "English" else 'cn'
|
| 79 |
+
|
| 80 |
+
if not theme:
|
| 81 |
+
return f"<p>{'Theme load failed' if lang_code == 'en' else '主题加载失败'}</p>"
|
| 82 |
+
|
| 83 |
+
# Translated labels
|
| 84 |
+
labels = {
|
| 85 |
+
"bg": "Background" if lang_code == "en" else "背景",
|
| 86 |
+
"text": "Text" if lang_code == "en" else "文字",
|
| 87 |
+
"motorway": "Motorway" if lang_code == "en" else "高速公路",
|
| 88 |
+
"primary": "Primary Road" if lang_code == "en" else "主干道",
|
| 89 |
+
"secondary": "Secondary Road" if lang_code == "en" else "次干道",
|
| 90 |
+
"water": "Water" if lang_code == "en" else "水域",
|
| 91 |
+
"parks": "Parks" if lang_code == "en" else "公园",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
# Create color swatches
|
| 95 |
+
colors = [
|
| 96 |
+
(labels["bg"], theme.get("bg", "#FFFFFF")),
|
| 97 |
+
(labels["text"], theme.get("text", "#000000")),
|
| 98 |
+
(labels["motorway"], theme.get("road_motorway", "#000000")),
|
| 99 |
+
(labels["primary"], theme.get("road_primary", "#333333")),
|
| 100 |
+
(labels["secondary"], theme.get("road_secondary", "#666666")),
|
| 101 |
+
(labels["water"], theme.get("water", "#C0C0C0")),
|
| 102 |
+
(labels["parks"], theme.get("parks", "#F0F0F0")),
|
| 103 |
+
]
|
| 104 |
+
|
| 105 |
+
display_name = translate(theme.get('name', theme_name), lang_code)
|
| 106 |
+
description = theme.get('description', '')
|
| 107 |
+
# Optional: translate description if we really want to, but might be too much work
|
| 108 |
+
|
| 109 |
+
html = f"""
|
| 110 |
+
<div style="padding: 12px; background: {theme.get('bg', '#FFFFFF')}; border-radius: 8px; border: 1px solid #ddd;">
|
| 111 |
+
<h4 style="color: {theme.get('text', '#000000')}; margin: 0 0 8px 0; font-size: 14px;">
|
| 112 |
+
{display_name}
|
| 113 |
+
</h4>
|
| 114 |
+
<p style="color: {theme.get('text', '#000000')}; opacity: 0.7; margin: 0 0 12px 0; font-size: 12px;">
|
| 115 |
+
{description}
|
| 116 |
+
</p>
|
| 117 |
+
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
for label, color in colors:
|
| 121 |
+
html += f"""
|
| 122 |
+
<div style="display: flex; align-items: center; gap: 4px;">
|
| 123 |
+
<div style="width: 20px; height: 20px; background: {color}; border-radius: 4px; border: 1px solid #ccc;"></div>
|
| 124 |
+
<span style="font-size: 10px; color: {theme.get('text', '#000')}; opacity: 0.8;">{label}</span>
|
| 125 |
+
</div>
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
html += "</div></div>"
|
| 129 |
+
return html
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def generate_poster(
|
| 133 |
+
country_display,
|
| 134 |
+
province,
|
| 135 |
+
city_dropdown,
|
| 136 |
+
theme_name,
|
| 137 |
+
distance,
|
| 138 |
+
width,
|
| 139 |
+
height,
|
| 140 |
+
output_format,
|
| 141 |
+
no_crop,
|
| 142 |
+
poster_lang,
|
| 143 |
+
layers_selection,
|
| 144 |
+
progress=gr.Progress()
|
| 145 |
+
):
|
| 146 |
+
"""
|
| 147 |
+
Generate the map poster with given parameters.
|
| 148 |
+
"""
|
| 149 |
+
# Import here to avoid circular imports and ensure THEME is set correctly
|
| 150 |
+
import create_map_poster as cmp
|
| 151 |
+
|
| 152 |
+
# Decode layer selection
|
| 153 |
+
show_motorway = any(x in ["Motorway", "高速公路"] for x in layers_selection)
|
| 154 |
+
show_primary = any(x in ["Primary Roads", "��干道"] for x in layers_selection)
|
| 155 |
+
show_secondary = any(x in ["Secondary Roads", "次干道"] for x in layers_selection)
|
| 156 |
+
show_water = any(x in ["Water", "水域"] for x in layers_selection)
|
| 157 |
+
show_parks = any(x in ["Parks", "公园"] for x in layers_selection)
|
| 158 |
+
|
| 159 |
+
# Parse country
|
| 160 |
+
selected_country = get_country_key(country_display)
|
| 161 |
+
|
| 162 |
+
# Use dropdown selection
|
| 163 |
+
selected_city = city_dropdown
|
| 164 |
+
# If the selected_city is translated, we need the original for geocoding
|
| 165 |
+
# But get_coordinates typically handles common translations or we can just pass the dropdown value
|
| 166 |
+
# Actually, selected_city from dropdown might be "Guangzhou" or "广州"
|
| 167 |
+
# cmp.get_coordinates(selected_city, selected_country)
|
| 168 |
+
# selected_country is now standardized (e.g. "中国" or "USA")
|
| 169 |
+
|
| 170 |
+
if not selected_city:
|
| 171 |
+
return None, "❌ 请选择城市名称"
|
| 172 |
+
|
| 173 |
+
if not selected_country:
|
| 174 |
+
return None, "❌ 请选择国家"
|
| 175 |
+
|
| 176 |
+
# Determine display names based on poster_lang
|
| 177 |
+
lang_code = 'en' if poster_lang == "English" else 'cn'
|
| 178 |
+
display_city = translate(selected_city, lang_code)
|
| 179 |
+
display_country = translate(selected_country, lang_code)
|
| 180 |
+
|
| 181 |
+
progress(0.1, desc="正在加载主题...")
|
| 182 |
+
|
| 183 |
+
# Load theme
|
| 184 |
+
cmp.THEME = cmp.load_theme(theme_name)
|
| 185 |
+
|
| 186 |
+
progress(0.2, desc="正在获取坐标...")
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# We search coordinates using the selection
|
| 190 |
+
# CMP might need the "original" names if OSM behaves better with them
|
| 191 |
+
# For China, "广州" is better than "Guangzhou" for geopy sometimes, but geopy is usually good.
|
| 192 |
+
# Let's ensure we use the 'original' internal key if possible for best OSM matching
|
| 193 |
+
# However, for cities, we don't have a strict key map like for countries.
|
| 194 |
+
# We can try to translate to CN if it's a Chinese city.
|
| 195 |
+
search_city = selected_city
|
| 196 |
+
search_country = selected_country
|
| 197 |
+
|
| 198 |
+
coords = cmp.get_coordinates(search_city, search_country)
|
| 199 |
+
except Exception as e:
|
| 200 |
+
return None, f"❌ 无法找到城市坐标: {str(e)}"
|
| 201 |
+
|
| 202 |
+
progress(0.3, desc="正在生成海报...")
|
| 203 |
+
|
| 204 |
+
# Generate output filename (using English/Slugified names)
|
| 205 |
+
en_city = translate(selected_city, 'en')
|
| 206 |
+
output_file = cmp.generate_output_filename(en_city, theme_name, output_format)
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
# Wrap the generator to yield status updates and final result
|
| 210 |
+
for status in cmp.create_poster(
|
| 211 |
+
display_city, # Pass translated names for display
|
| 212 |
+
display_country,
|
| 213 |
+
coords,
|
| 214 |
+
distance,
|
| 215 |
+
output_file,
|
| 216 |
+
output_format,
|
| 217 |
+
width=width,
|
| 218 |
+
height=height,
|
| 219 |
+
no_crop=no_crop,
|
| 220 |
+
show_motorway=show_motorway,
|
| 221 |
+
show_primary=show_primary,
|
| 222 |
+
show_secondary=show_secondary,
|
| 223 |
+
show_water=show_water,
|
| 224 |
+
show_parks=show_parks
|
| 225 |
+
):
|
| 226 |
+
# Translate common status messages if possible
|
| 227 |
+
status_display = status
|
| 228 |
+
if lang_code == 'cn':
|
| 229 |
+
if "Downloading street network" in status: status_display = "正在下载街道数据..."
|
| 230 |
+
elif "Downloading features" in status: status_display = "正在下载水域和公园数据..."
|
| 231 |
+
elif "Rendering map" in status: status_display = "正在渲染地图..."
|
| 232 |
+
elif "Applying road styles" in status: status_display = "正在应用样式..."
|
| 233 |
+
elif "Saving to" in status: status_display = "正在保存海报..."
|
| 234 |
+
elif "Done" in status: status_display = "完成!"
|
| 235 |
+
|
| 236 |
+
yield None, f"⏳ {status_display}"
|
| 237 |
+
|
| 238 |
+
progress(1.0, desc="完成!")
|
| 239 |
+
|
| 240 |
+
yield output_file, f"✅ 海报生成成功!保存至: {output_file}"
|
| 241 |
+
|
| 242 |
+
except Exception as e:
|
| 243 |
+
import traceback
|
| 244 |
+
traceback.print_exc()
|
| 245 |
+
yield None, f"❌ 生成失败: {str(e)}"
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def update_provinces(country, lang='en'):
|
| 249 |
+
"""Update province dropdown based on country selection and language."""
|
| 250 |
+
# Map back to internal key for data lookup
|
| 251 |
+
lang_code = 'en' if lang == "English" else 'cn'
|
| 252 |
+
provinces = get_provinces(country, lang_code)
|
| 253 |
+
if provinces:
|
| 254 |
+
return gr.update(choices=provinces, value=provinces[0], visible=True)
|
| 255 |
+
return gr.update(choices=[], value=None, visible=False)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def update_cities(country, province, lang='en'):
|
| 259 |
+
"""Update city dropdown based on province selection and language."""
|
| 260 |
+
lang_code = 'en' if lang == "English" else 'cn'
|
| 261 |
+
cities = get_cities(country, province, lang_code)
|
| 262 |
+
if cities:
|
| 263 |
+
return gr.update(choices=cities, value=cities[0])
|
| 264 |
+
return gr.update(choices=[], value=None)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def on_theme_change(theme_name, lang='en'):
|
| 269 |
+
"""Update theme preview when theme changes."""
|
| 270 |
+
return get_theme_preview_html(theme_name, lang)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# --- Build Gradio Interface ---
|
| 274 |
+
def create_interface():
|
| 275 |
+
"""Create and return the Gradio interface."""
|
| 276 |
+
|
| 277 |
+
# Get initial data (Default English)
|
| 278 |
+
default_lang_code = "en"
|
| 279 |
+
default_lang_radio = "English"
|
| 280 |
+
countries = get_countries(default_lang_code)
|
| 281 |
+
theme_choices = get_theme_choices(default_lang_code)
|
| 282 |
+
|
| 283 |
+
default_country = "China" if "China" in countries else countries[0]
|
| 284 |
+
default_provinces = get_provinces(default_country, default_lang_code)
|
| 285 |
+
default_province = default_provinces[0] if default_provinces else None
|
| 286 |
+
default_cities = get_cities(default_country, default_province, default_lang_code) if default_province else []
|
| 287 |
+
default_city = default_cities[0] if default_cities else None
|
| 288 |
+
default_theme = theme_choices[0][1] if theme_choices else "feature_based"
|
| 289 |
+
default_layers = LAYERS_EN
|
| 290 |
+
|
| 291 |
+
with gr.Blocks(
|
| 292 |
+
title="城市地图海报生成器",
|
| 293 |
+
) as demo:
|
| 294 |
+
|
| 295 |
+
# Header
|
| 296 |
+
gr.HTML("""
|
| 297 |
+
<div class="header-title">城市地图海报生成器</div>
|
| 298 |
+
<div class="header-subtitle">选择任意城市,自定义主题风格,生成精美地图海报</div>
|
| 299 |
+
""")
|
| 300 |
+
|
| 301 |
+
with gr.Row():
|
| 302 |
+
# Left Column - Controls
|
| 303 |
+
with gr.Column(scale=1):
|
| 304 |
+
|
| 305 |
+
# City Selection Section
|
| 306 |
+
gr.HTML('<div class="section-title">📍 城市选择</div>')
|
| 307 |
+
|
| 308 |
+
lang_radio = gr.Radio(
|
| 309 |
+
choices=["English", "Chinese"],
|
| 310 |
+
value="English",
|
| 311 |
+
label="海报语言",
|
| 312 |
+
info="选择海报及界面的显示语言"
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
country_dropdown = gr.Dropdown(
|
| 316 |
+
choices=countries,
|
| 317 |
+
value=default_country,
|
| 318 |
+
label="选择国家",
|
| 319 |
+
interactive=True
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
province_dropdown = gr.Dropdown(
|
| 323 |
+
choices=default_provinces,
|
| 324 |
+
value=default_province,
|
| 325 |
+
label="选择省份/州",
|
| 326 |
+
interactive=True
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
city_dropdown = gr.Dropdown(
|
| 330 |
+
choices=default_cities,
|
| 331 |
+
value=default_city,
|
| 332 |
+
label="选择城市",
|
| 333 |
+
interactive=True
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
gr.HTML("<hr style='margin: 20px 0; border-color: #eee;'>")
|
| 337 |
+
|
| 338 |
+
# Theme Section
|
| 339 |
+
gr.HTML('<div class="section-title">🎨 主题风格</div>')
|
| 340 |
+
|
| 341 |
+
theme_dropdown = gr.Dropdown(
|
| 342 |
+
choices=theme_choices,
|
| 343 |
+
value=default_theme,
|
| 344 |
+
label="选择主题",
|
| 345 |
+
interactive=True
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
theme_preview = gr.HTML(
|
| 349 |
+
value=get_theme_preview_html(default_theme, default_lang_radio),
|
| 350 |
+
label="主题预览"
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
gr.HTML("<hr style='margin: 20px 0; border-color: #eee;'>")
|
| 354 |
+
|
| 355 |
+
# Parameters Section
|
| 356 |
+
gr.HTML('<div class="section-title">⚙️ 参数设置</div>')
|
| 357 |
+
|
| 358 |
+
distance_slider = gr.Slider(
|
| 359 |
+
minimum=4000,
|
| 360 |
+
maximum=30000,
|
| 361 |
+
value=10000,
|
| 362 |
+
step=1000,
|
| 363 |
+
label="地图范围 (米)",
|
| 364 |
+
info="4000-6000: 小城区 | 8000-12000: 中等城市 | 15000+: 大都市 (范围越大生成越慢)"
|
| 365 |
+
)
|
| 366 |
+
|
| 367 |
+
with gr.Row():
|
| 368 |
+
width_input = gr.Number(
|
| 369 |
+
value=12.0,
|
| 370 |
+
label="宽度 (英寸)",
|
| 371 |
+
minimum=6,
|
| 372 |
+
maximum=24
|
| 373 |
+
)
|
| 374 |
+
height_input = gr.Number(
|
| 375 |
+
value=16.0,
|
| 376 |
+
label="高度 (英寸)",
|
| 377 |
+
minimum=8,
|
| 378 |
+
maximum=32
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
format_radio = gr.Radio(
|
| 382 |
+
choices=["png", "svg", "pdf"],
|
| 383 |
+
value="png",
|
| 384 |
+
label="输出格式",
|
| 385 |
+
info="PNG: 适合打印 | SVG: 矢量图 | PDF: 文档"
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
no_crop_checkbox = gr.Checkbox(
|
| 389 |
+
value=False,
|
| 390 |
+
label="保留边距 (不裁剪)",
|
| 391 |
+
info="勾选后保留海报边缘背景"
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
layers_checkbox = gr.CheckboxGroup(
|
| 395 |
+
choices=LAYERS_EN,
|
| 396 |
+
value=LAYERS_EN,
|
| 397 |
+
label="图层显示",
|
| 398 |
+
info="选择需要显示的地图元素"
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
# Generate Button
|
| 402 |
+
generate_btn = gr.Button(
|
| 403 |
+
"🚀 生成海��",
|
| 404 |
+
variant="primary",
|
| 405 |
+
size="lg"
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
+
# Right Column - Output
|
| 409 |
+
with gr.Column(scale=1):
|
| 410 |
+
gr.HTML('<div class="section-title">🖼️ 生成结果</div>')
|
| 411 |
+
|
| 412 |
+
output_image = gr.Image(
|
| 413 |
+
label="海报预览",
|
| 414 |
+
type="filepath",
|
| 415 |
+
elem_classes=["output-image"],
|
| 416 |
+
height=600,
|
| 417 |
+
interactive=False
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
output_status = gr.Textbox(
|
| 421 |
+
label="状态",
|
| 422 |
+
interactive=False
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
download_btn = gr.DownloadButton(
|
| 426 |
+
label="📥 下载海报",
|
| 427 |
+
visible=False
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# --- Event Handlers ---
|
| 431 |
+
|
| 432 |
+
# Language change -> update all dropdowns
|
| 433 |
+
def on_lang_change(lang, current_theme, current_layers):
|
| 434 |
+
lang_code = 'en' if lang == "English" else 'cn'
|
| 435 |
+
new_countries = get_countries(lang_code)
|
| 436 |
+
|
| 437 |
+
# Find closest match for current selections if possible
|
| 438 |
+
default_co = "China" if lang == "English" else "中国"
|
| 439 |
+
if default_co not in new_countries:
|
| 440 |
+
default_co = new_countries[0]
|
| 441 |
+
|
| 442 |
+
new_provinces = get_provinces(default_co, lang_code)
|
| 443 |
+
default_pr = new_provinces[0] if new_provinces else None
|
| 444 |
+
|
| 445 |
+
new_cities = get_cities(default_co, default_pr, lang_code) if default_pr else []
|
| 446 |
+
default_ci = new_cities[0] if new_cities else None
|
| 447 |
+
|
| 448 |
+
# Themes
|
| 449 |
+
new_theme_choices = get_theme_choices(lang_code)
|
| 450 |
+
new_preview = get_theme_preview_html(current_theme, lang)
|
| 451 |
+
|
| 452 |
+
# Layers
|
| 453 |
+
# Map current selection to keys then to new language
|
| 454 |
+
map_en_to_key = dict(zip(LAYERS_EN, LAYER_KEYS))
|
| 455 |
+
map_cn_to_key = dict(zip(LAYERS_CN, LAYER_KEYS))
|
| 456 |
+
map_key_to_en = dict(zip(LAYER_KEYS, LAYERS_EN))
|
| 457 |
+
map_key_to_cn = dict(zip(LAYER_KEYS, LAYERS_CN))
|
| 458 |
+
|
| 459 |
+
current_keys = []
|
| 460 |
+
for x in current_layers:
|
| 461 |
+
if x in map_en_to_key: current_keys.append(map_en_to_key[x])
|
| 462 |
+
elif x in map_cn_to_key: current_keys.append(map_cn_to_key[x])
|
| 463 |
+
|
| 464 |
+
target_map = map_key_to_en if lang == "English" else map_key_to_cn
|
| 465 |
+
new_layer_choices = LAYERS_EN if lang == "English" else LAYERS_CN
|
| 466 |
+
new_layer_values = [target_map[k] for k in current_keys if k in target_map]
|
| 467 |
+
|
| 468 |
+
return (
|
| 469 |
+
gr.update(choices=new_countries, value=default_co),
|
| 470 |
+
gr.update(choices=new_provinces, value=default_pr),
|
| 471 |
+
gr.update(choices=new_cities, value=default_ci),
|
| 472 |
+
gr.update(choices=new_theme_choices),
|
| 473 |
+
new_preview,
|
| 474 |
+
gr.update(choices=new_layer_choices, value=new_layer_values, label="Layers" if lang == "English" else "图层显示")
|
| 475 |
+
)
|
| 476 |
+
|
| 477 |
+
lang_radio.change(
|
| 478 |
+
fn=on_lang_change,
|
| 479 |
+
inputs=[lang_radio, theme_dropdown, layers_checkbox],
|
| 480 |
+
outputs=[country_dropdown, province_dropdown, city_dropdown, theme_dropdown, theme_preview, layers_checkbox]
|
| 481 |
+
)
|
| 482 |
+
|
| 483 |
+
# Country change -> update provinces
|
| 484 |
+
country_dropdown.change(
|
| 485 |
+
fn=update_provinces,
|
| 486 |
+
inputs=[country_dropdown, lang_radio],
|
| 487 |
+
outputs=[province_dropdown]
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
# Province change -> update cities
|
| 491 |
+
province_dropdown.change(
|
| 492 |
+
fn=update_cities,
|
| 493 |
+
inputs=[country_dropdown, province_dropdown, lang_radio],
|
| 494 |
+
outputs=[city_dropdown]
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
# Theme change -> update preview
|
| 498 |
+
theme_dropdown.change(
|
| 499 |
+
fn=on_theme_change,
|
| 500 |
+
inputs=[theme_dropdown, lang_radio],
|
| 501 |
+
outputs=[theme_preview]
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Generate button click
|
| 505 |
+
def on_generate_complete(filepath, status):
|
| 506 |
+
"""Handle generate completion - show download button if successful."""
|
| 507 |
+
if filepath and os.path.exists(filepath):
|
| 508 |
+
return filepath, status, gr.update(visible=True, value=filepath)
|
| 509 |
+
return filepath, status, gr.update(visible=False)
|
| 510 |
+
|
| 511 |
+
generate_btn.click(
|
| 512 |
+
fn=generate_poster,
|
| 513 |
+
inputs=[
|
| 514 |
+
country_dropdown,
|
| 515 |
+
province_dropdown,
|
| 516 |
+
city_dropdown,
|
| 517 |
+
theme_dropdown,
|
| 518 |
+
distance_slider,
|
| 519 |
+
width_input,
|
| 520 |
+
height_input,
|
| 521 |
+
format_radio,
|
| 522 |
+
no_crop_checkbox,
|
| 523 |
+
lang_radio,
|
| 524 |
+
layers_checkbox
|
| 525 |
+
],
|
| 526 |
+
outputs=[output_image, output_status]
|
| 527 |
+
).then(
|
| 528 |
+
fn=on_generate_complete,
|
| 529 |
+
inputs=[output_image, output_status],
|
| 530 |
+
outputs=[output_image, output_status, download_btn]
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
# Footer
|
| 534 |
+
gr.HTML("""
|
| 535 |
+
<div style="text-align: center; margin-top: 24px; padding: 12px; color: #888; font-size: 12px;">
|
| 536 |
+
<p>数据来源: © OpenStreetMap contributors | 地理编码: Nominatim</p>
|
| 537 |
+
<p>提示: 生成大范围地图可能需要较长时间,请耐心等待</p>
|
| 538 |
+
</div>
|
| 539 |
+
""")
|
| 540 |
+
|
| 541 |
+
return demo
|
| 542 |
+
|
| 543 |
+
|
| 544 |
+
# --- Main Entry ---
|
| 545 |
+
if __name__ == "__main__":
|
| 546 |
+
demo = create_interface()
|
| 547 |
+
demo.launch(
|
| 548 |
+
server_name="0.0.0.0",
|
| 549 |
+
server_port=7860,
|
| 550 |
+
share=False,
|
| 551 |
+
show_error=True,
|
| 552 |
+
theme=gr.themes.Default(),
|
| 553 |
+
css="""
|
| 554 |
+
.header-title {
|
| 555 |
+
text-align: center;
|
| 556 |
+
font-size: 2em;
|
| 557 |
+
font-weight: bold;
|
| 558 |
+
margin-bottom: 0.5em;
|
| 559 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 560 |
+
-webkit-background-clip: text;
|
| 561 |
+
-webkit-text-fill-color: transparent;
|
| 562 |
+
background-clip: text;
|
| 563 |
+
}
|
| 564 |
+
.header-subtitle {
|
| 565 |
+
text-align: center;
|
| 566 |
+
color: #666;
|
| 567 |
+
margin-bottom: 1.5em;
|
| 568 |
+
}
|
| 569 |
+
.section-title {
|
| 570 |
+
font-weight: 600;
|
| 571 |
+
font-size: 1.1em;
|
| 572 |
+
margin-bottom: 0.5em;
|
| 573 |
+
color: #333;
|
| 574 |
+
}
|
| 575 |
+
.output-image {
|
| 576 |
+
border-radius: 12px;
|
| 577 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
| 578 |
+
}
|
| 579 |
+
"""
|
| 580 |
+
)
|
cities_data.py
ADDED
|
@@ -0,0 +1,996 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
Cities database with hierarchical structure for cascading selection.
|
| 4 |
+
Format: Country -> Province/State -> Cities
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
CITIES = {
|
| 8 |
+
"中国": {
|
| 9 |
+
"北京": ["北京"],
|
| 10 |
+
"上海": ["上海"],
|
| 11 |
+
"天津": ["天津"],
|
| 12 |
+
"重庆": ["重庆"],
|
| 13 |
+
"广东": [
|
| 14 |
+
"广州",
|
| 15 |
+
"深圳",
|
| 16 |
+
"东莞",
|
| 17 |
+
"佛山",
|
| 18 |
+
"珠海",
|
| 19 |
+
"惠州",
|
| 20 |
+
"中山",
|
| 21 |
+
"汕头",
|
| 22 |
+
"湛江",
|
| 23 |
+
"江门",
|
| 24 |
+
],
|
| 25 |
+
"浙江": ["杭州", "宁波", "温州", "绍兴", "嘉兴", "金华", "台州", "湖州"],
|
| 26 |
+
"江苏": ["南京", "苏州", "无锡", "常州", "南通", "徐州", "扬州", "镇江"],
|
| 27 |
+
"山东": ["济南", "青岛", "烟台", "威海", "潍坊", "临沂", "济宁", "淄博"],
|
| 28 |
+
"四川": ["成都", "绵阳", "德阳", "宜宾", "泸州", "南充", "乐山"],
|
| 29 |
+
"湖北": ["武汉", "宜昌", "襄阳", "荆州", "黄石", "十堰"],
|
| 30 |
+
"湖南": ["长沙", "株洲", "湘潭", "衡阳", "岳阳", "常德"],
|
| 31 |
+
"河南": ["郑州", "洛阳", "开封", "新乡", "安阳", "焦作"],
|
| 32 |
+
"河北": ["石家庄", "唐山", "秦皇岛", "邯郸", "保定", "沧州"],
|
| 33 |
+
"福建": ["福州", "厦门", "泉州", "漳州", "莆田", "龙岩"],
|
| 34 |
+
"安徽": ["合肥", "芜湖", "蚌埠", "马鞍山", "安庆", "黄山"],
|
| 35 |
+
"江西": ["南昌", "九江", "景德镇", "赣州", "上饶", "吉安"],
|
| 36 |
+
"陕西": ["西安", "咸阳", "宝鸡", "延安", "榆林", "汉中"],
|
| 37 |
+
"山西": ["太原", "大同", "临汾", "运城", "晋中", "长治"],
|
| 38 |
+
"辽宁": ["沈阳", "大连", "鞍山", "抚顺", "本溪", "营口"],
|
| 39 |
+
"吉林": ["长春", "吉林", "四平", "通化", "延边"],
|
| 40 |
+
"黑龙江": ["哈尔滨", "齐齐哈尔", "牡丹江", "佳木斯", "大庆"],
|
| 41 |
+
"云南": ["昆明", "大理", "丽江", "西双版纳", "曲靖"],
|
| 42 |
+
"贵州": ["贵阳", "遵义", "安顺", "六盘水", "毕节"],
|
| 43 |
+
"甘肃": ["兰州", "天水", "嘉峪关", "酒泉", "张掖"],
|
| 44 |
+
"海南": ["海口", "三亚", "儋州", "琼海"],
|
| 45 |
+
"广西": ["南宁", "桂林", "柳州", "梧州", "北海", "玉林"],
|
| 46 |
+
"内蒙古": ["呼和浩特", "包头", "鄂尔多斯", "赤峰", "呼伦贝尔"],
|
| 47 |
+
"新疆": ["乌鲁木齐", "喀什", "吐鲁番", "阿克苏", "伊宁"],
|
| 48 |
+
"西藏": ["拉萨", "日喀则", "林芝", "昌都"],
|
| 49 |
+
"宁夏": ["银川", "石嘴山", "吴忠", "固原"],
|
| 50 |
+
"青海": ["西宁", "格尔木", "玉树", "海东"],
|
| 51 |
+
"香港": ["香港"],
|
| 52 |
+
"澳门": ["澳门"],
|
| 53 |
+
"台湾": ["台北", "高雄", "台中", "台南", "新竹", "基隆"],
|
| 54 |
+
},
|
| 55 |
+
"USA": {
|
| 56 |
+
"California": [
|
| 57 |
+
"Los Angeles",
|
| 58 |
+
"San Francisco",
|
| 59 |
+
"San Diego",
|
| 60 |
+
"San Jose",
|
| 61 |
+
"Sacramento",
|
| 62 |
+
"Oakland",
|
| 63 |
+
"Fresno",
|
| 64 |
+
],
|
| 65 |
+
"New York": ["New York City", "Buffalo", "Rochester", "Albany", "Syracuse"],
|
| 66 |
+
"Texas": [
|
| 67 |
+
"Houston",
|
| 68 |
+
"Dallas",
|
| 69 |
+
"Austin",
|
| 70 |
+
"San Antonio",
|
| 71 |
+
"Fort Worth",
|
| 72 |
+
"El Paso",
|
| 73 |
+
],
|
| 74 |
+
"Florida": ["Miami", "Orlando", "Tampa", "Jacksonville", "Fort Lauderdale"],
|
| 75 |
+
"Illinois": ["Chicago", "Aurora", "Naperville", "Rockford"],
|
| 76 |
+
"Pennsylvania": ["Philadelphia", "Pittsburgh", "Harrisburg"],
|
| 77 |
+
"Arizona": ["Phoenix", "Tucson", "Mesa", "Scottsdale"],
|
| 78 |
+
"Nevada": ["Las Vegas", "Reno", "Henderson"],
|
| 79 |
+
"Washington": ["Seattle", "Tacoma", "Spokane", "Bellevue"],
|
| 80 |
+
"Massachusetts": ["Boston", "Cambridge", "Worcester"],
|
| 81 |
+
"Colorado": ["Denver", "Colorado Springs", "Aurora", "Boulder"],
|
| 82 |
+
"Georgia": ["Atlanta", "Savannah", "Augusta"],
|
| 83 |
+
"North Carolina": ["Charlotte", "Raleigh", "Durham"],
|
| 84 |
+
"Michigan": ["Detroit", "Grand Rapids", "Ann Arbor"],
|
| 85 |
+
"Oregon": ["Portland", "Salem", "Eugene"],
|
| 86 |
+
"District of Columbia": ["Washington"],
|
| 87 |
+
"Hawaii": ["Honolulu"],
|
| 88 |
+
},
|
| 89 |
+
"Japan": {
|
| 90 |
+
"関東": ["Tokyo", "Yokohama", "Kawasaki", "Saitama", "Chiba"],
|
| 91 |
+
"関西": ["Osaka", "Kyoto", "Kobe", "Nara"],
|
| 92 |
+
"中部": ["Nagoya", "Kanazawa", "Shizuoka"],
|
| 93 |
+
"北海道": ["Sapporo", "Hakodate", "Asahikawa"],
|
| 94 |
+
"九州": ["Fukuoka", "Nagasaki", "Kumamoto", "Kagoshima"],
|
| 95 |
+
"東北": ["Sendai", "Aomori", "Akita"],
|
| 96 |
+
"中国": ["Hiroshima", "Okayama"],
|
| 97 |
+
"四国": ["Matsuyama", "Takamatsu"],
|
| 98 |
+
"沖縄": ["Naha", "Okinawa"],
|
| 99 |
+
},
|
| 100 |
+
"UK": {
|
| 101 |
+
"England": [
|
| 102 |
+
"London",
|
| 103 |
+
"Manchester",
|
| 104 |
+
"Birmingham",
|
| 105 |
+
"Liverpool",
|
| 106 |
+
"Leeds",
|
| 107 |
+
"Bristol",
|
| 108 |
+
"Sheffield",
|
| 109 |
+
"Newcastle",
|
| 110 |
+
"Nottingham",
|
| 111 |
+
"Cambridge",
|
| 112 |
+
"Oxford",
|
| 113 |
+
],
|
| 114 |
+
"Scotland": ["Edinburgh", "Glasgow", "Aberdeen", "Dundee"],
|
| 115 |
+
"Wales": ["Cardiff", "Swansea", "Newport"],
|
| 116 |
+
"Northern Ireland": ["Belfast", "Londonderry"],
|
| 117 |
+
},
|
| 118 |
+
"France": {
|
| 119 |
+
"Île-de-France": ["Paris"],
|
| 120 |
+
"Provence-Alpes-Côte d'Azur": ["Marseille", "Nice", "Cannes", "Toulon"],
|
| 121 |
+
"Auvergne-Rhône-Alpes": ["Lyon", "Grenoble", "Saint-Étienne"],
|
| 122 |
+
"Nouvelle-Aquitaine": ["Bordeaux", "Limoges"],
|
| 123 |
+
"Occitanie": ["Toulouse", "Montpellier", "Nîmes"],
|
| 124 |
+
"Hauts-de-France": ["Lille", "Amiens"],
|
| 125 |
+
"Grand Est": ["Strasbourg", "Reims", "Nancy", "Metz"],
|
| 126 |
+
"Normandie": ["Rouen", "Le Havre", "Caen"],
|
| 127 |
+
"Bretagne": ["Rennes", "Brest", "Nantes"],
|
| 128 |
+
},
|
| 129 |
+
"Germany": {
|
| 130 |
+
"Bayern": ["Munich", "Nuremberg", "Augsburg"],
|
| 131 |
+
"Berlin": ["Berlin"],
|
| 132 |
+
"Hamburg": ["Hamburg"],
|
| 133 |
+
"Hessen": ["Frankfurt", "Wiesbaden"],
|
| 134 |
+
"Baden-Württemberg": ["Stuttgart", "Heidelberg", "Freiburg", "Karlsruhe"],
|
| 135 |
+
"Nordrhein-Westfalen": ["Cologne", "Düsseldorf", "Dortmund", "Essen", "Bonn"],
|
| 136 |
+
"Niedersachsen": ["Hanover", "Braunschweig", "Oldenburg"],
|
| 137 |
+
"Sachsen": ["Dresden", "Leipzig"],
|
| 138 |
+
"Brandenburg": ["Potsdam"],
|
| 139 |
+
},
|
| 140 |
+
"Italy": {
|
| 141 |
+
"Lazio": ["Rome"],
|
| 142 |
+
"Lombardia": ["Milan", "Bergamo", "Brescia"],
|
| 143 |
+
"Veneto": ["Venice", "Verona", "Padua"],
|
| 144 |
+
"Toscana": ["Florence", "Pisa", "Siena"],
|
| 145 |
+
"Campania": ["Naples", "Salerno"],
|
| 146 |
+
"Piemonte": ["Turin", "Genoa"],
|
| 147 |
+
"Emilia-Romagna": ["Bologna", "Parma", "Modena"],
|
| 148 |
+
"Sicilia": ["Palermo", "Catania", "Syracuse"],
|
| 149 |
+
},
|
| 150 |
+
"Spain": {
|
| 151 |
+
"Comunidad de Madrid": ["Madrid"],
|
| 152 |
+
"Cataluña": ["Barcelona", "Tarragona", "Girona"],
|
| 153 |
+
"Andalucía": ["Seville", "Granada", "Málaga", "Córdoba"],
|
| 154 |
+
"Comunidad Valenciana": ["Valencia", "Alicante"],
|
| 155 |
+
"País Vasco": ["Bilbao", "San Sebastián"],
|
| 156 |
+
"Galicia": ["Santiago de Compostela", "A Coruña", "Vigo"],
|
| 157 |
+
"Islas Baleares": ["Palma de Mallorca"],
|
| 158 |
+
"Islas Canarias": ["Las Palmas", "Santa Cruz de Tenerife"],
|
| 159 |
+
},
|
| 160 |
+
"Australia": {
|
| 161 |
+
"New South Wales": ["Sydney", "Newcastle", "Wollongong"],
|
| 162 |
+
"Victoria": ["Melbourne", "Geelong"],
|
| 163 |
+
"Queensland": ["Brisbane", "Gold Coast", "Cairns"],
|
| 164 |
+
"Western Australia": ["Perth", "Fremantle"],
|
| 165 |
+
"South Australia": ["Adelaide"],
|
| 166 |
+
"Tasmania": ["Hobart"],
|
| 167 |
+
"Australian Capital Territory": ["Canberra"],
|
| 168 |
+
"Northern Territory": ["Darwin"],
|
| 169 |
+
},
|
| 170 |
+
"Canada": {
|
| 171 |
+
"Ontario": ["Toronto", "Ottawa", "Hamilton", "Mississauga"],
|
| 172 |
+
"Quebec": ["Montreal", "Quebec City", "Laval"],
|
| 173 |
+
"British Columbia": ["Vancouver", "Victoria", "Surrey"],
|
| 174 |
+
"Alberta": ["Calgary", "Edmonton"],
|
| 175 |
+
"Manitoba": ["Winnipeg"],
|
| 176 |
+
"Saskatchewan": ["Saskatoon", "Regina"],
|
| 177 |
+
"Nova Scotia": ["Halifax"],
|
| 178 |
+
},
|
| 179 |
+
"South Korea": {
|
| 180 |
+
"서울특별시": ["Seoul"],
|
| 181 |
+
"부산광역시": ["Busan"],
|
| 182 |
+
"경기도": ["Incheon", "Suwon", "Seongnam"],
|
| 183 |
+
"대구광역시": ["Daegu"],
|
| 184 |
+
"대전광역시": ["Daejeon"],
|
| 185 |
+
"광주광역시": ["Gwangju"],
|
| 186 |
+
"제주특별자치도": ["Jeju"],
|
| 187 |
+
},
|
| 188 |
+
"Singapore": {
|
| 189 |
+
"Singapore": ["Singapore"],
|
| 190 |
+
},
|
| 191 |
+
"India": {
|
| 192 |
+
"Maharashtra": ["Mumbai", "Pune", "Nagpur"],
|
| 193 |
+
"Delhi": ["New Delhi"],
|
| 194 |
+
"Karnataka": ["Bangalore", "Mysore"],
|
| 195 |
+
"Tamil Nadu": ["Chennai", "Coimbatore", "Madurai"],
|
| 196 |
+
"West Bengal": ["Kolkata"],
|
| 197 |
+
"Gujarat": ["Ahmedabad", "Surat", "Vadodara"],
|
| 198 |
+
"Telangana": ["Hyderabad"],
|
| 199 |
+
"Kerala": ["Kochi", "Trivandrum"],
|
| 200 |
+
"Rajasthan": ["Jaipur", "Jodhpur", "Udaipur"],
|
| 201 |
+
"Uttar Pradesh": ["Lucknow", "Agra", "Varanasi"],
|
| 202 |
+
},
|
| 203 |
+
"Russia": {
|
| 204 |
+
"Центральный": ["Moscow"],
|
| 205 |
+
"Северо-Западный": ["Saint Petersburg", "Kaliningrad"],
|
| 206 |
+
"Южный": ["Sochi", "Rostov-on-Don", "Krasnodar"],
|
| 207 |
+
"Приволжский": ["Kazan", "Nizhny Novgorod", "Samara"],
|
| 208 |
+
"Уральский": ["Yekaterinburg", "Chelyabinsk"],
|
| 209 |
+
"Сибирский": ["Novosibirsk", "Krasnoyarsk", "Irkutsk"],
|
| 210 |
+
"Дальневосточный": ["Vladivostok", "Khabarovsk"],
|
| 211 |
+
},
|
| 212 |
+
"Brazil": {
|
| 213 |
+
"São Paulo": ["São Paulo", "Campinas", "Santos"],
|
| 214 |
+
"Rio de Janeiro": ["Rio de Janeiro", "Niterói"],
|
| 215 |
+
"Minas Gerais": ["Belo Horizonte"],
|
| 216 |
+
"Bahia": ["Salvador"],
|
| 217 |
+
"Rio Grande do Sul": ["Porto Alegre"],
|
| 218 |
+
"Paraná": ["Curitiba"],
|
| 219 |
+
"Distrito Federal": ["Brasília"],
|
| 220 |
+
"Ceará": ["Fortaleza"],
|
| 221 |
+
"Pernambuco": ["Recife"],
|
| 222 |
+
"Amazonas": ["Manaus"],
|
| 223 |
+
},
|
| 224 |
+
"Mexico": {
|
| 225 |
+
"Ciudad de México": ["Mexico City"],
|
| 226 |
+
"Jalisco": ["Guadalajara"],
|
| 227 |
+
"Nuevo León": ["Monterrey"],
|
| 228 |
+
"Quintana Roo": ["Cancún", "Playa del Carmen"],
|
| 229 |
+
"Baja California": ["Tijuana", "Ensenada"],
|
| 230 |
+
"Yucatán": ["Mérida"],
|
| 231 |
+
"Puebla": ["Puebla"],
|
| 232 |
+
"Guanajuato": ["León", "Guanajuato"],
|
| 233 |
+
},
|
| 234 |
+
"Netherlands": {
|
| 235 |
+
"Noord-Holland": ["Amsterdam", "Haarlem"],
|
| 236 |
+
"Zuid-Holland": ["Rotterdam", "The Hague", "Leiden"],
|
| 237 |
+
"Utrecht": ["Utrecht"],
|
| 238 |
+
"Noord-Brabant": ["Eindhoven", "Tilburg", "'s-Hertogenbosch"],
|
| 239 |
+
"Gelderland": ["Arnhem", "Nijmegen"],
|
| 240 |
+
"Limburg": ["Maastricht"],
|
| 241 |
+
},
|
| 242 |
+
"Belgium": {
|
| 243 |
+
"Brussels-Capital": ["Brussels"],
|
| 244 |
+
"Flanders": ["Antwerp", "Ghent", "Bruges"],
|
| 245 |
+
"Wallonia": ["Liège", "Charleroi", "Namur"],
|
| 246 |
+
},
|
| 247 |
+
"Switzerland": {
|
| 248 |
+
"Zürich": ["Zurich"],
|
| 249 |
+
"Bern": ["Bern"],
|
| 250 |
+
"Geneva": ["Geneva"],
|
| 251 |
+
"Vaud": ["Lausanne"],
|
| 252 |
+
"Basel-Stadt": ["Basel"],
|
| 253 |
+
"Lucerne": ["Lucerne"],
|
| 254 |
+
},
|
| 255 |
+
"Austria": {
|
| 256 |
+
"Wien": ["Vienna"],
|
| 257 |
+
"Salzburg": ["Salzburg"],
|
| 258 |
+
"Tirol": ["Innsbruck"],
|
| 259 |
+
"Steiermark": ["Graz"],
|
| 260 |
+
"Oberösterreich": ["Linz"],
|
| 261 |
+
},
|
| 262 |
+
"Portugal": {
|
| 263 |
+
"Lisboa": ["Lisbon"],
|
| 264 |
+
"Porto": ["Porto"],
|
| 265 |
+
"Faro": ["Faro", "Albufeira"],
|
| 266 |
+
"Coimbra": ["Coimbra"],
|
| 267 |
+
},
|
| 268 |
+
"Greece": {
|
| 269 |
+
"Attica": ["Athens", "Piraeus"],
|
| 270 |
+
"Central Macedonia": ["Thessaloniki"],
|
| 271 |
+
"Crete": ["Heraklion"],
|
| 272 |
+
"South Aegean": ["Rhodes", "Mykonos", "Santorini"],
|
| 273 |
+
},
|
| 274 |
+
"Turkey": {
|
| 275 |
+
"Istanbul": ["Istanbul"],
|
| 276 |
+
"Ankara": ["Ankara"],
|
| 277 |
+
"Izmir": ["Izmir"],
|
| 278 |
+
"Antalya": ["Antalya"],
|
| 279 |
+
"Bursa": ["Bursa"],
|
| 280 |
+
"Cappadocia": ["Nevşehir", "Göreme"],
|
| 281 |
+
},
|
| 282 |
+
"UAE": {
|
| 283 |
+
"Dubai": ["Dubai"],
|
| 284 |
+
"Abu Dhabi": ["Abu Dhabi"],
|
| 285 |
+
"Sharjah": ["Sharjah"],
|
| 286 |
+
"Ras Al Khaimah": ["Ras Al Khaimah"],
|
| 287 |
+
},
|
| 288 |
+
"Thailand": {
|
| 289 |
+
"Bangkok": ["Bangkok"],
|
| 290 |
+
"Chiang Mai": ["Chiang Mai"],
|
| 291 |
+
"Phuket": ["Phuket"],
|
| 292 |
+
"Chonburi": ["Pattaya"],
|
| 293 |
+
"Krabi": ["Krabi"],
|
| 294 |
+
},
|
| 295 |
+
"Vietnam": {
|
| 296 |
+
"Hà Nội": ["Hanoi"],
|
| 297 |
+
"Hồ Chí Minh": ["Ho Chi Minh City"],
|
| 298 |
+
"Đà Nẵng": ["Da Nang"],
|
| 299 |
+
"Khánh Hòa": ["Nha Trang"],
|
| 300 |
+
"Quảng Ninh": ["Ha Long"],
|
| 301 |
+
},
|
| 302 |
+
"Indonesia": {
|
| 303 |
+
"DKI Jakarta": ["Jakarta"],
|
| 304 |
+
"Bali": ["Bali", "Denpasar", "Ubud"],
|
| 305 |
+
"Jawa Timur": ["Surabaya"],
|
| 306 |
+
"Jawa Barat": ["Bandung"],
|
| 307 |
+
"DI Yogyakarta": ["Yogyakarta"],
|
| 308 |
+
},
|
| 309 |
+
"Malaysia": {
|
| 310 |
+
"Kuala Lumpur": ["Kuala Lumpur"],
|
| 311 |
+
"Penang": ["George Town"],
|
| 312 |
+
"Selangor": ["Petaling Jaya", "Shah Alam"],
|
| 313 |
+
"Johor": ["Johor Bahru"],
|
| 314 |
+
"Sabah": ["Kota Kinabalu"],
|
| 315 |
+
"Sarawak": ["Kuching"],
|
| 316 |
+
},
|
| 317 |
+
"Philippines": {
|
| 318 |
+
"Metro Manila": ["Manila", "Makati", "Quezon City"],
|
| 319 |
+
"Cebu": ["Cebu City"],
|
| 320 |
+
"Davao": ["Davao City"],
|
| 321 |
+
"Palawan": ["Puerto Princesa"],
|
| 322 |
+
},
|
| 323 |
+
"Egypt": {
|
| 324 |
+
"Cairo Governorate": ["Cairo"],
|
| 325 |
+
"Alexandria Governorate": ["Alexandria"],
|
| 326 |
+
"Giza Governorate": ["Giza"],
|
| 327 |
+
"Luxor Governorate": ["Luxor"],
|
| 328 |
+
"Red Sea Governorate": ["Hurghada", "Sharm El Sheikh"],
|
| 329 |
+
},
|
| 330 |
+
"South Africa": {
|
| 331 |
+
"Gauteng": ["Johannesburg", "Pretoria"],
|
| 332 |
+
"Western Cape": ["Cape Town"],
|
| 333 |
+
"KwaZulu-Natal": ["Durban"],
|
| 334 |
+
"Eastern Cape": ["Port Elizabeth"],
|
| 335 |
+
},
|
| 336 |
+
"Morocco": {
|
| 337 |
+
"Casablanca-Settat": ["Casablanca"],
|
| 338 |
+
"Rabat-Salé-Kénitra": ["Rabat"],
|
| 339 |
+
"Marrakech-Safi": ["Marrakech", "Essaouira"],
|
| 340 |
+
"Fès-Meknès": ["Fes", "Meknes"],
|
| 341 |
+
"Tanger-Tétouan-Al Hoceïma": ["Tangier", "Chefchaouen"],
|
| 342 |
+
},
|
| 343 |
+
"Argentina": {
|
| 344 |
+
"Buenos Aires": ["Buenos Aires", "La Plata"],
|
| 345 |
+
"Córdoba": ["Córdoba"],
|
| 346 |
+
"Mendoza": ["Mendoza"],
|
| 347 |
+
"Santa Fe": ["Rosario"],
|
| 348 |
+
"Tierra del Fuego": ["Ushuaia"],
|
| 349 |
+
},
|
| 350 |
+
"Chile": {
|
| 351 |
+
"Región Metropolitana": ["Santiago"],
|
| 352 |
+
"Valparaíso": ["Valparaíso", "Viña del Mar"],
|
| 353 |
+
"Biobío": ["Concepción"],
|
| 354 |
+
"Los Lagos": ["Puerto Montt"],
|
| 355 |
+
"Magallanes": ["Punta Arenas"],
|
| 356 |
+
},
|
| 357 |
+
"Colombia": {
|
| 358 |
+
"Bogotá D.C.": ["Bogotá"],
|
| 359 |
+
"Antioquia": ["Medellín"],
|
| 360 |
+
"Valle del Cauca": ["Cali"],
|
| 361 |
+
"Atlántico": ["Barranquilla"],
|
| 362 |
+
"Bolívar": ["Cartagena"],
|
| 363 |
+
},
|
| 364 |
+
"Peru": {
|
| 365 |
+
"Lima": ["Lima"],
|
| 366 |
+
"Cusco": ["Cusco"],
|
| 367 |
+
"Arequipa": ["Arequipa"],
|
| 368 |
+
"La Libertad": ["Trujillo"],
|
| 369 |
+
},
|
| 370 |
+
"New Zealand": {
|
| 371 |
+
"Auckland": ["Auckland"],
|
| 372 |
+
"Wellington": ["Wellington"],
|
| 373 |
+
"Canterbury": ["Christchurch"],
|
| 374 |
+
"Otago": ["Dunedin", "Queenstown"],
|
| 375 |
+
},
|
| 376 |
+
"Ireland": {
|
| 377 |
+
"Leinster": ["Dublin"],
|
| 378 |
+
"Munster": ["Cork", "Limerick"],
|
| 379 |
+
"Connacht": ["Galway"],
|
| 380 |
+
},
|
| 381 |
+
"Sweden": {
|
| 382 |
+
"Stockholms län": ["Stockholm"],
|
| 383 |
+
"Västra Götalands län": ["Gothenburg"],
|
| 384 |
+
"Skåne län": ["Malmö"],
|
| 385 |
+
"Uppsala län": ["Uppsala"],
|
| 386 |
+
},
|
| 387 |
+
"Norway": {
|
| 388 |
+
"Oslo": ["Oslo"],
|
| 389 |
+
"Vestland": ["Bergen"],
|
| 390 |
+
"Troms og Finnmark": ["Tromsø"],
|
| 391 |
+
"Trøndelag": ["Trondheim"],
|
| 392 |
+
},
|
| 393 |
+
"Denmark": {
|
| 394 |
+
"Hovedstaden": ["Copenhagen"],
|
| 395 |
+
"Midtjylland": ["Aarhus"],
|
| 396 |
+
"Syddanmark": ["Odense"],
|
| 397 |
+
},
|
| 398 |
+
"Finland": {
|
| 399 |
+
"Uusimaa": ["Helsinki", "Espoo"],
|
| 400 |
+
"Pirkanmaa": ["Tampere"],
|
| 401 |
+
"Southwest Finland": ["Turku"],
|
| 402 |
+
"Lapland": ["Rovaniemi"],
|
| 403 |
+
},
|
| 404 |
+
"Poland": {
|
| 405 |
+
"Mazowieckie": ["Warsaw"],
|
| 406 |
+
"Małopolskie": ["Kraków"],
|
| 407 |
+
"Wielkopolskie": ["Poznań"],
|
| 408 |
+
"Dolnośląskie": ["Wrocław"],
|
| 409 |
+
"Pomorskie": ["Gdańsk"],
|
| 410 |
+
},
|
| 411 |
+
"Czech Republic": {
|
| 412 |
+
"Praha": ["Prague"],
|
| 413 |
+
"Jihomoravský": ["Brno"],
|
| 414 |
+
"Moravskoslezský": ["Ostrava"],
|
| 415 |
+
"Plzeňský": ["Plzeň"],
|
| 416 |
+
},
|
| 417 |
+
"Hungary": {
|
| 418 |
+
"Budapest": ["Budapest"],
|
| 419 |
+
"Pest": ["Szentendre"],
|
| 420 |
+
"Baranya": ["Pécs"],
|
| 421 |
+
"Hajdú-Bihar": ["Debrecen"],
|
| 422 |
+
},
|
| 423 |
+
"Israel": {
|
| 424 |
+
"Tel Aviv District": ["Tel Aviv", "Jaffa"],
|
| 425 |
+
"Jerusalem District": ["Jerusalem"],
|
| 426 |
+
"Haifa District": ["Haifa"],
|
| 427 |
+
"Southern District": ["Eilat"],
|
| 428 |
+
},
|
| 429 |
+
"Saudi Arabia": {
|
| 430 |
+
"Riyadh": ["Riyadh"],
|
| 431 |
+
"Makkah": ["Mecca", "Jeddah"],
|
| 432 |
+
"Eastern Province": ["Dammam", "Dhahran"],
|
| 433 |
+
"Medina": ["Medina"],
|
| 434 |
+
},
|
| 435 |
+
"Qatar": {
|
| 436 |
+
"Doha": ["Doha"],
|
| 437 |
+
},
|
| 438 |
+
"Kenya": {
|
| 439 |
+
"Nairobi": ["Nairobi"],
|
| 440 |
+
"Mombasa": ["Mombasa"],
|
| 441 |
+
"Nakuru": ["Nakuru"],
|
| 442 |
+
},
|
| 443 |
+
"Nigeria": {
|
| 444 |
+
"Lagos": ["Lagos"],
|
| 445 |
+
"Abuja": ["Abuja"],
|
| 446 |
+
"Kano": ["Kano"],
|
| 447 |
+
},
|
| 448 |
+
"Pakistan": {
|
| 449 |
+
"Punjab": ["Lahore", "Faisalabad"],
|
| 450 |
+
"Sindh": ["Karachi"],
|
| 451 |
+
"Islamabad": ["Islamabad"],
|
| 452 |
+
"Khyber Pakhtunkhwa": ["Peshawar"],
|
| 453 |
+
},
|
| 454 |
+
"Bangladesh": {
|
| 455 |
+
"Dhaka Division": ["Dhaka"],
|
| 456 |
+
"Chittagong Division": ["Chittagong"],
|
| 457 |
+
"Sylhet Division": ["Sylhet"],
|
| 458 |
+
},
|
| 459 |
+
"Sri Lanka": {
|
| 460 |
+
"Western Province": ["Colombo"],
|
| 461 |
+
"Central Province": ["Kandy"],
|
| 462 |
+
"Southern Province": ["Galle"],
|
| 463 |
+
},
|
| 464 |
+
"Nepal": {
|
| 465 |
+
"Bagmati": ["Kathmandu"],
|
| 466 |
+
"Gandaki": ["Pokhara"],
|
| 467 |
+
},
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
# Translation layer for dual-language support
|
| 471 |
+
CN_TO_EN = {
|
| 472 |
+
# Countries (CN -> EN)
|
| 473 |
+
"中国": "China",
|
| 474 |
+
"美国": "United States",
|
| 475 |
+
"日本": "Japan",
|
| 476 |
+
"英国": "United Kingdom",
|
| 477 |
+
"法国": "France",
|
| 478 |
+
"德国": "Germany",
|
| 479 |
+
"意大利": "Italy",
|
| 480 |
+
"西班牙": "Spain",
|
| 481 |
+
"澳大利亚": "Australia",
|
| 482 |
+
"加拿大": "Canada",
|
| 483 |
+
"韩国": "South Korea",
|
| 484 |
+
"新加坡": "Singapore",
|
| 485 |
+
"印度": "India",
|
| 486 |
+
"俄罗斯": "Russia",
|
| 487 |
+
"巴西": "Brazil",
|
| 488 |
+
"墨西哥": "Mexico",
|
| 489 |
+
"荷兰": "Netherlands",
|
| 490 |
+
"比利时": "Belgium",
|
| 491 |
+
"瑞士": "Switzerland",
|
| 492 |
+
"奥地利": "Austria",
|
| 493 |
+
"葡萄牙": "Portugal",
|
| 494 |
+
"希腊": "Greece",
|
| 495 |
+
"土耳其": "Turkey",
|
| 496 |
+
"阿联酋": "United Arab Emirates",
|
| 497 |
+
"泰国": "Thailand",
|
| 498 |
+
"越南": "Vietnam",
|
| 499 |
+
"印度尼西亚": "Indonesia",
|
| 500 |
+
"马来西亚": "Malaysia",
|
| 501 |
+
"菲律宾": "Philippines",
|
| 502 |
+
"埃及": "Egypt",
|
| 503 |
+
"南非": "South Africa",
|
| 504 |
+
"摩洛哥": "Morocco",
|
| 505 |
+
"阿根廷": "Argentina",
|
| 506 |
+
"智利": "Chile",
|
| 507 |
+
"哥伦比亚": "Colombia",
|
| 508 |
+
"秘鲁": "Peru",
|
| 509 |
+
"新西兰": "New Zealand",
|
| 510 |
+
"爱尔兰": "Ireland",
|
| 511 |
+
"瑞典": "Sweden",
|
| 512 |
+
"挪威": "Norway",
|
| 513 |
+
"丹麦": "Denmark",
|
| 514 |
+
"芬兰": "Finland",
|
| 515 |
+
"波兰": "Poland",
|
| 516 |
+
"捷克": "Czech Republic",
|
| 517 |
+
"匈牙利": "Hungary",
|
| 518 |
+
"以色列": "Israel",
|
| 519 |
+
"沙特阿拉伯": "Saudi Arabia",
|
| 520 |
+
"卡塔尔": "Qatar",
|
| 521 |
+
"肯尼亚": "Kenya",
|
| 522 |
+
"尼日利亚": "Nigeria",
|
| 523 |
+
"巴基斯坦": "Pakistan",
|
| 524 |
+
"孟加拉国": "Bangladesh",
|
| 525 |
+
"斯里兰卡": "Sri Lanka",
|
| 526 |
+
"尼泊尔": "Nepal",
|
| 527 |
+
# Chinese Provinces (CN -> EN)
|
| 528 |
+
"北京": "Beijing",
|
| 529 |
+
"上海": "Shanghai",
|
| 530 |
+
"天津": "Tianjin",
|
| 531 |
+
"重庆": "Chongqing",
|
| 532 |
+
"广东": "Guangdong",
|
| 533 |
+
"浙江": "Zhejiang",
|
| 534 |
+
"江苏": "Jiangsu",
|
| 535 |
+
"山东": "Shandong",
|
| 536 |
+
"四川": "Sichuan",
|
| 537 |
+
"湖北": "Hubei",
|
| 538 |
+
"湖南": "Hunan",
|
| 539 |
+
"河南": "Henan",
|
| 540 |
+
"河北": "Hebei",
|
| 541 |
+
"福建": "Fujian",
|
| 542 |
+
"安徽": "Anhui",
|
| 543 |
+
"江西": "Jiangxi",
|
| 544 |
+
"陕西": "Shaanxi",
|
| 545 |
+
"山西": "Shanxi",
|
| 546 |
+
"辽宁": "Liaoning",
|
| 547 |
+
"吉林": "Jilin",
|
| 548 |
+
"黑龙江": "Heilongjiang",
|
| 549 |
+
"云南": "Yunnan",
|
| 550 |
+
"贵州": "Guizhou",
|
| 551 |
+
"甘肃": "Gansu",
|
| 552 |
+
"海南": "Hainan",
|
| 553 |
+
"广西": "Guangxi",
|
| 554 |
+
"内蒙古": "Inner Mongolia",
|
| 555 |
+
"新疆": "Xinjiang",
|
| 556 |
+
"西藏": "Tibet",
|
| 557 |
+
"宁夏": "Ningxia",
|
| 558 |
+
"青海": "Qinghai",
|
| 559 |
+
"香港": "Hong Kong",
|
| 560 |
+
"澳门": "Macau",
|
| 561 |
+
"台湾": "Taiwan",
|
| 562 |
+
# Chinese Cities (CN -> EN)
|
| 563 |
+
"广州": "Guangzhou",
|
| 564 |
+
"深圳": "Shenzhen",
|
| 565 |
+
"东莞": "Dongguan",
|
| 566 |
+
"佛山": "Foshan",
|
| 567 |
+
"珠海": "Zhuhai",
|
| 568 |
+
"惠州": "Huizhou",
|
| 569 |
+
"中山": "Zhongshan",
|
| 570 |
+
"汕头": "Shantou",
|
| 571 |
+
"湛江": "Zhanjiang",
|
| 572 |
+
"江门": "Jiangmen",
|
| 573 |
+
"梧州": "Wuzhou",
|
| 574 |
+
"杭州": "Hangzhou",
|
| 575 |
+
"宁波": "Ningbo",
|
| 576 |
+
"温州": "Wenzhou",
|
| 577 |
+
"绍���": "Shaoxing",
|
| 578 |
+
"嘉兴": "Jiaxing",
|
| 579 |
+
"金华": "Jinhua",
|
| 580 |
+
"台州": "Taizhou",
|
| 581 |
+
"湖州": "Huzhou",
|
| 582 |
+
"南京": "Nanjing",
|
| 583 |
+
"苏州": "Suzhou",
|
| 584 |
+
"无锡": "Wuxi",
|
| 585 |
+
"常州": "Changzhou",
|
| 586 |
+
"南通": "Nantong",
|
| 587 |
+
"徐州": "Xuzhou",
|
| 588 |
+
"扬州": "Yangzhou",
|
| 589 |
+
"镇江": "Zhenjiang",
|
| 590 |
+
"济南": "Jinan",
|
| 591 |
+
"青岛": "Qingdao",
|
| 592 |
+
"烟台": "Yantai",
|
| 593 |
+
"威海": "Weihai",
|
| 594 |
+
"潍坊": "Weifang",
|
| 595 |
+
"临沂": "Linyi",
|
| 596 |
+
"济宁": "Jining",
|
| 597 |
+
"淄博": "Zibo",
|
| 598 |
+
"成都": "Chengdu",
|
| 599 |
+
"绵阳": "Mianyang",
|
| 600 |
+
"德阳": "Deyang",
|
| 601 |
+
"宜宾": "Yibin",
|
| 602 |
+
"泸州": "Luzhou",
|
| 603 |
+
"南充": "Nanchong",
|
| 604 |
+
"乐山": "Leshan",
|
| 605 |
+
"武汉": "Wuhan",
|
| 606 |
+
"宜昌": "Yichang",
|
| 607 |
+
"襄阳": "Xiangyang",
|
| 608 |
+
"荆州": "Jingzhou",
|
| 609 |
+
"黄石": "Huangshi",
|
| 610 |
+
"十堰": "Shiyan",
|
| 611 |
+
"长沙": "Changsha",
|
| 612 |
+
"株洲": "Zhuzhou",
|
| 613 |
+
"湘潭": "Xiangtan",
|
| 614 |
+
"衡阳": "Hengyang",
|
| 615 |
+
"岳阳": "Yueyang",
|
| 616 |
+
"常德": "Changde",
|
| 617 |
+
"郑州": "Zhengzhou",
|
| 618 |
+
"洛阳": "Luoyang",
|
| 619 |
+
"开封": "Kaifeng",
|
| 620 |
+
"新乡": "Xinxiang",
|
| 621 |
+
"安阳": "Anyang",
|
| 622 |
+
"焦作": "Jiaozuo",
|
| 623 |
+
"石家庄": "Shijiazhuang",
|
| 624 |
+
"唐山": "Tangshan",
|
| 625 |
+
"秦皇岛": "Qinhuangdao",
|
| 626 |
+
"邯郸": "Handan",
|
| 627 |
+
"保定": "Baoding",
|
| 628 |
+
"沧州": "Cangzhou",
|
| 629 |
+
"福州": "Fuzhou",
|
| 630 |
+
"厦门": "Xiamen",
|
| 631 |
+
"泉州": "Quanzhou",
|
| 632 |
+
"漳州": "Zhangzhou",
|
| 633 |
+
"莆田": "Putian",
|
| 634 |
+
"龙岩": "Longyan",
|
| 635 |
+
"合肥": "Hefei",
|
| 636 |
+
"芜湖": "Wuhu",
|
| 637 |
+
"蚌埠": "Bengbu",
|
| 638 |
+
"马鞍山": "Ma'anshan",
|
| 639 |
+
"安庆": "Anqing",
|
| 640 |
+
"黄山": "Huangshan",
|
| 641 |
+
"南昌": "Nanchang",
|
| 642 |
+
"九江": "Jiujiang",
|
| 643 |
+
"景德镇": "Jingdezhen",
|
| 644 |
+
"赣州": "Ganzhou",
|
| 645 |
+
"上饶": "Shangrao",
|
| 646 |
+
"吉安": "Ji'an",
|
| 647 |
+
"西安": "Xi'an",
|
| 648 |
+
"咸阳": "Xianyang",
|
| 649 |
+
"宝鸡": "Baoji",
|
| 650 |
+
"延安": "Yan'an",
|
| 651 |
+
"榆林": "Yulin",
|
| 652 |
+
"汉中": "Hanzhong",
|
| 653 |
+
"太原": "Taiyuan",
|
| 654 |
+
"大同": "Datong",
|
| 655 |
+
"临汾": "Linfen",
|
| 656 |
+
"运城": "Yuncheng",
|
| 657 |
+
"晋中": "Jinzhong",
|
| 658 |
+
"长治": "Changzhi",
|
| 659 |
+
"沈阳": "Shenyang",
|
| 660 |
+
"大连": "Dalian",
|
| 661 |
+
"鞍山": "Anshan",
|
| 662 |
+
"抚顺": "Fushun",
|
| 663 |
+
"本溪": "Benxi",
|
| 664 |
+
"营口": "Yingkou",
|
| 665 |
+
"长春": "Changchun",
|
| 666 |
+
"吉林": "Jilin",
|
| 667 |
+
"四平": "Siping",
|
| 668 |
+
"通化": "Tonghua",
|
| 669 |
+
"延边": "Yanbian",
|
| 670 |
+
"哈尔滨": "Harbin",
|
| 671 |
+
"齐齐哈尔": "Qiqihar",
|
| 672 |
+
"牡丹江": "Mudanjiang",
|
| 673 |
+
"佳木斯": "Jiamusi",
|
| 674 |
+
"大庆": "Daqing",
|
| 675 |
+
"昆明": "Kunming",
|
| 676 |
+
"大理": "Dali",
|
| 677 |
+
"丽江": "Lijiang",
|
| 678 |
+
"西双版纳": "Xishuangbanna",
|
| 679 |
+
"曲靖": "Qujing",
|
| 680 |
+
"贵阳": "Guiyang",
|
| 681 |
+
"遵义": "Zunyi",
|
| 682 |
+
"安顺": "Anshun",
|
| 683 |
+
"六盘水": "Liupanshui",
|
| 684 |
+
"毕节": "Bijie",
|
| 685 |
+
"兰州": "Lanzhou",
|
| 686 |
+
"天水": "Tianshui",
|
| 687 |
+
"嘉峪关": "Jiayuguan",
|
| 688 |
+
"酒泉": "Jiuquan",
|
| 689 |
+
"张掖": "Zhangye",
|
| 690 |
+
"海口": "Haikou",
|
| 691 |
+
"三亚": "Sanya",
|
| 692 |
+
"儋州": "Danzhou",
|
| 693 |
+
"琼海": "Qionghai",
|
| 694 |
+
"南宁": "Nanning",
|
| 695 |
+
"桂林": "Guilin",
|
| 696 |
+
"柳州": "Liuzhou",
|
| 697 |
+
"北海": "Beihai",
|
| 698 |
+
"玉林": "Yulin",
|
| 699 |
+
"呼和浩特": "Hohhot",
|
| 700 |
+
"包头": "Baotou",
|
| 701 |
+
"鄂尔多斯": "Ordos",
|
| 702 |
+
"赤峰": "Chifeng",
|
| 703 |
+
"呼伦贝尔": "Hulunbuir",
|
| 704 |
+
"乌鲁木齐": "Urumqi",
|
| 705 |
+
"喀什": "Kashgar",
|
| 706 |
+
"吐鲁番": "Turpan",
|
| 707 |
+
"阿克苏": "Aksu",
|
| 708 |
+
"伊宁": "Yining",
|
| 709 |
+
"拉萨": "Lhasa",
|
| 710 |
+
"日喀则": "Shigatse",
|
| 711 |
+
"林芝": "Nyingchi",
|
| 712 |
+
"昌都": "Qamdo",
|
| 713 |
+
"银川": "Yinchuan",
|
| 714 |
+
"石嘴山": "Shizuishan",
|
| 715 |
+
"吴忠": "Wuzhong",
|
| 716 |
+
"固原": "Guyuan",
|
| 717 |
+
"西宁": "Xining",
|
| 718 |
+
"格尔木": "Golmud",
|
| 719 |
+
"玉树": "Yushu",
|
| 720 |
+
"海东": "Haidong",
|
| 721 |
+
"台北": "Taipei",
|
| 722 |
+
"高雄": "Kaohsiung",
|
| 723 |
+
"台中": "Taichung",
|
| 724 |
+
"台南": "Tainan",
|
| 725 |
+
"新竹": "Hsinchu",
|
| 726 |
+
"基隆": "Keelung",
|
| 727 |
+
# Theme Names - MOVED TO EN_TO_CN
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
# English name to Chinese (for when UI/Poster is in Chinese)
|
| 731 |
+
EN_TO_CN = {
|
| 732 |
+
"China": "中国",
|
| 733 |
+
"USA": "美国",
|
| 734 |
+
"United States": "美国",
|
| 735 |
+
"Japan": "日本",
|
| 736 |
+
"UK": "英国",
|
| 737 |
+
"United Kingdom": "英国",
|
| 738 |
+
"France": "法国",
|
| 739 |
+
"Germany": "德国",
|
| 740 |
+
"Italy": "意大利",
|
| 741 |
+
"Spain": "西班牙",
|
| 742 |
+
"Australia": "澳大利亚",
|
| 743 |
+
"Canada": "加拿大",
|
| 744 |
+
"South Korea": "韩国",
|
| 745 |
+
"Singapore": "新加坡",
|
| 746 |
+
"India": "印度",
|
| 747 |
+
"Russia": "俄罗斯",
|
| 748 |
+
"Brazil": "巴西",
|
| 749 |
+
"Mexico": "墨西哥",
|
| 750 |
+
"Netherlands": "荷兰",
|
| 751 |
+
"Belgium": "比利时",
|
| 752 |
+
"Switzerland": "瑞士",
|
| 753 |
+
"Austria": "奥地利",
|
| 754 |
+
"Portugal": "葡萄牙",
|
| 755 |
+
"Greece": "希腊",
|
| 756 |
+
"Turkey": "土耳其",
|
| 757 |
+
"UAE": "阿联酋",
|
| 758 |
+
"United Arab Emirates": "阿联酋",
|
| 759 |
+
"Thailand": "泰国",
|
| 760 |
+
"Vietnam": "越南",
|
| 761 |
+
"Indonesia": "印度尼西亚",
|
| 762 |
+
"Malaysia": "马来西亚",
|
| 763 |
+
"Philippines": "菲律宾",
|
| 764 |
+
"Egypt": "埃及",
|
| 765 |
+
"South Africa": "南非",
|
| 766 |
+
"Morocco": "摩洛哥",
|
| 767 |
+
"Argentina": "阿根廷",
|
| 768 |
+
"Chile": "智利",
|
| 769 |
+
"Colombia": "哥伦比亚",
|
| 770 |
+
"Peru": "秘鲁",
|
| 771 |
+
"New Zealand": "新西兰",
|
| 772 |
+
"Ireland": "爱尔兰",
|
| 773 |
+
"Sweden": "瑞典",
|
| 774 |
+
"Norway": "挪威",
|
| 775 |
+
"Denmark": "丹麦",
|
| 776 |
+
"Finland": "芬兰",
|
| 777 |
+
"Poland": "波兰",
|
| 778 |
+
"Czech Republic": "捷克",
|
| 779 |
+
"Hungary": "匈牙利",
|
| 780 |
+
"Israel": "以色列",
|
| 781 |
+
"Saudi Arabia": "沙特阿拉伯",
|
| 782 |
+
"Qatar": "卡塔尔",
|
| 783 |
+
"Kenya": "肯尼亚",
|
| 784 |
+
"Nigeria": "尼日利亚",
|
| 785 |
+
"Pakistan": "巴基斯坦",
|
| 786 |
+
"Bangladesh": "孟加拉国",
|
| 787 |
+
"Sri Lanka": "斯里兰卡",
|
| 788 |
+
"Nepal": "尼泊尔",
|
| 789 |
+
# Common Cities
|
| 790 |
+
"New York City": "纽约",
|
| 791 |
+
"London": "伦敦",
|
| 792 |
+
"Paris": "巴黎",
|
| 793 |
+
"Tokyo": "东京",
|
| 794 |
+
"Guangzhou": "广州",
|
| 795 |
+
"Shenzhen": "深圳",
|
| 796 |
+
"Beijing": "北京",
|
| 797 |
+
"Shanghai": "上海",
|
| 798 |
+
# Theme Names
|
| 799 |
+
"Feature-Based Shading": "特征着色",
|
| 800 |
+
"Japanese Ink": "日式水墨",
|
| 801 |
+
"Midnight Blue": "午夜蓝",
|
| 802 |
+
"Terracotta": "陶土色",
|
| 803 |
+
"Autumn": "秋日",
|
| 804 |
+
"Blueprint": "蓝图",
|
| 805 |
+
"Contrast Zones": "高对比度",
|
| 806 |
+
"Copper Patina": "铜绿",
|
| 807 |
+
"Forest": "森林",
|
| 808 |
+
"Gradient Roads": "渐变道路",
|
| 809 |
+
"Monochrome Blue": "单色蓝",
|
| 810 |
+
"Neon Cyberpunk": "霓虹赛博",
|
| 811 |
+
"Noir": "诺尔黑白",
|
| 812 |
+
"Ocean": "海洋",
|
| 813 |
+
"Pastel Dream": "蜡笔梦幻",
|
| 814 |
+
"Sunset": "日落",
|
| 815 |
+
"Warm Beige": "温暖米色",
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
# Add inverse mappings to ensure completeness
|
| 819 |
+
for k, v in CN_TO_EN.items():
|
| 820 |
+
if v not in EN_TO_CN:
|
| 821 |
+
EN_TO_CN[v] = k
|
| 822 |
+
|
| 823 |
+
# Expose helpers
|
| 824 |
+
__all__ = [
|
| 825 |
+
"CITIES",
|
| 826 |
+
"get_countries",
|
| 827 |
+
"get_provinces",
|
| 828 |
+
"get_cities",
|
| 829 |
+
"get_city_full_name",
|
| 830 |
+
"translate",
|
| 831 |
+
"get_country_key",
|
| 832 |
+
"get_manual_coordinates",
|
| 833 |
+
]
|
| 834 |
+
|
| 835 |
+
# Manual overrides for city centers to ensure logical centering (e.g. Tiananmen for Beijing)
|
| 836 |
+
CITY_CENTERS = {
|
| 837 |
+
"北京": (39.9042, 116.4074),
|
| 838 |
+
"Beijing": (39.9042, 116.4074),
|
| 839 |
+
"上海": (31.2304, 121.4737),
|
| 840 |
+
"Shanghai": (31.2304, 121.4737),
|
| 841 |
+
"广州": (23.1291, 113.2644),
|
| 842 |
+
"Guangzhou": (23.1291, 113.2644),
|
| 843 |
+
"深圳": (22.5422, 114.0579),
|
| 844 |
+
"Shenzhen": (22.5422, 114.0579),
|
| 845 |
+
"杭州": (30.2741, 120.1551),
|
| 846 |
+
"Hangzhou": (30.2741, 120.1551),
|
| 847 |
+
"成都": (30.6570, 104.0660),
|
| 848 |
+
"Chengdu": (30.6570, 104.0660),
|
| 849 |
+
"南京": (32.0603, 118.7969),
|
| 850 |
+
"Nanjing": (32.0603, 118.7969),
|
| 851 |
+
"武汉": (30.5928, 114.3055),
|
| 852 |
+
"Wuhan": (30.5928, 114.3055),
|
| 853 |
+
"西安": (34.3416, 108.9398),
|
| 854 |
+
"Xi'an": (34.3416, 108.9398),
|
| 855 |
+
"苏州": (31.2990, 120.5853),
|
| 856 |
+
"Suzhou": (31.2990, 120.5853),
|
| 857 |
+
"重庆": (29.5630, 106.5516),
|
| 858 |
+
"Chongqing": (29.5630, 106.5516),
|
| 859 |
+
"天津": (39.1255, 117.1901),
|
| 860 |
+
"Tianjin": (39.1255, 117.1901),
|
| 861 |
+
"香港": (22.3193, 114.1694),
|
| 862 |
+
"Hong Kong": (22.3193, 114.1694),
|
| 863 |
+
"台北": (25.0330, 121.5654),
|
| 864 |
+
"Taipei": (25.0330, 121.5654),
|
| 865 |
+
"New York City": (40.7128, -74.0060),
|
| 866 |
+
"纽约": (40.7128, -74.0060),
|
| 867 |
+
"London": (51.5074, -0.1278),
|
| 868 |
+
"伦敦": (51.5074, -0.1278),
|
| 869 |
+
"Paris": (48.8566, 2.3522),
|
| 870 |
+
"巴黎": (48.8566, 2.3522),
|
| 871 |
+
"Tokyo": (35.6895, 139.6917),
|
| 872 |
+
"东京": (35.6895, 139.6917),
|
| 873 |
+
"桂林": (25.2736, 110.2902),
|
| 874 |
+
"Guilin": (25.2736, 110.2902),
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
def get_manual_coordinates(city_name):
|
| 879 |
+
"""Return manual coordinates if available, else None."""
|
| 880 |
+
if not city_name:
|
| 881 |
+
return None
|
| 882 |
+
return CITY_CENTERS.get(city_name)
|
| 883 |
+
|
| 884 |
+
|
| 885 |
+
def translate(text, target_lang="en"):
|
| 886 |
+
"""
|
| 887 |
+
Simple translation helper.
|
| 888 |
+
target_lang: 'en' or 'cn'
|
| 889 |
+
"""
|
| 890 |
+
if not text:
|
| 891 |
+
return text
|
| 892 |
+
|
| 893 |
+
if target_lang == "en":
|
| 894 |
+
# If text is already in CN_TO_EN keys (Chinese), return the English value
|
| 895 |
+
return CN_TO_EN.get(text, text)
|
| 896 |
+
|
| 897 |
+
if target_lang == "cn":
|
| 898 |
+
# If text is in EN_TO_CN keys (English), return the Chinese value
|
| 899 |
+
return EN_TO_CN.get(text, text)
|
| 900 |
+
|
| 901 |
+
return text
|
| 902 |
+
|
| 903 |
+
|
| 904 |
+
def get_countries(lang="en"):
|
| 905 |
+
"""Return list of countries in the target language."""
|
| 906 |
+
countries = []
|
| 907 |
+
for country_key in sorted(CITIES.keys()):
|
| 908 |
+
countries.append(translate(country_key, lang))
|
| 909 |
+
return sorted(list(set(countries)))
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
def get_country_key(name):
|
| 913 |
+
"""Map a localized name back to the CITIES dictionary key."""
|
| 914 |
+
# Check if it's already a key
|
| 915 |
+
if name in CITIES:
|
| 916 |
+
return name
|
| 917 |
+
# Check if it's an English name that needs to be Chinese key
|
| 918 |
+
cn_name = EN_TO_CN.get(name)
|
| 919 |
+
if cn_name in CITIES:
|
| 920 |
+
return cn_name
|
| 921 |
+
# Check if it's a Chinese name that needs to be English key
|
| 922 |
+
en_name = CN_TO_EN.get(name)
|
| 923 |
+
if en_name in CITIES:
|
| 924 |
+
return en_name
|
| 925 |
+
return name
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def get_provinces(country_name, lang="en"):
|
| 929 |
+
"""Return sorted list of provinces/states for a given country in target language."""
|
| 930 |
+
country_key = get_country_key(country_name)
|
| 931 |
+
if country_key in CITIES:
|
| 932 |
+
# Translate provinces to target lang
|
| 933 |
+
provinces = [translate(p, lang) for p in CITIES[country_key].keys()]
|
| 934 |
+
return sorted(provinces)
|
| 935 |
+
return []
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
def get_cities(country_name, province_name, lang="en"):
|
| 939 |
+
"""Return sorted list of cities in target language."""
|
| 940 |
+
country_key = get_country_key(country_name)
|
| 941 |
+
# To find the province key, we might need to map it back if it was translated
|
| 942 |
+
# For now, let's assume provincial keys in CITIES are what we search against
|
| 943 |
+
# If the province_name is translated, we need to find the original key
|
| 944 |
+
province_key = province_name
|
| 945 |
+
if country_key in CITIES:
|
| 946 |
+
# Check if province_name is a direct key
|
| 947 |
+
if province_name not in CITIES[country_key]:
|
| 948 |
+
# Try to find the key that translates to province_name
|
| 949 |
+
for p_key in CITIES[country_key].keys():
|
| 950 |
+
if translate(p_key, lang) == province_name:
|
| 951 |
+
province_key = p_key
|
| 952 |
+
break
|
| 953 |
+
|
| 954 |
+
if province_key in CITIES[country_key]:
|
| 955 |
+
cities = [translate(c, lang) for c in CITIES[country_key][province_key]]
|
| 956 |
+
return sorted(cities)
|
| 957 |
+
return []
|
| 958 |
+
|
| 959 |
+
|
| 960 |
+
def search_cities(query):
|
| 961 |
+
"""
|
| 962 |
+
Search for cities matching the query (checks both Native and English names).
|
| 963 |
+
Returns list of tuples: (city, province, country)
|
| 964 |
+
"""
|
| 965 |
+
if not query or len(query) < 2:
|
| 966 |
+
return []
|
| 967 |
+
|
| 968 |
+
query = query.lower()
|
| 969 |
+
results = []
|
| 970 |
+
|
| 971 |
+
for country, provinces in CITIES.items():
|
| 972 |
+
country_en = CN_TO_EN.get(country, "").lower()
|
| 973 |
+
for province, cities in provinces.items():
|
| 974 |
+
for city in cities:
|
| 975 |
+
city_en = CN_TO_EN.get(city, "").lower()
|
| 976 |
+
# Match against native name or English name
|
| 977 |
+
if query in city.lower() or (city_en and query in city_en):
|
| 978 |
+
results.append((city, province, country))
|
| 979 |
+
|
| 980 |
+
# Sort by city name
|
| 981 |
+
return sorted(results, key=lambda x: x[0])[:20]
|
| 982 |
+
|
| 983 |
+
|
| 984 |
+
def get_city_full_name(city, province, country, target_lang=None):
|
| 985 |
+
"""
|
| 986 |
+
Return formatted full name.
|
| 987 |
+
If target_lang is provided ('en' or 'cn'), translates components.
|
| 988 |
+
"""
|
| 989 |
+
if target_lang:
|
| 990 |
+
city = translate(city, target_lang)
|
| 991 |
+
province = translate(province, target_lang)
|
| 992 |
+
country = translate(country, target_lang)
|
| 993 |
+
|
| 994 |
+
if province == city: # Direct-controlled municipalities
|
| 995 |
+
return f"{city}, {country}"
|
| 996 |
+
return f"{city}, {province}, {country}"
|
create_map_poster.py
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import osmnx as ox
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from matplotlib.font_manager import FontProperties
|
| 5 |
+
import matplotlib.colors as mcolors
|
| 6 |
+
import numpy as np
|
| 7 |
+
from geopy.geocoders import Nominatim
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
import time
|
| 10 |
+
import json
|
| 11 |
+
import os
|
| 12 |
+
from datetime import datetime
|
| 13 |
+
import argparse
|
| 14 |
+
|
| 15 |
+
# Explicitly enable caching
|
| 16 |
+
ox.settings.use_cache = True
|
| 17 |
+
|
| 18 |
+
THEMES_DIR = "themes"
|
| 19 |
+
FONTS_DIR = "fonts"
|
| 20 |
+
POSTERS_DIR = "posters"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def load_fonts():
|
| 24 |
+
"""
|
| 25 |
+
Load fonts for both English (Goudy Old Style) and Chinese (HYWenRunSongYunU).
|
| 26 |
+
"""
|
| 27 |
+
fonts = {
|
| 28 |
+
"en_bold": os.path.join(FONTS_DIR, "GoudyOldStyle-Bold.ttf"),
|
| 29 |
+
"en_regular": os.path.join(FONTS_DIR, "GoudyOldStyle-Regular.ttf"),
|
| 30 |
+
"cn": os.path.join(FONTS_DIR, "HYWenRunSongYunU.ttf"),
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
# Verify fonts exist
|
| 34 |
+
available_fonts = {}
|
| 35 |
+
for key, path in fonts.items():
|
| 36 |
+
if os.path.exists(path):
|
| 37 |
+
available_fonts[key] = path
|
| 38 |
+
else:
|
| 39 |
+
print(f"⚠ Font not found: {path}")
|
| 40 |
+
|
| 41 |
+
return available_fonts if available_fonts else None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
FONTS = load_fonts()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def generate_output_filename(city, theme_name, output_format):
|
| 48 |
+
"""
|
| 49 |
+
Generate unique output filename with city, theme, and datetime.
|
| 50 |
+
"""
|
| 51 |
+
if not os.path.exists(POSTERS_DIR):
|
| 52 |
+
os.makedirs(POSTERS_DIR)
|
| 53 |
+
|
| 54 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 55 |
+
city_slug = city.lower().replace(" ", "_")
|
| 56 |
+
ext = output_format.lower()
|
| 57 |
+
filename = f"{city_slug}_{theme_name}_{timestamp}.{ext}"
|
| 58 |
+
return os.path.join(POSTERS_DIR, filename)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def get_available_themes():
|
| 62 |
+
"""
|
| 63 |
+
Scans the themes directory and returns a list of available theme names.
|
| 64 |
+
"""
|
| 65 |
+
if not os.path.exists(THEMES_DIR):
|
| 66 |
+
os.makedirs(THEMES_DIR)
|
| 67 |
+
return []
|
| 68 |
+
|
| 69 |
+
themes = []
|
| 70 |
+
for file in sorted(os.listdir(THEMES_DIR)):
|
| 71 |
+
if file.endswith(".json"):
|
| 72 |
+
theme_name = file[:-5] # Remove .json extension
|
| 73 |
+
themes.append(theme_name)
|
| 74 |
+
return themes
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def load_theme(theme_name="feature_based"):
|
| 78 |
+
"""
|
| 79 |
+
Load theme from JSON file in themes directory.
|
| 80 |
+
"""
|
| 81 |
+
theme_file = os.path.join(THEMES_DIR, f"{theme_name}.json")
|
| 82 |
+
|
| 83 |
+
if not os.path.exists(theme_file):
|
| 84 |
+
print(
|
| 85 |
+
f"⚠ Theme file '{theme_file}' not found. Using default feature_based theme."
|
| 86 |
+
)
|
| 87 |
+
# Fallback to embedded default theme
|
| 88 |
+
return {
|
| 89 |
+
"name": "Feature-Based Shading",
|
| 90 |
+
"bg": "#FFFFFF",
|
| 91 |
+
"text": "#000000",
|
| 92 |
+
"gradient_color": "#FFFFFF",
|
| 93 |
+
"water": "#C0C0C0",
|
| 94 |
+
"parks": "#F0F0F0",
|
| 95 |
+
"road_motorway": "#0A0A0A",
|
| 96 |
+
"road_primary": "#1A1A1A",
|
| 97 |
+
"road_secondary": "#2A2A2A",
|
| 98 |
+
"road_tertiary": "#3A3A3A",
|
| 99 |
+
"road_residential": "#4A4A4A",
|
| 100 |
+
"road_default": "#3A3A3A",
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
with open(theme_file, "r") as f:
|
| 104 |
+
theme = json.load(f)
|
| 105 |
+
print(f"✓ Loaded theme: {theme.get('name', theme_name)}")
|
| 106 |
+
if "description" in theme:
|
| 107 |
+
print(f" {theme['description']}")
|
| 108 |
+
return theme
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# Load theme (can be changed via command line or input)
|
| 112 |
+
THEME = None # Will be loaded later
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def create_gradient_fade(ax, color, location="bottom", zorder=10):
|
| 116 |
+
"""
|
| 117 |
+
Creates a fade effect at the top or bottom of the map.
|
| 118 |
+
"""
|
| 119 |
+
vals = np.linspace(0, 1, 256).reshape(-1, 1)
|
| 120 |
+
gradient = np.hstack((vals, vals))
|
| 121 |
+
|
| 122 |
+
rgb = mcolors.to_rgb(color)
|
| 123 |
+
my_colors = np.zeros((256, 4))
|
| 124 |
+
my_colors[:, 0] = rgb[0]
|
| 125 |
+
my_colors[:, 1] = rgb[1]
|
| 126 |
+
my_colors[:, 2] = rgb[2]
|
| 127 |
+
|
| 128 |
+
if location == "bottom":
|
| 129 |
+
my_colors[:, 3] = np.linspace(1, 0, 256)
|
| 130 |
+
extent_y_start = 0
|
| 131 |
+
extent_y_end = 0.25
|
| 132 |
+
else:
|
| 133 |
+
my_colors[:, 3] = np.linspace(0, 1, 256)
|
| 134 |
+
extent_y_start = 0.75
|
| 135 |
+
extent_y_end = 1.0
|
| 136 |
+
|
| 137 |
+
custom_cmap = mcolors.ListedColormap(my_colors)
|
| 138 |
+
|
| 139 |
+
xlim = ax.get_xlim()
|
| 140 |
+
ylim = ax.get_ylim()
|
| 141 |
+
y_range = ylim[1] - ylim[0]
|
| 142 |
+
|
| 143 |
+
y_bottom = ylim[0] + y_range * extent_y_start
|
| 144 |
+
y_top = ylim[0] + y_range * extent_y_end
|
| 145 |
+
|
| 146 |
+
ax.imshow(
|
| 147 |
+
gradient,
|
| 148 |
+
extent=[xlim[0], xlim[1], y_bottom, y_top],
|
| 149 |
+
aspect="auto",
|
| 150 |
+
cmap=custom_cmap,
|
| 151 |
+
zorder=zorder,
|
| 152 |
+
origin="lower",
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def get_edge_colors_by_type(
|
| 157 |
+
G, show_motorway=True, show_primary=True, show_secondary=True
|
| 158 |
+
):
|
| 159 |
+
"""
|
| 160 |
+
Assigns colors to edges based on road type hierarchy.
|
| 161 |
+
Returns a list of colors corresponding to each edge in the graph.
|
| 162 |
+
If a layer is hidden, returns 'none' for that edge.
|
| 163 |
+
"""
|
| 164 |
+
edge_colors = []
|
| 165 |
+
|
| 166 |
+
for u, v, data in G.edges(data=True):
|
| 167 |
+
# Get the highway type (can be a list or string)
|
| 168 |
+
highway = data.get("highway", "unclassified")
|
| 169 |
+
|
| 170 |
+
# Handle list of highway types (take the first one)
|
| 171 |
+
if isinstance(highway, list):
|
| 172 |
+
highway = highway[0] if highway else "unclassified"
|
| 173 |
+
|
| 174 |
+
# Assign color based on road type
|
| 175 |
+
if highway in ["motorway", "motorway_link"]:
|
| 176 |
+
color = THEME["road_motorway"] if show_motorway else "none"
|
| 177 |
+
elif highway in ["trunk", "trunk_link", "primary", "primary_link"]:
|
| 178 |
+
color = THEME["road_primary"] if show_primary else "none"
|
| 179 |
+
elif highway in ["secondary", "secondary_link"]:
|
| 180 |
+
color = THEME["road_secondary"] if show_secondary else "none"
|
| 181 |
+
elif highway in ["tertiary", "tertiary_link"]:
|
| 182 |
+
color = (
|
| 183 |
+
THEME["road_tertiary"] if show_secondary else "none"
|
| 184 |
+
) # Group tertiary with secondary
|
| 185 |
+
elif highway in [
|
| 186 |
+
"residential",
|
| 187 |
+
"living_street",
|
| 188 |
+
"unclassified",
|
| 189 |
+
"service",
|
| 190 |
+
"road",
|
| 191 |
+
]:
|
| 192 |
+
color = THEME["road_residential"]
|
| 193 |
+
elif highway in ["path", "footway", "track", "cycleway", "pedestrian"]:
|
| 194 |
+
# Use residential color or a lighter version if defined, for now residential
|
| 195 |
+
color = THEME["road_residential"]
|
| 196 |
+
else:
|
| 197 |
+
color = THEME["road_default"]
|
| 198 |
+
|
| 199 |
+
edge_colors.append(color)
|
| 200 |
+
|
| 201 |
+
return edge_colors
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def get_edge_widths_by_type(G):
|
| 205 |
+
"""
|
| 206 |
+
Assigns line widths to edges based on road type.
|
| 207 |
+
Major roads get thicker lines.
|
| 208 |
+
"""
|
| 209 |
+
edge_widths = []
|
| 210 |
+
|
| 211 |
+
for u, v, data in G.edges(data=True):
|
| 212 |
+
highway = data.get("highway", "unclassified")
|
| 213 |
+
|
| 214 |
+
if isinstance(highway, list):
|
| 215 |
+
highway = highway[0] if highway else "unclassified"
|
| 216 |
+
|
| 217 |
+
# Assign width based on road importance (increased for visibility)
|
| 218 |
+
if highway in ["motorway", "motorway_link"]:
|
| 219 |
+
width = 1.6
|
| 220 |
+
elif highway in ["trunk", "trunk_link", "primary", "primary_link"]:
|
| 221 |
+
width = 1.3
|
| 222 |
+
elif highway in ["secondary", "secondary_link"]:
|
| 223 |
+
width = 1.0
|
| 224 |
+
elif highway in ["tertiary", "tertiary_link"]:
|
| 225 |
+
width = 0.8
|
| 226 |
+
elif highway in [
|
| 227 |
+
"residential",
|
| 228 |
+
"living_street",
|
| 229 |
+
"unclassified",
|
| 230 |
+
"service",
|
| 231 |
+
"road",
|
| 232 |
+
]:
|
| 233 |
+
width = 0.8
|
| 234 |
+
elif highway in ["path", "footway", "track", "cycleway", "pedestrian"]:
|
| 235 |
+
width = 0.5
|
| 236 |
+
else:
|
| 237 |
+
width = 0.6
|
| 238 |
+
|
| 239 |
+
edge_widths.append(width)
|
| 240 |
+
|
| 241 |
+
return edge_widths
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def has_chinese(text):
|
| 245 |
+
"""Check if a string contains any Chinese characters."""
|
| 246 |
+
if not text:
|
| 247 |
+
return False
|
| 248 |
+
for char in text:
|
| 249 |
+
if "\u4e00" <= char <= "\u9fff":
|
| 250 |
+
return True
|
| 251 |
+
return False
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def get_coordinates(city, country):
|
| 255 |
+
"""
|
| 256 |
+
Fetches coordinates for a given city and country using OSMnx (which handles caching and retry).
|
| 257 |
+
First checks for manual overrides in cities_data.
|
| 258 |
+
"""
|
| 259 |
+
from cities_data import get_manual_coordinates
|
| 260 |
+
|
| 261 |
+
# Check for manual override first (e.g. for large cities like Beijing)
|
| 262 |
+
manual_coords = get_manual_coordinates(city)
|
| 263 |
+
if manual_coords:
|
| 264 |
+
print(f"✓ Using manual coordinate override for {city}: {manual_coords}")
|
| 265 |
+
return manual_coords
|
| 266 |
+
|
| 267 |
+
print(f"Looking up coordinates for {city}, {country} via OSMnx...")
|
| 268 |
+
|
| 269 |
+
# Configure OSMnx settings for robustness
|
| 270 |
+
ox.settings.timeout = 30
|
| 271 |
+
ox.settings.user_agent = "modelscope_map_to_poster/1.0"
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
# Try City + Country
|
| 275 |
+
query = f"{city}, {country}"
|
| 276 |
+
lat, lon = ox.geocode(query)
|
| 277 |
+
print(f"✓ Found: {lat}, {lon}")
|
| 278 |
+
return (lat, lon)
|
| 279 |
+
except Exception as e:
|
| 280 |
+
print(f"⚠ First attempt failed: {e}")
|
| 281 |
+
try:
|
| 282 |
+
# Try just City
|
| 283 |
+
print(f"Retrying with city name only: {city}")
|
| 284 |
+
lat, lon = ox.geocode(city)
|
| 285 |
+
print(f"✓ Found: {lat}, {lon}")
|
| 286 |
+
return (lat, lon)
|
| 287 |
+
except Exception as e2:
|
| 288 |
+
print(f"⚠ Second attempt failed: {e2}")
|
| 289 |
+
raise ValueError(f"Could not find coordinates for {city}, {country}")
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def create_poster(
|
| 293 |
+
city,
|
| 294 |
+
country,
|
| 295 |
+
point,
|
| 296 |
+
dist,
|
| 297 |
+
output_file,
|
| 298 |
+
output_format,
|
| 299 |
+
width=12,
|
| 300 |
+
height=16,
|
| 301 |
+
no_crop=False,
|
| 302 |
+
show_motorway=True,
|
| 303 |
+
show_primary=True,
|
| 304 |
+
show_secondary=True,
|
| 305 |
+
show_water=True,
|
| 306 |
+
show_parks=True,
|
| 307 |
+
):
|
| 308 |
+
msg = f"Generating map for {city}, {country}..."
|
| 309 |
+
print(f"\n{msg}")
|
| 310 |
+
yield msg
|
| 311 |
+
|
| 312 |
+
# Calculate non-square distances to match figure aspect ratio
|
| 313 |
+
# dist is the vertical (North-South) half-distance
|
| 314 |
+
dist_ns = dist
|
| 315 |
+
dist_ew = dist * (width / height)
|
| 316 |
+
|
| 317 |
+
# Progress bar for data fetching
|
| 318 |
+
# Note: tqdm writes to stderr, we will just yield status updates for the UI
|
| 319 |
+
|
| 320 |
+
# 1. Fetch Street Network using a bounding box
|
| 321 |
+
yield "Downloading street network..."
|
| 322 |
+
|
| 323 |
+
import math
|
| 324 |
+
|
| 325 |
+
lat, lon = point
|
| 326 |
+
delta_lat = dist_ns / 111320.0
|
| 327 |
+
delta_lon = dist_ew / (111320.0 * math.cos(math.radians(lat)))
|
| 328 |
+
|
| 329 |
+
north, south = lat + delta_lat, lat - delta_lat
|
| 330 |
+
west, east = lon - delta_lon, lon + delta_lon
|
| 331 |
+
|
| 332 |
+
bbox = (west, south, east, north)
|
| 333 |
+
G = ox.graph_from_bbox(bbox, network_type="all")
|
| 334 |
+
|
| 335 |
+
# 2. Fetch Water and Parks in one request
|
| 336 |
+
yield "Downloading features (water, parks)..."
|
| 337 |
+
tags = {
|
| 338 |
+
"natural": ["water", "wood", "scrub"],
|
| 339 |
+
"waterway": ["riverbank", "dock"],
|
| 340 |
+
"leisure": ["park", "garden", "nature_reserve"],
|
| 341 |
+
"landuse": [
|
| 342 |
+
"forest",
|
| 343 |
+
"grass",
|
| 344 |
+
"cemetery",
|
| 345 |
+
"recreation_ground",
|
| 346 |
+
"village_green",
|
| 347 |
+
],
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
water_polys = None
|
| 351 |
+
water_lines = None
|
| 352 |
+
parks = None
|
| 353 |
+
|
| 354 |
+
try:
|
| 355 |
+
features = ox.features_from_bbox(bbox, tags=tags)
|
| 356 |
+
|
| 357 |
+
# Separate features and simplify geometries for faster rendering
|
| 358 |
+
if features is not None and not features.empty:
|
| 359 |
+
# Safe mask creation for Water
|
| 360 |
+
water_mask = pd.Series(False, index=features.index)
|
| 361 |
+
if "natural" in features.columns:
|
| 362 |
+
water_mask |= features["natural"].isin(["water"])
|
| 363 |
+
if "waterway" in features.columns:
|
| 364 |
+
water_mask |= features["waterway"].notna()
|
| 365 |
+
|
| 366 |
+
water = features[water_mask]
|
| 367 |
+
|
| 368 |
+
if not water.empty:
|
| 369 |
+
# Water Polygons (Lakes, Wide Rivers)
|
| 370 |
+
water_polys = water[
|
| 371 |
+
water.geometry.type.isin(["Polygon", "MultiPolygon"])
|
| 372 |
+
]
|
| 373 |
+
if not water_polys.empty:
|
| 374 |
+
water_polys.geometry = water_polys.geometry.simplify(
|
| 375 |
+
tolerance=0.00001, preserve_topology=True
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
# Water Lines (Rivers, Streams represented as lines)
|
| 379 |
+
water_lines = water[
|
| 380 |
+
water.geometry.type.isin(["LineString", "MultiLineString"])
|
| 381 |
+
]
|
| 382 |
+
else:
|
| 383 |
+
water_polys, water_lines = None, None # No water features found
|
| 384 |
+
|
| 385 |
+
# Green space/Parks features
|
| 386 |
+
park_mask = pd.Series(False, index=features.index)
|
| 387 |
+
if "leisure" in features.columns:
|
| 388 |
+
park_mask |= features["leisure"].notna()
|
| 389 |
+
if "landuse" in features.columns:
|
| 390 |
+
park_mask |= features["landuse"].isin(
|
| 391 |
+
[
|
| 392 |
+
"forest",
|
| 393 |
+
"grass",
|
| 394 |
+
"cemetery",
|
| 395 |
+
"recreation_ground",
|
| 396 |
+
"village_green",
|
| 397 |
+
]
|
| 398 |
+
)
|
| 399 |
+
if "natural" in features.columns:
|
| 400 |
+
park_mask |= features["natural"].isin(["wood", "scrub"])
|
| 401 |
+
|
| 402 |
+
parks = features[park_mask]
|
| 403 |
+
if not parks.empty:
|
| 404 |
+
parks = parks[parks.geometry.type.isin(["Polygon", "MultiPolygon"])]
|
| 405 |
+
# Simplify geometries
|
| 406 |
+
parks.geometry = parks.geometry.simplify(
|
| 407 |
+
tolerance=0.00001, preserve_topology=True
|
| 408 |
+
)
|
| 409 |
+
else:
|
| 410 |
+
parks = None # No park features found
|
| 411 |
+
else:
|
| 412 |
+
water_polys, water_lines, parks = None, None, None
|
| 413 |
+
except Exception as e:
|
| 414 |
+
print(f"Warning: Could not fetch features: {e}")
|
| 415 |
+
water_polys, water_lines, parks = None, None, None
|
| 416 |
+
|
| 417 |
+
print("✓ All data downloaded successfully!")
|
| 418 |
+
yield "Data downloaded. Rendering map..."
|
| 419 |
+
|
| 420 |
+
# 2. Setup Plot
|
| 421 |
+
print("Rendering map...")
|
| 422 |
+
fig, ax = plt.subplots(figsize=(width, height), facecolor=THEME["bg"])
|
| 423 |
+
ax.set_facecolor(THEME["bg"])
|
| 424 |
+
ax.set_position([0, 0, 1, 1])
|
| 425 |
+
|
| 426 |
+
# 3. Plot Layers
|
| 427 |
+
# Layer 1: Polygons (filter to only plot polygon/multipolygon geometries, not points)
|
| 428 |
+
# Layer 1: Water (Polygons and Lines)
|
| 429 |
+
if show_water:
|
| 430 |
+
if water_polys is not None and not water_polys.empty:
|
| 431 |
+
water_polys.plot(
|
| 432 |
+
ax=ax, facecolor=THEME["water"], edgecolor="none", zorder=1
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
if water_lines is not None and not water_lines.empty:
|
| 436 |
+
# Plot water lines (rivers) with some thickness
|
| 437 |
+
water_lines.plot(ax=ax, color=THEME["water"], linewidth=2.0, zorder=1)
|
| 438 |
+
|
| 439 |
+
if show_parks and parks is not None and not parks.empty:
|
| 440 |
+
# Filter to only polygon/multipolygon geometries to avoid point features showing as dots
|
| 441 |
+
parks_polys = parks[parks.geometry.type.isin(["Polygon", "MultiPolygon"])]
|
| 442 |
+
if not parks_polys.empty:
|
| 443 |
+
parks_polys.plot(
|
| 444 |
+
ax=ax, facecolor=THEME["parks"], edgecolor="none", zorder=2
|
| 445 |
+
)
|
| 446 |
+
|
| 447 |
+
# Layer 2: Roads with hierarchy coloring
|
| 448 |
+
print("Applying road hierarchy colors...")
|
| 449 |
+
yield "Applying road styles..."
|
| 450 |
+
edge_colors = get_edge_colors_by_type(
|
| 451 |
+
G,
|
| 452 |
+
show_motorway=show_motorway,
|
| 453 |
+
show_primary=show_primary,
|
| 454 |
+
show_secondary=show_secondary,
|
| 455 |
+
)
|
| 456 |
+
edge_widths = get_edge_widths_by_type(G)
|
| 457 |
+
|
| 458 |
+
ox.plot_graph(
|
| 459 |
+
G,
|
| 460 |
+
ax=ax,
|
| 461 |
+
bgcolor=THEME["bg"],
|
| 462 |
+
node_size=0,
|
| 463 |
+
edge_color=edge_colors,
|
| 464 |
+
edge_linewidth=edge_widths,
|
| 465 |
+
show=False,
|
| 466 |
+
close=False,
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
# Layer 3: Gradients (Top and Bottom)
|
| 470 |
+
create_gradient_fade(ax, THEME["gradient_color"], location="bottom", zorder=10)
|
| 471 |
+
create_gradient_fade(ax, THEME["gradient_color"], location="top", zorder=10)
|
| 472 |
+
|
| 473 |
+
# 4. Typography
|
| 474 |
+
is_chinese = has_chinese(city)
|
| 475 |
+
|
| 476 |
+
if FONTS:
|
| 477 |
+
if is_chinese:
|
| 478 |
+
# Chinese styling
|
| 479 |
+
font_path = FONTS.get("cn") or FONTS.get("en_bold")
|
| 480 |
+
font_main_base = FontProperties(fname=font_path)
|
| 481 |
+
font_sub_base = FontProperties(fname=font_path)
|
| 482 |
+
font_coords = FontProperties(fname=font_path, size=14)
|
| 483 |
+
|
| 484 |
+
display_city = city
|
| 485 |
+
display_country = country
|
| 486 |
+
else:
|
| 487 |
+
# English styling (Original)
|
| 488 |
+
font_path_bold = FONTS.get("en_bold")
|
| 489 |
+
font_path_reg = FONTS.get("en_regular")
|
| 490 |
+
font_main_base = FontProperties(fname=font_path_bold)
|
| 491 |
+
font_sub_base = FontProperties(fname=font_path_reg)
|
| 492 |
+
font_coords = FontProperties(fname=font_path_reg, size=14)
|
| 493 |
+
|
| 494 |
+
# Spaced and uppercase for English
|
| 495 |
+
display_city = " ".join(list(city.upper()))
|
| 496 |
+
display_country = country.upper()
|
| 497 |
+
else:
|
| 498 |
+
# Fallback to system fonts
|
| 499 |
+
font_main_base = FontProperties(family="serif", weight="bold")
|
| 500 |
+
font_sub_base = FontProperties(family="serif")
|
| 501 |
+
font_coords = FontProperties(family="monospace", size=14)
|
| 502 |
+
|
| 503 |
+
if is_chinese:
|
| 504 |
+
display_city = city
|
| 505 |
+
display_country = country
|
| 506 |
+
else:
|
| 507 |
+
display_city = " ".join(list(city.upper()))
|
| 508 |
+
display_country = country.upper()
|
| 509 |
+
|
| 510 |
+
# Dynamically adjust font size
|
| 511 |
+
base_font_size = 60 if not is_chinese else 54
|
| 512 |
+
city_char_count = len(city)
|
| 513 |
+
if not is_chinese and city_char_count > 10:
|
| 514 |
+
scale_factor = 10 / city_char_count
|
| 515 |
+
adjusted_font_size = max(base_font_size * scale_factor, 24)
|
| 516 |
+
elif is_chinese and city_char_count > 6:
|
| 517 |
+
scale_factor = 6 / city_char_count
|
| 518 |
+
adjusted_font_size = max(base_font_size * scale_factor, 32)
|
| 519 |
+
else:
|
| 520 |
+
adjusted_font_size = base_font_size
|
| 521 |
+
|
| 522 |
+
font_main = font_main_base.copy()
|
| 523 |
+
font_main.set_size(adjusted_font_size)
|
| 524 |
+
|
| 525 |
+
font_sub = font_sub_base.copy()
|
| 526 |
+
font_sub.set_size(22)
|
| 527 |
+
|
| 528 |
+
# --- BOTTOM TEXT ---
|
| 529 |
+
ax.text(
|
| 530 |
+
0.5,
|
| 531 |
+
0.14,
|
| 532 |
+
display_city,
|
| 533 |
+
transform=ax.transAxes,
|
| 534 |
+
color=THEME["text"],
|
| 535 |
+
ha="center",
|
| 536 |
+
fontproperties=font_main,
|
| 537 |
+
zorder=11,
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
ax.text(
|
| 541 |
+
0.5,
|
| 542 |
+
0.10,
|
| 543 |
+
display_country,
|
| 544 |
+
transform=ax.transAxes,
|
| 545 |
+
color=THEME["text"],
|
| 546 |
+
ha="center",
|
| 547 |
+
fontproperties=font_sub,
|
| 548 |
+
zorder=11,
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
lat, lon = point
|
| 552 |
+
coords_text = (
|
| 553 |
+
f"{lat:.4f}° N / {lon:.4f}° E"
|
| 554 |
+
if lat >= 0
|
| 555 |
+
else f"{abs(lat):.4f}° S / {lon:.4f}° E"
|
| 556 |
+
)
|
| 557 |
+
if lon < 0:
|
| 558 |
+
coords_text = coords_text.replace("E", "W")
|
| 559 |
+
|
| 560 |
+
ax.text(
|
| 561 |
+
0.5,
|
| 562 |
+
0.07,
|
| 563 |
+
coords_text,
|
| 564 |
+
transform=ax.transAxes,
|
| 565 |
+
color=THEME["text"],
|
| 566 |
+
alpha=0.7,
|
| 567 |
+
ha="center",
|
| 568 |
+
fontproperties=font_coords,
|
| 569 |
+
zorder=11,
|
| 570 |
+
)
|
| 571 |
+
|
| 572 |
+
ax.plot(
|
| 573 |
+
[0.4, 0.6],
|
| 574 |
+
[0.125, 0.125],
|
| 575 |
+
transform=ax.transAxes,
|
| 576 |
+
color=THEME["text"],
|
| 577 |
+
linewidth=1,
|
| 578 |
+
zorder=11,
|
| 579 |
+
)
|
| 580 |
+
|
| 581 |
+
# --- ATTRIBUTION (bottom right) ---
|
| 582 |
+
attr_font = font_sub_base.copy()
|
| 583 |
+
attr_font.set_size(8)
|
| 584 |
+
|
| 585 |
+
ax.text(
|
| 586 |
+
0.98,
|
| 587 |
+
0.02,
|
| 588 |
+
"© OpenStreetMap contributors",
|
| 589 |
+
transform=ax.transAxes,
|
| 590 |
+
color=THEME["text"],
|
| 591 |
+
alpha=0.5,
|
| 592 |
+
ha="right",
|
| 593 |
+
va="bottom",
|
| 594 |
+
fontproperties=attr_font,
|
| 595 |
+
zorder=11,
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
# 5. Save
|
| 599 |
+
print(f"Saving to {output_file}...")
|
| 600 |
+
yield f"Saving to {output_file}..."
|
| 601 |
+
|
| 602 |
+
fmt = output_format.lower()
|
| 603 |
+
save_kwargs = dict(facecolor=THEME["bg"], pad_inches=0.05)
|
| 604 |
+
|
| 605 |
+
if not no_crop:
|
| 606 |
+
save_kwargs["bbox_inches"] = "tight"
|
| 607 |
+
|
| 608 |
+
# DPI matters mainly for raster formats
|
| 609 |
+
if fmt == "png":
|
| 610 |
+
save_kwargs["dpi"] = 300
|
| 611 |
+
|
| 612 |
+
plt.savefig(output_file, format=fmt, **save_kwargs)
|
| 613 |
+
|
| 614 |
+
plt.close()
|
| 615 |
+
print(f"✓ Done! Poster saved as {output_file}")
|
| 616 |
+
yield "Done!"
|
| 617 |
+
|
| 618 |
+
|
| 619 |
+
def print_examples():
|
| 620 |
+
"""Print usage examples."""
|
| 621 |
+
print("""
|
| 622 |
+
City Map Poster Generator
|
| 623 |
+
=========================
|
| 624 |
+
|
| 625 |
+
Usage:
|
| 626 |
+
python create_map_poster.py --city <city> --country <country> [options]
|
| 627 |
+
|
| 628 |
+
Examples:
|
| 629 |
+
# Iconic grid patterns
|
| 630 |
+
python create_map_poster.py -c "New York" -C "USA" -t noir -d 12000 # Manhattan grid
|
| 631 |
+
python create_map_poster.py -c "Barcelona" -C "Spain" -t warm_beige -d 8000 # Eixample district grid
|
| 632 |
+
|
| 633 |
+
# Waterfront & canals
|
| 634 |
+
python create_map_poster.py -c "Venice" -C "Italy" -t blueprint -d 4000 # Canal network
|
| 635 |
+
python create_map_poster.py -c "Amsterdam" -C "Netherlands" -t ocean -d 6000 # Concentric canals
|
| 636 |
+
python create_map_poster.py -c "Dubai" -C "UAE" -t midnight_blue -d 15000 # Palm & coastline
|
| 637 |
+
|
| 638 |
+
# Radial patterns
|
| 639 |
+
python create_map_poster.py -c "Paris" -C "France" -t pastel_dream -d 10000 # Haussmann boulevards
|
| 640 |
+
python create_map_poster.py -c "Moscow" -C "Russia" -t noir -d 12000 # Ring roads
|
| 641 |
+
|
| 642 |
+
# Organic old cities
|
| 643 |
+
python create_map_poster.py -c "Tokyo" -C "Japan" -t japanese_ink -d 15000 # Dense organic streets
|
| 644 |
+
python create_map_poster.py -c "Marrakech" -C "Morocco" -t terracotta -d 5000 # Medina maze
|
| 645 |
+
python create_map_poster.py -c "Rome" -C "Italy" -t warm_beige -d 8000 # Ancient street layout
|
| 646 |
+
|
| 647 |
+
# Coastal cities
|
| 648 |
+
python create_map_poster.py -c "San Francisco" -C "USA" -t sunset -d 10000 # Peninsula grid
|
| 649 |
+
python create_map_poster.py -c "Sydney" -C "Australia" -t ocean -d 12000 # Harbor city
|
| 650 |
+
python create_map_poster.py -c "Mumbai" -C "India" -t contrast_zones -d 18000 # Coastal peninsula
|
| 651 |
+
|
| 652 |
+
# River cities
|
| 653 |
+
python create_map_poster.py -c "London" -C "UK" -t noir -d 15000 # Thames curves
|
| 654 |
+
python create_map_poster.py -c "Budapest" -C "Hungary" -t copper_patina -d 8000 # Danube split
|
| 655 |
+
|
| 656 |
+
# List themes
|
| 657 |
+
python create_map_poster.py --list-themes
|
| 658 |
+
|
| 659 |
+
Options:
|
| 660 |
+
--city, -c City name (required)
|
| 661 |
+
--country, -C Country name (required)
|
| 662 |
+
--theme, -t Theme name (default: feature_based)
|
| 663 |
+
--distance, -d Map radius in meters (default: 29000)
|
| 664 |
+
--list-themes List all available themes
|
| 665 |
+
|
| 666 |
+
Distance guide:
|
| 667 |
+
4000-6000m Small/dense cities (Venice, Amsterdam old center)
|
| 668 |
+
8000-12000m Medium cities, focused downtown (Paris, Barcelona)
|
| 669 |
+
15000-20000m Large metros, full city view (Tokyo, Mumbai)
|
| 670 |
+
|
| 671 |
+
Available themes can be found in the 'themes/' directory.
|
| 672 |
+
Generated posters are saved to 'posters/' directory.
|
| 673 |
+
""")
|
| 674 |
+
|
| 675 |
+
|
| 676 |
+
def list_themes():
|
| 677 |
+
"""List all available themes with descriptions."""
|
| 678 |
+
available_themes = get_available_themes()
|
| 679 |
+
if not available_themes:
|
| 680 |
+
print("No themes found in 'themes/' directory.")
|
| 681 |
+
return
|
| 682 |
+
|
| 683 |
+
print("\nAvailable Themes:")
|
| 684 |
+
print("-" * 60)
|
| 685 |
+
for theme_name in available_themes:
|
| 686 |
+
theme_path = os.path.join(THEMES_DIR, f"{theme_name}.json")
|
| 687 |
+
try:
|
| 688 |
+
with open(theme_path, "r") as f:
|
| 689 |
+
theme_data = json.load(f)
|
| 690 |
+
display_name = theme_data.get("name", theme_name)
|
| 691 |
+
description = theme_data.get("description", "")
|
| 692 |
+
except:
|
| 693 |
+
display_name = theme_name
|
| 694 |
+
description = ""
|
| 695 |
+
print(f" {theme_name}")
|
| 696 |
+
print(f" {display_name}")
|
| 697 |
+
if description:
|
| 698 |
+
print(f" {description}")
|
| 699 |
+
print()
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
if __name__ == "__main__":
|
| 703 |
+
parser = argparse.ArgumentParser(
|
| 704 |
+
description="Generate beautiful map posters for any city",
|
| 705 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 706 |
+
epilog="""
|
| 707 |
+
Examples:
|
| 708 |
+
python create_map_poster.py --city "New York" --country "USA"
|
| 709 |
+
python create_map_poster.py --city Tokyo --country Japan --theme midnight_blue
|
| 710 |
+
python create_map_poster.py --city Paris --country France --theme noir --distance 15000
|
| 711 |
+
python create_map_poster.py --list-themes
|
| 712 |
+
""",
|
| 713 |
+
)
|
| 714 |
+
|
| 715 |
+
parser.add_argument("--city", "-c", type=str, help="City name")
|
| 716 |
+
parser.add_argument("--country", "-C", type=str, help="Country name")
|
| 717 |
+
parser.add_argument(
|
| 718 |
+
"--theme",
|
| 719 |
+
"-t",
|
| 720 |
+
type=str,
|
| 721 |
+
default="feature_based",
|
| 722 |
+
help="Theme name (default: feature_based)",
|
| 723 |
+
)
|
| 724 |
+
parser.add_argument(
|
| 725 |
+
"--distance",
|
| 726 |
+
"-d",
|
| 727 |
+
type=int,
|
| 728 |
+
default=10000,
|
| 729 |
+
help="Map radius in meters (default: 10000)",
|
| 730 |
+
)
|
| 731 |
+
parser.add_argument(
|
| 732 |
+
"--width",
|
| 733 |
+
"-W",
|
| 734 |
+
type=float,
|
| 735 |
+
default=12.0,
|
| 736 |
+
help="Poster width in inches (default: 12.0)",
|
| 737 |
+
)
|
| 738 |
+
parser.add_argument(
|
| 739 |
+
"--height",
|
| 740 |
+
"-H",
|
| 741 |
+
type=float,
|
| 742 |
+
default=16.0,
|
| 743 |
+
help="Poster height in inches (default: 16.0)",
|
| 744 |
+
)
|
| 745 |
+
parser.add_argument(
|
| 746 |
+
"--no-crop",
|
| 747 |
+
action="store_true",
|
| 748 |
+
help="Do not crop the image to the data extent (keeps background)",
|
| 749 |
+
)
|
| 750 |
+
parser.add_argument(
|
| 751 |
+
"--list-themes", action="store_true", help="List all available themes"
|
| 752 |
+
)
|
| 753 |
+
parser.add_argument(
|
| 754 |
+
"--format",
|
| 755 |
+
"-f",
|
| 756 |
+
default="png",
|
| 757 |
+
choices=["png", "svg", "pdf"],
|
| 758 |
+
help="Output format for the poster (default: png)",
|
| 759 |
+
)
|
| 760 |
+
|
| 761 |
+
args = parser.parse_args()
|
| 762 |
+
|
| 763 |
+
# If no arguments provided, show examples
|
| 764 |
+
if len(os.sys.argv) == 1:
|
| 765 |
+
print_examples()
|
| 766 |
+
os.sys.exit(0)
|
| 767 |
+
|
| 768 |
+
# List themes if requested
|
| 769 |
+
if args.list_themes:
|
| 770 |
+
list_themes()
|
| 771 |
+
os.sys.exit(0)
|
| 772 |
+
|
| 773 |
+
# Validate required arguments
|
| 774 |
+
if not args.city or not args.country:
|
| 775 |
+
print("Error: --city and --country are required.\n")
|
| 776 |
+
print_examples()
|
| 777 |
+
os.sys.exit(1)
|
| 778 |
+
|
| 779 |
+
# Validate theme exists
|
| 780 |
+
available_themes = get_available_themes()
|
| 781 |
+
if args.theme not in available_themes:
|
| 782 |
+
print(f"Error: Theme '{args.theme}' not found.")
|
| 783 |
+
print(f"Available themes: {', '.join(available_themes)}")
|
| 784 |
+
os.sys.exit(1)
|
| 785 |
+
|
| 786 |
+
print("=" * 50)
|
| 787 |
+
print("City Map Poster Generator")
|
| 788 |
+
print("=" * 50)
|
| 789 |
+
|
| 790 |
+
# Load theme
|
| 791 |
+
THEME = load_theme(args.theme)
|
| 792 |
+
|
| 793 |
+
# Get coordinates and generate poster
|
| 794 |
+
try:
|
| 795 |
+
coords = get_coordinates(args.city, args.country)
|
| 796 |
+
output_file = generate_output_filename(args.city, args.theme, args.format)
|
| 797 |
+
|
| 798 |
+
# Iterate over the generator to execute it
|
| 799 |
+
for status in create_poster(
|
| 800 |
+
args.city,
|
| 801 |
+
args.country,
|
| 802 |
+
coords,
|
| 803 |
+
args.distance,
|
| 804 |
+
output_file,
|
| 805 |
+
args.format,
|
| 806 |
+
width=args.width,
|
| 807 |
+
height=args.height,
|
| 808 |
+
no_crop=args.no_crop,
|
| 809 |
+
):
|
| 810 |
+
# We already print inside the function, but we act as a consumer here
|
| 811 |
+
pass
|
| 812 |
+
|
| 813 |
+
print("\n" + "=" * 50)
|
| 814 |
+
print("✓ Poster generation complete!")
|
| 815 |
+
print("=" * 50)
|
| 816 |
+
|
| 817 |
+
except Exception as e:
|
| 818 |
+
print(f"\n✗ Error: {e}")
|
| 819 |
+
import traceback
|
| 820 |
+
|
| 821 |
+
traceback.print_exc()
|
| 822 |
+
os.sys.exit(1)
|
fonts/GoudyOldStyle-Bold.ttf
ADDED
|
Binary file (82.8 kB). View file
|
|
|
fonts/GoudyOldStyle-Regular.ttf
ADDED
|
Binary file (81.4 kB). View file
|
|
|
fonts/HYWenRunSongYunU.ttf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c0d3409418059e092ddb5e8d71139c76025fd3017d07d4a7440474e9e8477437
|
| 3 |
+
size 49883792
|
pyproject.toml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "maptoposter"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
authors = [
|
| 7 |
+
{ name = "IsaacHuo", email = "“2210286979@qq.com" }
|
| 8 |
+
]
|
| 9 |
+
requires-python = ">=3.12"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"certifi==2026.1.4",
|
| 12 |
+
"charset-normalizer==3.4.4",
|
| 13 |
+
"contourpy==1.3.3",
|
| 14 |
+
"cycler==0.12.1",
|
| 15 |
+
"fonttools==4.61.1",
|
| 16 |
+
"geographiclib==2.1",
|
| 17 |
+
"geopandas==1.1.2",
|
| 18 |
+
"geopy==2.4.1",
|
| 19 |
+
"idna==3.11",
|
| 20 |
+
"kiwisolver==1.4.9",
|
| 21 |
+
"matplotlib==3.10.8",
|
| 22 |
+
"networkx==3.6.1",
|
| 23 |
+
"numpy==2.4.0",
|
| 24 |
+
"osmnx==2.0.7",
|
| 25 |
+
"packaging==25.0",
|
| 26 |
+
"pandas==2.3.3",
|
| 27 |
+
"pillow==12.1.0",
|
| 28 |
+
"pyogrio==0.12.1",
|
| 29 |
+
"pyparsing==3.3.1",
|
| 30 |
+
"pyproj==3.7.2",
|
| 31 |
+
"python-dateutil==2.9.0.post0",
|
| 32 |
+
"pytz==2025.2",
|
| 33 |
+
"requests==2.32.5",
|
| 34 |
+
"scipy==1.16.3",
|
| 35 |
+
"shapely==2.1.2",
|
| 36 |
+
"six==1.17.0",
|
| 37 |
+
"tqdm==4.67.1",
|
| 38 |
+
"tzdata==2025.3",
|
| 39 |
+
"urllib3==2.6.3",
|
| 40 |
+
"gradio>=4.0.0",
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
[build-system]
|
| 44 |
+
requires = ["uv_build>=0.9.24,<0.10.0"]
|
| 45 |
+
build-backend = "uv_build"
|
requirements.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
certifi==2026.1.4
|
| 2 |
+
charset-normalizer==3.4.4
|
| 3 |
+
contourpy==1.3.3
|
| 4 |
+
cycler==0.12.1
|
| 5 |
+
fonttools==4.61.1
|
| 6 |
+
geographiclib==2.1
|
| 7 |
+
geopandas==1.1.2
|
| 8 |
+
geopy==2.4.1
|
| 9 |
+
idna==3.11
|
| 10 |
+
kiwisolver==1.4.9
|
| 11 |
+
matplotlib==3.10.8
|
| 12 |
+
networkx==3.6.1
|
| 13 |
+
numpy==2.4.0
|
| 14 |
+
osmnx==2.0.7
|
| 15 |
+
packaging==25.0
|
| 16 |
+
pandas==2.3.3
|
| 17 |
+
pillow==12.1.0
|
| 18 |
+
pyogrio==0.12.1
|
| 19 |
+
pyparsing==3.3.1
|
| 20 |
+
pyproj==3.7.2
|
| 21 |
+
python-dateutil==2.9.0.post0
|
| 22 |
+
pytz==2025.2
|
| 23 |
+
requests==2.32.5
|
| 24 |
+
scipy==1.16.3
|
| 25 |
+
shapely==2.1.2
|
| 26 |
+
six==1.17.0
|
| 27 |
+
tqdm==4.67.1
|
| 28 |
+
tzdata==2025.3
|
| 29 |
+
urllib3==2.6.3
|
restart.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Port to check
|
| 4 |
+
PORT=7860
|
| 5 |
+
|
| 6 |
+
echo "Checking for processes on port $PORT..."
|
| 7 |
+
|
| 8 |
+
# Find and kill processes using the port
|
| 9 |
+
PIDS=$(lsof -t -i:$PORT)
|
| 10 |
+
|
| 11 |
+
if [ -n "$PIDS" ]; then
|
| 12 |
+
echo "Killing processes on port $PORT: $PIDS"
|
| 13 |
+
kill -9 $PIDS
|
| 14 |
+
sleep 1
|
| 15 |
+
else
|
| 16 |
+
echo "No processes found on port $PORT."
|
| 17 |
+
fi
|
| 18 |
+
|
| 19 |
+
# Path to python in venv
|
| 20 |
+
VENV_PYTHON="./.venv/bin/python"
|
| 21 |
+
|
| 22 |
+
if [ -f "$VENV_PYTHON" ]; then
|
| 23 |
+
echo "Starting application with virtual environment..."
|
| 24 |
+
$VENV_PYTHON app.py
|
| 25 |
+
else
|
| 26 |
+
echo "Virtual environment not found, trying system python..."
|
| 27 |
+
python3 app.py
|
| 28 |
+
fi
|
src/maptoposter/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def hello() -> str:
|
| 2 |
+
return "Hello from maptoposter!"
|
src/maptoposter/py.typed
ADDED
|
File without changes
|
themes/autumn.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Autumn",
|
| 3 |
+
"description": "Burnt oranges, deep reds, golden yellows - seasonal warmth",
|
| 4 |
+
"bg": "#FBF7F0",
|
| 5 |
+
"text": "#8B4513",
|
| 6 |
+
"gradient_color": "#FBF7F0",
|
| 7 |
+
"water": "#D8CFC0",
|
| 8 |
+
"parks": "#E8E0D0",
|
| 9 |
+
"road_motorway": "#701A00",
|
| 10 |
+
"road_primary": "#8B2500",
|
| 11 |
+
"road_secondary": "#B8450A",
|
| 12 |
+
"road_tertiary": "#CC7A30",
|
| 13 |
+
"road_residential": "#C9A050",
|
| 14 |
+
"road_default": "#B8450A"
|
| 15 |
+
}
|
themes/blueprint.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Blueprint",
|
| 3 |
+
"description": "Classic architectural blueprint - technical drawing aesthetic",
|
| 4 |
+
"bg": "#1A3A5C",
|
| 5 |
+
"text": "#E8F4FF",
|
| 6 |
+
"gradient_color": "#1A3A5C",
|
| 7 |
+
"water": "#0F2840",
|
| 8 |
+
"parks": "#1E4570",
|
| 9 |
+
"road_motorway": "#E8F4FF",
|
| 10 |
+
"road_primary": "#C5DCF0",
|
| 11 |
+
"road_secondary": "#9FC5E8",
|
| 12 |
+
"road_tertiary": "#7BAED4",
|
| 13 |
+
"road_residential": "#5A96C0",
|
| 14 |
+
"road_default": "#7BAED4"
|
| 15 |
+
}
|
themes/contrast_zones.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Contrast Zones",
|
| 3 |
+
"description": "Strong contrast showing urban density - darker in center, lighter at edges",
|
| 4 |
+
"bg": "#FFFFFF",
|
| 5 |
+
"text": "#000000",
|
| 6 |
+
"gradient_color": "#FFFFFF",
|
| 7 |
+
"water": "#B0B0B0",
|
| 8 |
+
"parks": "#ECECEC",
|
| 9 |
+
"road_motorway": "#000000",
|
| 10 |
+
"road_primary": "#0F0F0F",
|
| 11 |
+
"road_secondary": "#252525",
|
| 12 |
+
"road_tertiary": "#404040",
|
| 13 |
+
"road_residential": "#5A5A5A",
|
| 14 |
+
"road_default": "#404040"
|
| 15 |
+
}
|
themes/copper_patina.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Copper Patina",
|
| 3 |
+
"description": "Oxidized copper aesthetic - teal-green patina with copper accents",
|
| 4 |
+
"bg": "#E8F0F0",
|
| 5 |
+
"text": "#2A5A5A",
|
| 6 |
+
"gradient_color": "#E8F0F0",
|
| 7 |
+
"water": "#C0D8D8",
|
| 8 |
+
"parks": "#D8E8E0",
|
| 9 |
+
"road_motorway": "#B87333",
|
| 10 |
+
"road_primary": "#5A8A8A",
|
| 11 |
+
"road_secondary": "#6B9E9E",
|
| 12 |
+
"road_tertiary": "#88B4B4",
|
| 13 |
+
"road_residential": "#A8CCCC",
|
| 14 |
+
"road_default": "#88B4B4"
|
| 15 |
+
}
|
themes/feature_based.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Feature-Based Shading",
|
| 3 |
+
"description": "Different shades for different road types and features with clear hierarchy",
|
| 4 |
+
"bg": "#FFFFFF",
|
| 5 |
+
"text": "#000000",
|
| 6 |
+
"gradient_color": "#FFFFFF",
|
| 7 |
+
"water": "#C0C0C0",
|
| 8 |
+
"parks": "#F0F0F0",
|
| 9 |
+
"road_motorway": "#0A0A0A",
|
| 10 |
+
"road_primary": "#1A1A1A",
|
| 11 |
+
"road_secondary": "#2A2A2A",
|
| 12 |
+
"road_tertiary": "#3A3A3A",
|
| 13 |
+
"road_residential": "#4A4A4A",
|
| 14 |
+
"road_default": "#3A3A3A"
|
| 15 |
+
}
|
themes/forest.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Forest",
|
| 3 |
+
"description": "Deep greens and sage tones - organic botanical aesthetic",
|
| 4 |
+
"bg": "#F0F4F0",
|
| 5 |
+
"text": "#2D4A3E",
|
| 6 |
+
"gradient_color": "#F0F4F0",
|
| 7 |
+
"water": "#B8D4D4",
|
| 8 |
+
"parks": "#D4E8D4",
|
| 9 |
+
"road_motorway": "#2D4A3E",
|
| 10 |
+
"road_primary": "#3D6B55",
|
| 11 |
+
"road_secondary": "#5A8A70",
|
| 12 |
+
"road_tertiary": "#7AAA90",
|
| 13 |
+
"road_residential": "#A0C8B0",
|
| 14 |
+
"road_default": "#7AAA90"
|
| 15 |
+
}
|
themes/gradient_roads.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Gradient Roads",
|
| 3 |
+
"description": "Smooth gradient from dark center to light edges with subtle features",
|
| 4 |
+
"bg": "#FFFFFF",
|
| 5 |
+
"text": "#000000",
|
| 6 |
+
"gradient_color": "#FFFFFF",
|
| 7 |
+
"water": "#D5D5D5",
|
| 8 |
+
"parks": "#EFEFEF",
|
| 9 |
+
"road_motorway": "#050505",
|
| 10 |
+
"road_primary": "#151515",
|
| 11 |
+
"road_secondary": "#2A2A2A",
|
| 12 |
+
"road_tertiary": "#404040",
|
| 13 |
+
"road_residential": "#555555",
|
| 14 |
+
"road_default": "#404040"
|
| 15 |
+
}
|
themes/japanese_ink.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Japanese Ink",
|
| 3 |
+
"description": "Traditional ink wash inspired - minimalist with subtle red accent",
|
| 4 |
+
"bg": "#FAF8F5",
|
| 5 |
+
"text": "#2C2C2C",
|
| 6 |
+
"gradient_color": "#FAF8F5",
|
| 7 |
+
"water": "#E8E4E0",
|
| 8 |
+
"parks": "#F0EDE8",
|
| 9 |
+
"road_motorway": "#701A00",
|
| 10 |
+
"road_primary": "#1A1A1A",
|
| 11 |
+
"road_secondary": "#333333",
|
| 12 |
+
"road_tertiary": "#4D4D4D",
|
| 13 |
+
"road_residential": "#666666",
|
| 14 |
+
"road_default": "#4D4D4D"
|
| 15 |
+
}
|
themes/midnight_blue.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Midnight Blue",
|
| 3 |
+
"description": "Deep navy background with gold/copper roads - luxury atlas aesthetic",
|
| 4 |
+
"bg": "#0A1628",
|
| 5 |
+
"text": "#D4AF37",
|
| 6 |
+
"gradient_color": "#0A1628",
|
| 7 |
+
"water": "#061020",
|
| 8 |
+
"parks": "#0F2235",
|
| 9 |
+
"road_motorway": "#D4AF37",
|
| 10 |
+
"road_primary": "#C9A227",
|
| 11 |
+
"road_secondary": "#A8893A",
|
| 12 |
+
"road_tertiary": "#8B7355",
|
| 13 |
+
"road_residential": "#6B5B4F",
|
| 14 |
+
"road_default": "#8B7355"
|
| 15 |
+
}
|
themes/monochrome_blue.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Monochrome Blue",
|
| 3 |
+
"description": "Single blue color family with varying saturation - clean and cohesive",
|
| 4 |
+
"bg": "#F5F8FA",
|
| 5 |
+
"text": "#1A3A5C",
|
| 6 |
+
"gradient_color": "#F5F8FA",
|
| 7 |
+
"water": "#D0E0F0",
|
| 8 |
+
"parks": "#E0EAF2",
|
| 9 |
+
"road_motorway": "#1A3A5C",
|
| 10 |
+
"road_primary": "#2A5580",
|
| 11 |
+
"road_secondary": "#4A7AA8",
|
| 12 |
+
"road_tertiary": "#7AA0C8",
|
| 13 |
+
"road_residential": "#A8C4E0",
|
| 14 |
+
"road_default": "#4A7AA8"
|
| 15 |
+
}
|
themes/neon_cyberpunk.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Neon Cyberpunk",
|
| 3 |
+
"description": "Dark background with electric pink/cyan - bold night city vibes",
|
| 4 |
+
"bg": "#0D0D1A",
|
| 5 |
+
"text": "#00FFFF",
|
| 6 |
+
"gradient_color": "#0D0D1A",
|
| 7 |
+
"water": "#0A0A15",
|
| 8 |
+
"parks": "#151525",
|
| 9 |
+
"road_motorway": "#FF00FF",
|
| 10 |
+
"road_primary": "#00FFFF",
|
| 11 |
+
"road_secondary": "#00C8C8",
|
| 12 |
+
"road_tertiary": "#0098A0",
|
| 13 |
+
"road_residential": "#006870",
|
| 14 |
+
"road_default": "#0098A0"
|
| 15 |
+
}
|
themes/noir.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Noir",
|
| 3 |
+
"description": "Pure black background with white/gray roads - modern gallery aesthetic",
|
| 4 |
+
"bg": "#000000",
|
| 5 |
+
"text": "#FFFFFF",
|
| 6 |
+
"gradient_color": "#000000",
|
| 7 |
+
"water": "#0A0A0A",
|
| 8 |
+
"parks": "#111111",
|
| 9 |
+
"road_motorway": "#FFFFFF",
|
| 10 |
+
"road_primary": "#E0E0E0",
|
| 11 |
+
"road_secondary": "#B0B0B0",
|
| 12 |
+
"road_tertiary": "#808080",
|
| 13 |
+
"road_residential": "#505050",
|
| 14 |
+
"road_default": "#808080"
|
| 15 |
+
}
|
themes/ocean.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Ocean",
|
| 3 |
+
"description": "Various blues and teals - perfect for coastal cities",
|
| 4 |
+
"bg": "#F0F8FA",
|
| 5 |
+
"text": "#1A5F7A",
|
| 6 |
+
"gradient_color": "#F0F8FA",
|
| 7 |
+
"water": "#B8D8E8",
|
| 8 |
+
"parks": "#D8EAE8",
|
| 9 |
+
"road_motorway": "#1A5F7A",
|
| 10 |
+
"road_primary": "#2A7A9A",
|
| 11 |
+
"road_secondary": "#4A9AB8",
|
| 12 |
+
"road_tertiary": "#70B8D0",
|
| 13 |
+
"road_residential": "#A0D0E0",
|
| 14 |
+
"road_default": "#4A9AB8"
|
| 15 |
+
}
|
themes/pastel_dream.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Pastel Dream",
|
| 3 |
+
"description": "Soft muted pastels with dusty blues and mauves - dreamy artistic aesthetic",
|
| 4 |
+
"bg": "#FAF7F2",
|
| 5 |
+
"text": "#5D5A6D",
|
| 6 |
+
"gradient_color": "#FAF7F2",
|
| 7 |
+
"water": "#D4E4ED",
|
| 8 |
+
"parks": "#E8EDE4",
|
| 9 |
+
"road_motorway": "#7B8794",
|
| 10 |
+
"road_primary": "#9BA4B0",
|
| 11 |
+
"road_secondary": "#B5AEBB",
|
| 12 |
+
"road_tertiary": "#C9C0C9",
|
| 13 |
+
"road_residential": "#D8D2D8",
|
| 14 |
+
"road_default": "#C9C0C9"
|
| 15 |
+
}
|
themes/sunset.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Sunset",
|
| 3 |
+
"description": "Warm oranges and pinks on soft peach - dreamy golden hour aesthetic",
|
| 4 |
+
"bg": "#FDF5F0",
|
| 5 |
+
"text": "#C45C3E",
|
| 6 |
+
"gradient_color": "#FDF5F0",
|
| 7 |
+
"water": "#F0D8D0",
|
| 8 |
+
"parks": "#F8E8E0",
|
| 9 |
+
"road_motorway": "#C45C3E",
|
| 10 |
+
"road_primary": "#D87A5A",
|
| 11 |
+
"road_secondary": "#E8A088",
|
| 12 |
+
"road_tertiary": "#F0B8A8",
|
| 13 |
+
"road_residential": "#F5D0C8",
|
| 14 |
+
"road_default": "#E8A088"
|
| 15 |
+
}
|
themes/terracotta.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Terracotta",
|
| 3 |
+
"description": "Mediterranean warmth - burnt orange and clay tones on cream",
|
| 4 |
+
"bg": "#F5EDE4",
|
| 5 |
+
"text": "#8B4513",
|
| 6 |
+
"gradient_color": "#F5EDE4",
|
| 7 |
+
"water": "#A8C4C4",
|
| 8 |
+
"parks": "#E8E0D0",
|
| 9 |
+
"road_motorway": "#8B3E2F",
|
| 10 |
+
"road_primary": "#A0522D",
|
| 11 |
+
"road_secondary": "#B8653A",
|
| 12 |
+
"road_tertiary": "#C9846A",
|
| 13 |
+
"road_residential": "#B59A8B",
|
| 14 |
+
"road_default": "#C9846A"
|
| 15 |
+
}
|
themes/warm_beige.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Warm Beige",
|
| 3 |
+
"description": "Earthy warm neutrals with sepia tones - vintage map aesthetic",
|
| 4 |
+
"bg": "#F5F0E8",
|
| 5 |
+
"text": "#6B5B4F",
|
| 6 |
+
"gradient_color": "#F5F0E8",
|
| 7 |
+
"water": "#DDD5C8",
|
| 8 |
+
"parks": "#E8E4D8",
|
| 9 |
+
"road_motorway": "#8B7355",
|
| 10 |
+
"road_primary": "#A08B70",
|
| 11 |
+
"road_secondary": "#B5A48E",
|
| 12 |
+
"road_tertiary": "#C9BBAA",
|
| 13 |
+
"road_residential": "#D9CFC2",
|
| 14 |
+
"road_default": "#C9BBAA"
|
| 15 |
+
}
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|