Spaces:
Running
Running
| # -*- 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) | |