Spaces:
Sleeping
Sleeping
File size: 36,377 Bytes
bc6fc76 fbcd493 c6c1a1c a009142 c6c1a1c a009142 c6c1a1c a009142 c6c1a1c a009142 c6c1a1c a009142 c6c1a1c bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 8509a7d a1041b7 bc6fc76 a1041b7 bc6fc76 8509a7d bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 8509a7d a1041b7 bc6fc76 a1041b7 8509a7d bdfb445 72afff5 8509a7d a1041b7 8509a7d a1041b7 8509a7d a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 fbcd493 a1041b7 bc6fc76 a1041b7 b3469aa a1041b7 bc6fc76 a1041b7 bc6fc76 8509a7d bc6fc76 a1041b7 bc6fc76 a1041b7 fbcd493 a1041b7 bc6fc76 a1041b7 fbcd493 a1041b7 bc6fc76 a1041b7 bc6fc76 fbcd493 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 d1e54f6 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 9e29544 bc6fc76 a1041b7 d1e54f6 a1041b7 bc6fc76 a1041b7 bc6fc76 ec099d2 4e09d38 ec099d2 bc6fc76 a1041b7 fae9639 9670f79 4e09d38 fae9639 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 9e29544 bc6fc76 a1041b7 bc6fc76 a1041b7 8509a7d bc6fc76 a1041b7 8509a7d a1041b7 8509a7d a1041b7 8509a7d d1e54f6 8509a7d d1e54f6 8509a7d 9274587 8509a7d d1e54f6 8509a7d d1e54f6 8509a7d a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 8509a7d bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 fbcd493 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 d1e54f6 a1041b7 bc6fc76 a1041b7 d1e54f6 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 ac75d61 8509a7d ac75d61 a1041b7 d1e54f6 ac75d61 a1041b7 bc6fc76 a1041b7 ac75d61 8509a7d ac75d61 8509a7d ac75d61 d1e54f6 ac75d61 d1e54f6 9274587 d1e54f6 bc6fc76 a1041b7 d1e54f6 a1041b7 ac75d61 8509a7d ac75d61 d1e54f6 9274587 a1041b7 bc6fc76 a1041b7 8509a7d bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 8509a7d bc6fc76 a1041b7 bc6fc76 8509a7d bc6fc76 a1041b7 bc6fc76 fbcd493 bc6fc76 fbcd493 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 a1041b7 bc6fc76 8addb03 | 1 2 3 4 5 6 7 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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 | # -*- coding: utf-8 -*-
"""
Gradio Web Interface for City Map Poster Generator
"""
import os
import json
import tempfile
# Monkey-patch gradio_client bug before importing gradio
# Fix for: TypeError: argument of type 'bool' is not iterable
# and APIInfoParseError: Cannot parse schema True
try:
import gradio_client.utils as client_utils
# Patch _json_schema_to_python_type to handle boolean schemas
_original_json_schema_to_python_type = client_utils._json_schema_to_python_type
def _patched_json_schema_to_python_type(schema, defs):
# Handle boolean schema (e.g., additionalProperties: true/false)
if schema is True:
return "Any"
if schema is False:
return "None"
return _original_json_schema_to_python_type(schema, defs)
client_utils._json_schema_to_python_type = _patched_json_schema_to_python_type
except Exception as e:
print(f"Warning: Could not patch gradio_client: {e}")
import gradio as gr
# Import from the main module
from cities_data import (
get_countries,
get_provinces,
get_cities,
get_districts,
translate,
get_country_key,
)
# --- Constants ---
THEMES_DIR = "themes"
FONTS_DIR = "fonts"
POSTERS_DIR = "posters"
# Layer Constants
LAYERS_EN = ["Motorway", "Primary Roads", "Secondary Roads", "Water", "Parks"]
LAYERS_CN = ["高速公路", "主干道", "次干道", "水域", "公园"]
LAYER_KEYS = ["motorway", "primary", "secondary", "water", "parks"]
def get_available_themes():
"""Scans the themes directory and returns a list of available theme names."""
if not os.path.exists(THEMES_DIR):
return []
themes = []
for file in sorted(os.listdir(THEMES_DIR)):
if file.endswith(".json"):
theme_name = file[:-5]
themes.append(theme_name)
return themes
def get_theme_choices(lang="en"):
"""Return a list of (Display Name, Internal Name) tuples for themes."""
internal_names = get_available_themes()
choices = []
for internal_name in internal_names:
theme_info = load_theme_info(internal_name)
if theme_info:
proper_name = theme_info.get("name", internal_name)
display_name = translate(proper_name, lang)
choices.append((display_name, internal_name))
else:
choices.append((internal_name, internal_name))
return choices
def load_theme_info(theme_name):
"""Load theme details for preview."""
theme_file = os.path.join(THEMES_DIR, f"{theme_name}.json")
if os.path.exists(theme_file):
with open(theme_file, "r") as f:
return json.load(f)
return None
def get_theme_preview_html(theme_name, lang="en"):
"""Generate HTML preview for a theme."""
theme = load_theme_info(theme_name)
lang_code = "en" if lang == "English" else "cn"
if not theme:
return f"<p>{'Theme load failed' if lang_code == 'en' else '主题加载失败'}</p>"
# Translated labels
labels = {
"bg": "Background" if lang_code == "en" else "背景",
"text": "Text" if lang_code == "en" else "文字",
"motorway": "Motorway" if lang_code == "en" else "高速公路",
"primary": "Primary Road" if lang_code == "en" else "主干道",
"secondary": "Secondary Road" if lang_code == "en" else "次干道",
"water": "Water" if lang_code == "en" else "水域",
"parks": "Parks" if lang_code == "en" else "公园",
}
# Create color swatches
colors = [
(labels["bg"], theme.get("bg", "#FFFFFF")),
(labels["text"], theme.get("text", "#000000")),
(labels["motorway"], theme.get("road_motorway", "#000000")),
(labels["primary"], theme.get("road_primary", "#333333")),
(labels["secondary"], theme.get("road_secondary", "#666666")),
(labels["water"], theme.get("water", "#C0C0C0")),
(labels["parks"], theme.get("parks", "#F0F0F0")),
]
display_name = translate(theme.get("name", theme_name), lang_code)
description = theme.get("description", "")
# Optional: translate description if we really want to, but might be too much work
html = f"""
<div style="padding: 12px; background: {theme.get("bg", "#FFFFFF")}; border-radius: 8px; border: 1px solid #ddd;">
<h4 style="color: {theme.get("text", "#000000")}; margin: 0 0 8px 0; font-size: 14px;">
{display_name}
</h4>
<p style="color: {theme.get("text", "#000000")}; opacity: 0.7; margin: 0 0 12px 0; font-size: 12px;">
{description}
</p>
<div style="display: flex; flex-wrap: wrap; gap: 6px;">
"""
for label, color in colors:
html += f"""
<div style="display: flex; align-items: center; gap: 4px;">
<div style="width: 20px; height: 20px; background: {color}; border-radius: 4px; border: 1px solid #ccc;"></div>
<span style="font-size: 10px; color: {theme.get("text", "#000")}; opacity: 0.8;">{label}</span>
</div>
"""
html += "</div></div>"
return html
def generate_poster(
location_mode,
custom_lat,
custom_lon,
custom_city_name,
custom_country_name,
country_display,
province,
city_dropdown,
district_dropdown,
theme_name,
distance,
width,
height,
output_format,
no_crop,
show_text,
poster_lang,
layers_selection,
progress=gr.Progress(),
):
"""
Generate the map poster with given parameters.
"""
# Import here to avoid circular imports and ensure THEME is set correctly
import create_map_poster as cmp
# Decode layer selection
show_motorway = any(x in ["Motorway", "高速公路"] for x in layers_selection)
show_primary = any(x in ["Primary Roads", "主干道"] for x in layers_selection)
show_secondary = any(x in ["Secondary Roads", "次干道"] for x in layers_selection)
show_water = any(x in ["Water", "水域"] for x in layers_selection)
show_parks = any(x in ["Parks", "公园"] for x in layers_selection)
# Determine display names based on poster_lang
lang_code = "en" if poster_lang == "English" else "cn"
actual_distance = distance
display_city = ""
display_country = ""
coords = None
progress(0.1, desc="正在加载主题...")
# Load theme
cmp.THEME = cmp.load_theme(theme_name)
if location_mode == "自定义坐标" or location_mode == "Custom Coordinates":
lat_val = float(custom_lat)
lon_val = float(custom_lon)
# Basic validation: Latitude must be between -90 and 90
if lat_val < -90 or lat_val > 90:
return None, f"❌ 纬度超出范围 (-90 到 90)。你是否填反了经纬度?(当前填入: {lat_val})"
coords = (lat_val, lon_val)
display_city = custom_city_name if custom_city_name else ""
display_country = custom_country_name if custom_country_name else ""
# Determine output filename component
selected_location = display_city
else:
# Standard Dropdown Logic
# Parse country
# Since we now use (Display, Value) tuples where Value is the key,
# country_display is likely already the key. get_country_key remains for safety.
selected_country = get_country_key(country_display)
# Use dropdown selection
# If district is selected and isn't "整个城市", use its coordinates
selected_location = (
district_dropdown
if (district_dropdown and district_dropdown != city_dropdown)
else city_dropdown
)
# Detect if we are selecting an entire province
is_whole_province = False
if selected_country == "中国" and selected_location:
if selected_location.endswith("_WHOLE"):
is_whole_province = True
selected_location = selected_location.replace("_WHOLE", "")
if not selected_location:
return None, "❌ 请选择城市或区县名称"
if not selected_country:
return None, "❌ 请选择国家"
# For whole province, we might want to override the distance if it's too small
if is_whole_province:
# A province is much larger than a city. Default 10km is way too small.
# We'll use a larger default or just trust the user if they've slid it up,
# but let's ensure it's at least 150km for a province.
if distance < 100000:
actual_distance = 200000 # 200km default for province
print(
f"Whole province detected ({selected_location}). Increasing distance to {actual_distance}m"
)
if selected_country == "中国" and lang_code == "cn":
if is_whole_province:
display_city = selected_location
display_country = "中国"
# Hierarchical logic for China (Chinese language)
elif district_dropdown and district_dropdown != city_dropdown:
# Case 3 & 4: District selected
display_city = district_dropdown
if province == city_dropdown:
# Case 3: District of Municipality
display_country = f"中国 {province}"
else:
# Case 4: District of Regular City
display_country = f"中国 {province} {city_dropdown}"
else:
# Case 1 & 2: City selected (or "Whole City")
display_city = city_dropdown
if province == city_dropdown:
# Case 1: Municipality
display_country = "中国"
else:
# Case 2: Regular City
display_country = f"中国 {province}"
else:
# Standard logic for International or English posters
display_city = translate(selected_location, lang_code)
display_country = translate(selected_country, lang_code)
progress(0.2, desc="正在获取坐标...")
try:
# We search coordinates using the selection
# CMP might need the "original" names if OSM behaves better with them
# For China, "广州" is better than "Guangzhou" for geopy sometimes, but geopy is usually good.
# Let's ensure we use the 'original' internal key if possible for best OSM matching
# However, for cities, we don't have a strict key map like for countries.
# We can try to translate to CN if it's a Chinese city.
search_city = selected_location
search_country = selected_country
# Use city as parent if searching for a district, otherwise use province
search_parent = (
city_dropdown
if (district_dropdown and district_dropdown != city_dropdown)
else province
)
coords = cmp.get_coordinates(
search_city, search_country, parent=search_parent
)
except Exception as e:
return None, f"❌ 无法找到城市坐标: {str(e)}"
progress(0.3, desc="正在生成海报...")
# Generate output filename (using English/Slugified names)
en_city = translate(selected_location, "en")
temp_dir = tempfile.gettempdir()
output_file = cmp.generate_output_filename(
en_city, theme_name, output_format, directory=temp_dir
)
try:
# Wrap the generator to yield status updates and final result
for status in cmp.create_poster(
display_city, # Pass translated names for display
display_country,
coords,
actual_distance,
output_file,
output_format,
width=width,
height=height,
no_crop=no_crop,
show_text=show_text,
show_motorway=show_motorway,
show_primary=show_primary,
show_secondary=show_secondary,
show_water=show_water,
show_parks=show_parks,
):
# Translate common status messages if possible
status_display = status
if lang_code == "cn":
if "Downloading street network" in status:
status_display = "正在下载街道数据..."
elif "Downloading features" in status:
status_display = "正在下载水域和公园数据..."
elif "Rendering map" in status:
status_display = "正在渲染地图..."
elif "Applying road styles" in status:
status_display = "正在应用样式..."
elif "Saving to" in status:
status_display = "正在保存海报..."
elif "Done" in status:
status_display = "完成!"
yield None, f"⏳ {status_display}", None
progress(1.0, desc="完成!")
yield output_file, f"✅ 海报生成成功!保存至: {output_file}", output_file
except Exception as e:
import traceback
traceback.print_exc()
yield None, f"❌ 生成失败: {str(e)}", None
def update_provinces(country, lang="en"):
"""Update province dropdown based on country selection and language."""
# country is the internal key
lang_code = "en" if lang == "English" else "cn"
provinces = get_provinces(country, lang_code)
if provinces:
return gr.update(choices=provinces, value=provinces[0][1], visible=True)
return gr.update(choices=[], value=None, visible=False)
def update_cities(country, province, lang="en"):
"""Update city dropdown based on province selection and language."""
# country and province are internal keys
lang_code = "en" if lang == "English" else "cn"
cities = get_cities(country, province, lang_code)
if cities:
# For China, if it's a municipality, get_cities returns [province_name]
# We should automatically trigger update_districts then.
return gr.update(choices=cities, value=cities[0][1])
return gr.update(choices=[], value=None)
def update_districts(country, province, city, lang="en"):
"""Update district dropdown based on city selection."""
if country != "中国" or not city or city.endswith("_WHOLE"):
return gr.update(choices=[], value=None, visible=False)
lang_code = "en" if lang == "English" else "cn"
districts = get_districts(country, province, city, lang_code)
if districts:
return gr.update(choices=districts, value=districts[0][1], visible=True)
return gr.update(choices=[], value=None, visible=False)
def on_theme_change(theme_name, lang="en"):
"""Update theme preview when theme changes."""
return get_theme_preview_html(theme_name, lang)
# --- Build Gradio Interface ---
def create_interface():
"""Create and return the Gradio interface."""
# Get initial data (Default English)
# Get initial data (Default English)
default_lang_code = "cn"
default_lang_radio = "中文"
countries = get_countries(default_lang_code)
theme_choices = get_theme_choices(default_lang_code)
default_country_key = "中国"
default_provinces = get_provinces(default_country_key, default_lang_code)
default_province_key = default_provinces[0][1] if default_provinces else None
default_cities = (
get_cities(default_country_key, default_province_key, default_lang_code)
if default_province_key
else []
)
default_city_key = default_cities[0][1] if default_cities else None
# Handle district initialization carefully to avoid "Value not in choices" error
default_districts = (
get_districts(
default_country_key,
default_province_key,
default_city_key,
default_lang_code,
)
if default_city_key
else []
)
default_district_key = default_districts[0][1] if default_districts else None
default_theme = theme_choices[0][1] if theme_choices else "feature_based"
default_theme = theme_choices[0][1] if theme_choices else "feature_based"
with gr.Blocks(
title="城市地图海报生成器",
theme=gr.themes.Default(),
css="""
.header-title {
text-align: center;
font-size: 2em;
font-weight: bold;
margin-bottom: 0.5em;
background: linear-gradient(135deg, #FF8C00 0%, #FFA500 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-subtitle {
text-align: center;
color: #666;
margin-bottom: 1.5em;
}
.section-title {
font-weight: 600;
font-size: 1.1em;
margin-bottom: 0.5em;
color: #333;
}
.output-image {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
""",
) as demo:
# Header
gr.HTML("""
<div class="header-title">城市地图海报生成器</div>
<div class="header-subtitle">选择任意城市,自定义主题风格,生成精美地图海报</div>
<div style="max-width: 800px; margin: 0 auto 20px auto; padding: 12px; background: #fff5f5; border: 1px solid #feb2b2; border-radius: 8px; text-align: left; font-size: 13px; line-height: 1.6; color: #c53030;">
<b>⚠️ 注意!</b><br>
• <b>特大城市</b>(如北京): 当城市面积过大时,中心定位可能不准。<br>
• <b>省级行政区生成超级慢!</b>: 正在优化中。<br>
• <b>地点中英文不完善</b> : 由于地点中英文翻译数据量过大,未能显示完善。<br>
• <b>小城市</b> : 由于 OpenStreetMap 数据缺失,部分图层(如公园/水域)可能无法显示。<br>
• <b>生成速度</b> : 国外地点使用国外服务器数据且渲染逻辑较基础,下载和生成速度可能较慢。<br>
• <b>数据来源</b> : © OpenStreetMap contributors
</div>
""")
with gr.Row():
# Left Column - Controls
with gr.Column(scale=1):
# City Selection Section
gr.HTML('<div class="section-title">📍 城市选择</div>')
lang_radio = gr.Radio(
choices=["中文", "English"],
value="中文",
label="海报语言",
info="选择海报及界面的显示语言",
)
location_mode = gr.Radio(
choices=["城市选择", "自定义坐标"],
value="城市选择",
label="定位方式",
info="选择通过列表选择城市或输入自定义经纬度",
)
with gr.Group(visible=True) as city_selection_group:
country_dropdown = gr.Dropdown(
choices=countries,
value=default_country_key,
label="选择国家",
interactive=True,
)
province_dropdown = gr.Dropdown(
choices=default_provinces,
value=default_province_key,
label="选择省份/州",
interactive=True,
)
city_dropdown = gr.Dropdown(
choices=default_cities,
value=default_city_key,
label="选择城市",
interactive=True,
)
district_dropdown = gr.Dropdown(
choices=default_districts,
value=default_district_key,
label="选择区县",
interactive=True,
visible=(default_country_key == "中国" and default_district_key is not None),
)
with gr.Group(visible=False) as custom_coords_group:
custom_coords_info = gr.Markdown(
"💡 **填写指南**:\n"
"- **纬度 (Latitude)**: 南北向坐标,范围 -90 至 90 (中国约 18~53)。\n"
"- **经度 (Longitude)**: 东西向坐标,范围 -180 至 180 (中国约 73~135)。\n\n"
"您可以访问 [坐标拾取系统](https://map.jiqrxx.com/jingweidu/) 获取精确数值。"
)
with gr.Row():
custom_lat = gr.Number(
label="纬度 (Latitude)", value=None, precision=6
)
custom_lon = gr.Number(
label="经度 (Longitude)", value=None, precision=6
)
custom_city_name = gr.Textbox(
label="主标题 (Main Title)", placeholder="如: Shanghai 或 '我们的家'"
)
custom_country_name = gr.Textbox(
label="副标题 (Subtitle)",
placeholder="如: China 或 '2024.10.20'",
value="",
)
gr.HTML("<hr style='margin: 20px 0; border-color: #eee;'>")
# Theme Section
gr.HTML('<div class="section-title">🎨 主题风格</div>')
theme_dropdown = gr.Dropdown(
choices=theme_choices,
value=default_theme,
label="选择主题",
interactive=True,
)
theme_preview = gr.HTML(
value=get_theme_preview_html(default_theme, default_lang_radio),
label="主题预览",
)
gr.HTML("<hr style='margin: 20px 0; border-color: #eee;'>")
# Parameters Section
gr.HTML('<div class="section-title">⚙️ 参数设置</div>')
distance_slider = gr.Slider(
minimum=4000,
maximum=30000,
value=10000,
step=1000,
label="地图范围 (米)",
info="4000-6000: 小城区 | 8000-12000: 中等城市 | 15000+: 大都市 (范围越大生成越慢)",
)
with gr.Row():
width_input = gr.Number(
value=12.0, label="宽度 (英寸)", minimum=6, maximum=24
)
height_input = gr.Number(
value=16.0, label="高度 (英寸)", minimum=8, maximum=32
)
format_radio = gr.Radio(
choices=["png", "svg", "pdf"],
value="png",
label="输出格式",
info="PNG: 适合打印 | SVG: 矢量图 | PDF: 文档",
)
no_crop_checkbox = gr.Checkbox(
value=False,
label="保留边距 (不裁剪)",
info="勾选后保留海报边缘背景",
)
show_text_checkbox = gr.Checkbox(
value=True,
label="显示文字",
info="在海报上显示城市名和经纬度",
)
layers_checkbox = gr.CheckboxGroup(
choices=LAYERS_EN,
value=LAYERS_EN,
label="图层显示",
info="选择需要显示的地图元素",
)
# Generate Button
generate_btn = gr.Button("🚀 生成海报", variant="primary", size="lg")
# Right Column - Output
with gr.Column(scale=1):
gr.HTML('<div class="section-title">🖼️ 生成结果</div>')
output_image = gr.Image(
label="海报预览",
type="filepath",
elem_classes=["output-image"],
height=600,
interactive=False,
)
output_status = gr.Textbox(label="状态", interactive=False)
download_btn = gr.DownloadButton(label="📥 下载海报", visible=False)
poster_state = gr.State()
# --- Event Handlers ---
# Language change -> update all dropdowns
def on_lang_change(
lang,
current_country,
current_province,
current_city,
current_district,
current_theme,
current_layers,
):
lang_code = "en" if lang == "English" else "cn"
new_countries = get_countries(lang_code)
# Use keys to maintain selection
new_provinces = get_provinces(current_country, lang_code)
new_cities = (
get_cities(current_country, current_province, lang_code)
if current_province
else []
)
new_districts = (
get_districts(
current_country, current_province, current_city, lang_code
)
if current_city
else []
)
# Ensure district value is valid for new choices
valid_district = None
if new_districts:
if any(v == current_district for d, v in new_districts):
valid_district = current_district
else:
valid_district = new_districts[0][1]
# Themes
new_theme_choices = get_theme_choices(lang_code)
new_preview = get_theme_preview_html(current_theme, lang)
# Layers
map_en_to_key = dict(zip(LAYERS_EN, LAYER_KEYS))
map_cn_to_key = dict(zip(LAYERS_CN, LAYER_KEYS))
map_key_to_en = dict(zip(LAYER_KEYS, LAYERS_EN))
map_key_to_cn = dict(zip(LAYER_KEYS, LAYERS_CN))
current_keys = []
for x in current_layers:
if x in map_en_to_key:
current_keys.append(map_en_to_key[x])
elif x in map_cn_to_key:
current_keys.append(map_cn_to_key[x])
target_map = map_key_to_en if lang == "English" else map_key_to_cn
new_layer_choices = LAYERS_EN if lang == "English" else LAYERS_CN
new_layer_values = [target_map[k] for k in current_keys if k in target_map]
return (
gr.update(
choices=new_countries,
value=current_country,
label="Select Country" if lang == "English" else "选择国家",
),
gr.update(
choices=new_provinces,
value=current_province,
label="Select Province/State"
if lang == "English"
else "选择省份/州",
),
gr.update(
choices=new_cities,
value=current_city,
label="Select City" if lang == "English" else "选择城市",
),
gr.update(
choices=new_districts,
value=valid_district,
visible=(current_country == "中国" and valid_district is not None),
label="Select District" if lang == "English" else "选择区县",
),
gr.update(
choices=new_theme_choices,
label="Select Theme" if lang == "English" else "选择主题",
),
new_preview,
gr.update(
choices=new_layer_choices,
value=new_layer_values,
label="Layers" if lang == "English" else "图层显示",
),
gr.update(
label="Map Range (m)" if lang == "English" else "地图范围 (米)",
info="4000-6000: Small | 8000-12000: Medium | 15000+: Large"
if lang == "English"
else "4000-6000: 小城区 | 8000-12000: 中等城市 | 15000+: 大都市 (范围越大生成越慢)",
),
gr.update(label="Width (inch)" if lang == "English" else "宽度 (英寸)"),
gr.update(
label="Height (inch)" if lang == "English" else "高度 (英寸)"
),
gr.update(
label="Format" if lang == "English" else "输出格式",
info="PNG: Print | SVG: Vector | PDF: Doc"
if lang == "English"
else "PNG: 适合打印 | SVG: 矢量图 | PDF: 文档",
),
gr.update(
label="No Crop (Keep Margins)"
if lang == "English"
else "保留边距 (不裁剪)",
info="Keep background margins"
if lang == "English"
else "勾选后保留海报边缘背景",
),
gr.update(
label="Show Text" if lang == "English" else "显示文字",
info="Show city name and coordinates on poster"
if lang == "English"
else "在海报上显示城市名和经纬度",
),
gr.update(
value="🚀 Generate Poster" if lang == "English" else "🚀 生成海报"
),
gr.update(
label="📥 Download Poster" if lang == "English" else "📥 下载海报"
),
gr.update(
label="Main Title" if lang == "English" else "主标题 (Main Title)",
placeholder="e.g. Shanghai or 'Our Home'" if lang == "English" else "如: Shanghai 或 '我们的家'"
),
gr.update(
label="Subtitle" if lang == "English" else "副标题 (Subtitle)",
placeholder="e.g. China or '2024.10.20'" if lang == "English" else "如: China 或 '2024.10.20'"
),
gr.update(
value="💡 **Guide**:\n"
"- **Latitude**: North-South, range -90 to 90 (China: ~18~53).\n"
"- **Longitude**: East-West, range -180 to 180 (China: ~73~135).\n\n"
"Use [Coordinate Picker](https://map.jiqrxx.com/jingweidu/) to find exact values."
if lang == "English" else
"💡 **填写指南**:\n"
"- **纬度 (Latitude)**: 南北向坐标,范围 -90 至 90 (中国约 18~53)。\n"
"- **经度 (Longitude)**: 东西向坐标,范围 -180 至 180 (中国约 73~135)。\n\n"
"您可以访问 [坐标拾取系统](https://map.jiqrxx.com/jingweidu/) 获取精确数值。"
)
)
lang_radio.change(
fn=on_lang_change,
inputs=[
lang_radio,
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
layers_checkbox,
],
outputs=[
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
theme_preview,
layers_checkbox,
distance_slider,
width_input,
height_input,
format_radio,
no_crop_checkbox,
show_text_checkbox,
generate_btn,
download_btn,
custom_city_name,
custom_country_name,
custom_coords_info,
],
)
def toggle_location_mode(mode):
is_custom = mode in ["自定义坐标", "Custom Coordinates"]
return {
city_selection_group: gr.update(visible=not is_custom),
custom_coords_group: gr.update(visible=is_custom),
}
location_mode.change(
fn=toggle_location_mode,
inputs=[location_mode],
outputs=[city_selection_group, custom_coords_group],
)
# Country change -> update provinces
country_dropdown.change(
fn=update_provinces,
inputs=[country_dropdown, lang_radio],
outputs=[province_dropdown],
)
# Province change -> update cities
province_dropdown.change(
fn=update_cities,
inputs=[country_dropdown, province_dropdown, lang_radio],
outputs=[city_dropdown],
).then( # Update districts after city changes
fn=update_districts,
inputs=[country_dropdown, province_dropdown, city_dropdown, lang_radio],
outputs=[district_dropdown],
)
# City change -> update districts
city_dropdown.change(
fn=update_districts,
inputs=[country_dropdown, province_dropdown, city_dropdown, lang_radio],
outputs=[district_dropdown],
)
# Theme change -> update preview
theme_dropdown.change(
fn=on_theme_change,
inputs=[theme_dropdown, lang_radio],
outputs=[theme_preview],
)
# Generate button click
def on_generate_complete(filepath, status):
"""Handle generate completion - show download button if successful."""
if filepath and os.path.exists(filepath):
return filepath, status, gr.update(visible=True, value=filepath)
return filepath, status, gr.update(visible=False)
generate_btn.click(
fn=generate_poster,
inputs=[
location_mode,
custom_lat,
custom_lon,
custom_city_name,
custom_country_name,
country_dropdown,
province_dropdown,
city_dropdown,
district_dropdown,
theme_dropdown,
distance_slider,
width_input,
height_input,
format_radio,
no_crop_checkbox,
show_text_checkbox,
lang_radio,
layers_checkbox,
],
outputs=[output_image, output_status, poster_state],
).then(
fn=on_generate_complete,
inputs=[poster_state, output_status],
outputs=[output_image, output_status, download_btn],
)
# Footer
gr.HTML("""
<div style="text-align: center; margin-top: 24px; padding: 20px; color: #666; font-size: 13px; border-top: 1px solid #eee;">
<p>项目地址: <a href="https://github.com/IsaacHuo/maptoposter" target="_blank" style="color: #764ba2; text-decoration: none; font-weight: bold;">GitHub - IsaacHuo/maptoposter</a></p>
<p>✨ 欢迎提 <b>Issue</b> 和 <b>PR</b> | 鸣谢:该项目基于 <a href="https://github.com/originalankur/maptoposter" target="_blank" style="color: #666;">originalankur/maptoposter</a> 开发</p>
</div>
""")
return demo
# --- Main Entry ---
if __name__ == "__main__":
demo = create_interface()
demo.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
|