MapToPoster / cities_data.py
isaachwf's picture
feat: improve custom coordinate text layout (two-line title/subtitle)
d1e54f6
import json
import os
CHINA_DATA_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "public", "china-city-data"
)
_CHINA_INFO_CACHE = None
def _load_china_info():
global _CHINA_INFO_CACHE
if _CHINA_INFO_CACHE is None:
info_path = os.path.join(CHINA_DATA_DIR, "info.json")
try:
with open(info_path, "r", encoding="utf-8") as f:
_CHINA_INFO_CACHE = json.load(f)
except Exception as e:
print(f"Error loading China city data: {e}")
_CHINA_INFO_CACHE = {}
return _CHINA_INFO_CACHE
CITIES = {
"中国": {
"北京": ["北京"],
"上海": ["上海"],
"天津": ["天津"],
"重庆": ["重庆"],
"广东": [
"广州",
"深圳",
"东莞",
"佛山",
"珠海",
"惠州",
"中山",
"汕头",
"湛江",
"江门",
],
"浙江": ["杭州", "宁波", "温州", "绍兴", "嘉兴", "金华", "台州", "湖州"],
"江苏": ["南京", "苏州", "无锡", "常州", "南通", "徐州", "扬州", "镇江"],
"山东": ["济南", "青岛", "烟台", "威海", "潍坊", "临沂", "济宁", "淄博"],
"四川": ["成都", "绵阳", "德阳", "宜宾", "泸州", "南充", "乐山"],
"湖北": ["武汉", "宜昌", "襄阳", "荆州", "黄石", "十堰"],
"湖南": ["长沙", "株洲", "湘潭", "衡阳", "岳阳", "常德"],
"河南": ["郑州", "洛阳", "开封", "新乡", "安阳", "焦作"],
"河北": ["石家庄", "唐山", "秦皇岛", "邯郸", "保定", "沧州"],
"福建": ["福州", "厦门", "泉州", "漳州", "莆田", "龙岩"],
"安徽": ["合肥", "芜湖", "蚌埠", "马鞍山", "安庆", "黄山"],
"江西": ["南昌", "九江", "景德镇", "赣州", "上饶", "吉安"],
"陕西": ["西安", "咸阳", "宝鸡", "延安", "榆林", "汉中"],
"山西": ["太原", "大同", "临汾", "运城", "晋中", "长治"],
"辽宁": ["沈阳", "大连", "鞍山", "抚顺", "本溪", "营口"],
"吉林": ["长春", "吉林", "四平", "通化", "延边"],
"黑龙江": ["哈尔滨", "齐齐哈尔", "牡丹江", "佳木斯", "大庆"],
"云南": ["昆明", "大理", "丽江", "西双版纳", "曲靖"],
"贵州": ["贵阳", "遵义", "安顺", "六盘水", "毕节"],
"甘肃": ["兰州", "天水", "嘉峪关", "酒泉", "张掖"],
"海南": ["海口", "三亚", "儋州", "琼海"],
"广西": ["南宁", "桂林", "柳州", "梧州", "北海", "玉林"],
"内蒙古": ["呼和浩特", "包头", "鄂尔多斯", "赤峰", "呼伦贝尔"],
"新疆": ["乌鲁木齐", "喀什", "吐鲁番", "阿克苏", "伊宁"],
"西藏": ["拉萨", "日喀则", "林芝", "昌都"],
"宁夏": ["银川", "石嘴山", "吴忠", "固原"],
"青海": ["西宁", "格尔木", "玉树", "海东"],
"香港": ["香港"],
"澳门": ["澳门"],
"台湾": ["台北", "高雄", "台中", "台南", "新竹", "基隆"],
},
"USA": {
"California": [
"Los Angeles",
"San Francisco",
"San Diego",
"San Jose",
"Sacramento",
"Oakland",
"Fresno",
],
"New York": ["New York City", "Buffalo", "Rochester", "Albany", "Syracuse"],
"Texas": [
"Houston",
"Dallas",
"Austin",
"San Antonio",
"Fort Worth",
"El Paso",
],
"Florida": ["Miami", "Orlando", "Tampa", "Jacksonville", "Fort Lauderdale"],
"Illinois": ["Chicago", "Aurora", "Naperville", "Rockford"],
"Pennsylvania": ["Philadelphia", "Pittsburgh", "Harrisburg"],
"Arizona": ["Phoenix", "Tucson", "Mesa", "Scottsdale"],
"Nevada": ["Las Vegas", "Reno", "Henderson"],
"Washington": ["Seattle", "Tacoma", "Spokane", "Bellevue"],
"Massachusetts": ["Boston", "Cambridge", "Worcester"],
"Colorado": ["Denver", "Colorado Springs", "Aurora", "Boulder"],
"Georgia": ["Atlanta", "Savannah", "Augusta"],
"North Carolina": ["Charlotte", "Raleigh", "Durham"],
"Michigan": ["Detroit", "Grand Rapids", "Ann Arbor"],
"Oregon": ["Portland", "Salem", "Eugene"],
"District of Columbia": ["Washington"],
"Hawaii": ["Honolulu"],
},
"Japan": {
"関東": ["Tokyo", "Yokohama", "Kawasaki", "Saitama", "Chiba"],
"関西": ["Osaka", "Kyoto", "Kobe", "Nara"],
"中部": ["Nagoya", "Kanazawa", "Shizuoka"],
"北海道": ["Sapporo", "Hakodate", "Asahikawa"],
"九州": ["Fukuoka", "Nagasaki", "Kumamoto", "Kagoshima"],
"東北": ["Sendai", "Aomori", "Akita"],
"中国": ["Hiroshima", "Okayama"],
"四国": ["Matsuyama", "Takamatsu"],
"沖縄": ["Naha", "Okinawa"],
},
"UK": {
"England": [
"London",
"Manchester",
"Birmingham",
"Liverpool",
"Leeds",
"Bristol",
"Sheffield",
"Newcastle",
"Nottingham",
"Cambridge",
"Oxford",
],
"Scotland": ["Edinburgh", "Glasgow", "Aberdeen", "Dundee"],
"Wales": ["Cardiff", "Swansea", "Newport"],
"Northern Ireland": ["Belfast", "Londonderry"],
},
"France": {
"Île-de-France": ["Paris"],
"Provence-Alpes-Côte d'Azur": ["Marseille", "Nice", "Cannes", "Toulon"],
"Auvergne-Rhône-Alpes": ["Lyon", "Grenoble", "Saint-Étienne"],
"Nouvelle-Aquitaine": ["Bordeaux", "Limoges"],
"Occitanie": ["Toulouse", "Montpellier", "Nîmes"],
"Hauts-de-France": ["Lille", "Amiens"],
"Grand Est": ["Strasbourg", "Reims", "Nancy", "Metz"],
"Normandie": ["Rouen", "Le Havre", "Caen"],
"Bretagne": ["Rennes", "Brest", "Nantes"],
},
"Germany": {
"Bayern": ["Munich", "Nuremberg", "Augsburg"],
"Berlin": ["Berlin"],
"Hamburg": ["Hamburg"],
"Hessen": ["Frankfurt", "Wiesbaden"],
"Baden-Württemberg": ["Stuttgart", "Heidelberg", "Freiburg", "Karlsruhe"],
"Nordrhein-Westfalen": ["Cologne", "Düsseldorf", "Dortmund", "Essen", "Bonn"],
"Niedersachsen": ["Hanover", "Braunschweig", "Oldenburg"],
"Sachsen": ["Dresden", "Leipzig"],
"Brandenburg": ["Potsdam"],
},
"Italy": {
"Lazio": ["Rome"],
"Lombardia": ["Milan", "Bergamo", "Brescia"],
"Veneto": ["Venice", "Verona", "Padua"],
"Toscana": ["Florence", "Pisa", "Siena"],
"Campania": ["Naples", "Salerno"],
"Piemonte": ["Turin", "Genoa"],
"Emilia-Romagna": ["Bologna", "Parma", "Modena"],
"Sicilia": ["Palermo", "Catania", "Syracuse"],
},
"Spain": {
"Comunidad de Madrid": ["Madrid"],
"Cataluña": ["Barcelona", "Tarragona", "Girona"],
"Andalucía": ["Seville", "Granada", "Málaga", "Córdoba"],
"Comunidad Valenciana": ["Valencia", "Alicante"],
"País Vasco": ["Bilbao", "San Sebastián"],
"Galicia": ["Santiago de Compostela", "A Coruña", "Vigo"],
"Islas Baleares": ["Palma de Mallorca"],
"Islas Canarias": ["Las Palmas", "Santa Cruz de Tenerife"],
},
"Australia": {
"New South Wales": ["Sydney", "Newcastle", "Wollongong"],
"Victoria": ["Melbourne", "Geelong"],
"Queensland": ["Brisbane", "Gold Coast", "Cairns"],
"Western Australia": ["Perth", "Fremantle"],
"South Australia": ["Adelaide"],
"Tasmania": ["Hobart"],
"Australian Capital Territory": ["Canberra"],
"Northern Territory": ["Darwin"],
},
"Canada": {
"Ontario": ["Toronto", "Ottawa", "Hamilton", "Mississauga"],
"Quebec": ["Montreal", "Quebec City", "Laval"],
"British Columbia": ["Vancouver", "Victoria", "Surrey"],
"Alberta": ["Calgary", "Edmonton"],
"Manitoba": ["Winnipeg"],
"Saskatchewan": ["Saskatoon", "Regina"],
"Nova Scotia": ["Halifax"],
},
"South Korea": {
"서울특별시": ["Seoul"],
"부산광역시": ["Busan"],
"경기도": ["Incheon", "Suwon", "Seongnam"],
"대구광역시": ["Daegu"],
"대전광역시": ["Daejeon"],
"광주광역시": ["Gwangju"],
"제주특별자치도": ["Jeju"],
},
"Singapore": {
"Singapore": ["Singapore"],
},
"India": {
"Maharashtra": ["Mumbai", "Pune", "Nagpur"],
"Delhi": ["New Delhi"],
"Karnataka": ["Bangalore", "Mysore"],
"Tamil Nadu": ["Chennai", "Coimbatore", "Madurai"],
"West Bengal": ["Kolkata"],
"Gujarat": ["Ahmedabad", "Surat", "Vadodara"],
"Telangana": ["Hyderabad"],
"Kerala": ["Kochi", "Trivandrum"],
"Rajasthan": ["Jaipur", "Jodhpur", "Udaipur"],
"Uttar Pradesh": ["Lucknow", "Agra", "Varanasi"],
},
"Russia": {
"Центральный": ["Moscow"],
"Северо-Западный": ["Saint Petersburg", "Kaliningrad"],
"Южный": ["Sochi", "Rostov-on-Don", "Krasnodar"],
"Приволжский": ["Kazan", "Nizhny Novgorod", "Samara"],
"Уральский": ["Yekaterinburg", "Chelyabinsk"],
"Сибирский": ["Novosibirsk", "Krasnoyarsk", "Irkutsk"],
"Дальневосточный": ["Vladivostok", "Khabarovsk"],
},
"Brazil": {
"São Paulo": ["São Paulo", "Campinas", "Santos"],
"Rio de Janeiro": ["Rio de Janeiro", "Niterói"],
"Minas Gerais": ["Belo Horizonte"],
"Bahia": ["Salvador"],
"Rio Grande do Sul": ["Porto Alegre"],
"Paraná": ["Curitiba"],
"Distrito Federal": ["Brasília"],
"Ceará": ["Fortaleza"],
"Pernambuco": ["Recife"],
"Amazonas": ["Manaus"],
},
"Mexico": {
"Ciudad de México": ["Mexico City"],
"Jalisco": ["Guadalajara"],
"Nuevo León": ["Monterrey"],
"Quintana Roo": ["Cancún", "Playa del Carmen"],
"Baja California": ["Tijuana", "Ensenada"],
"Yucatán": ["Mérida"],
"Puebla": ["Puebla"],
"Guanajuato": ["León", "Guanajuato"],
},
"Netherlands": {
"Noord-Holland": ["Amsterdam", "Haarlem"],
"Zuid-Holland": ["Rotterdam", "The Hague", "Leiden"],
"Utrecht": ["Utrecht"],
"Noord-Brabant": ["Eindhoven", "Tilburg", "'s-Hertogenbosch"],
"Gelderland": ["Arnhem", "Nijmegen"],
"Limburg": ["Maastricht"],
},
"Belgium": {
"Brussels-Capital": ["Brussels"],
"Flanders": ["Antwerp", "Ghent", "Bruges"],
"Wallonia": ["Liège", "Charleroi", "Namur"],
},
"Switzerland": {
"Zürich": ["Zurich"],
"Bern": ["Bern"],
"Geneva": ["Geneva"],
"Vaud": ["Lausanne"],
"Basel-Stadt": ["Basel"],
"Lucerne": ["Lucerne"],
},
"Austria": {
"Wien": ["Vienna"],
"Salzburg": ["Salzburg"],
"Tirol": ["Innsbruck"],
"Steiermark": ["Graz"],
"Oberösterreich": ["Linz"],
},
"Portugal": {
"Lisboa": ["Lisbon"],
"Porto": ["Porto"],
"Faro": ["Faro", "Albufeira"],
"Coimbra": ["Coimbra"],
},
"Greece": {
"Attica": ["Athens", "Piraeus"],
"Central Macedonia": ["Thessaloniki"],
"Crete": ["Heraklion"],
"South Aegean": ["Rhodes", "Mykonos", "Santorini"],
},
"Turkey": {
"Istanbul": ["Istanbul"],
"Ankara": ["Ankara"],
"Izmir": ["Izmir"],
"Antalya": ["Antalya"],
"Bursa": ["Bursa"],
"Cappadocia": ["Nevşehir", "Göreme"],
},
"UAE": {
"Dubai": ["Dubai"],
"Abu Dhabi": ["Abu Dhabi"],
"Sharjah": ["Sharjah"],
"Ras Al Khaimah": ["Ras Al Khaimah"],
},
"Thailand": {
"Bangkok": ["Bangkok"],
"Chiang Mai": ["Chiang Mai"],
"Phuket": ["Phuket"],
"Chonburi": ["Pattaya"],
"Krabi": ["Krabi"],
},
"Vietnam": {
"Hà Nội": ["Hanoi"],
"Hồ Chí Minh": ["Ho Chi Minh City"],
"Đà Nẵng": ["Da Nang"],
"Khánh Hòa": ["Nha Trang"],
"Quảng Ninh": ["Ha Long"],
},
"Indonesia": {
"DKI Jakarta": ["Jakarta"],
"Bali": ["Bali", "Denpasar", "Ubud"],
"Jawa Timur": ["Surabaya"],
"Jawa Barat": ["Bandung"],
"DI Yogyakarta": ["Yogyakarta"],
},
"Malaysia": {
"Kuala Lumpur": ["Kuala Lumpur"],
"Penang": ["George Town"],
"Selangor": ["Petaling Jaya", "Shah Alam"],
"Johor": ["Johor Bahru"],
"Sabah": ["Kota Kinabalu"],
"Sarawak": ["Kuching"],
},
"Philippines": {
"Metro Manila": ["Manila", "Makati", "Quezon City"],
"Cebu": ["Cebu City"],
"Davao": ["Davao City"],
"Palawan": ["Puerto Princesa"],
},
"Egypt": {
"Cairo Governorate": ["Cairo"],
"Alexandria Governorate": ["Alexandria"],
"Giza Governorate": ["Giza"],
"Luxor Governorate": ["Luxor"],
"Red Sea Governorate": ["Hurghada", "Sharm El Sheikh"],
},
"South Africa": {
"Gauteng": ["Johannesburg", "Pretoria"],
"Western Cape": ["Cape Town"],
"KwaZulu-Natal": ["Durban"],
"Eastern Cape": ["Port Elizabeth"],
},
"Morocco": {
"Casablanca-Settat": ["Casablanca"],
"Rabat-Salé-Kénitra": ["Rabat"],
"Marrakech-Safi": ["Marrakech", "Essaouira"],
"Fès-Meknès": ["Fes", "Meknes"],
"Tanger-Tétouan-Al Hoceïma": ["Tangier", "Chefchaouen"],
},
"Argentina": {
"Buenos Aires": ["Buenos Aires", "La Plata"],
"Córdoba": ["Córdoba"],
"Mendoza": ["Mendoza"],
"Santa Fe": ["Rosario"],
"Tierra del Fuego": ["Ushuaia"],
},
"Chile": {
"Región Metropolitana": ["Santiago"],
"Valparaíso": ["Valparaíso", "Viña del Mar"],
"Biobío": ["Concepción"],
"Los Lagos": ["Puerto Montt"],
"Magallanes": ["Punta Arenas"],
},
"Colombia": {
"Bogotá D.C.": ["Bogotá"],
"Antioquia": ["Medellín"],
"Valle del Cauca": ["Cali"],
"Atlántico": ["Barranquilla"],
"Bolívar": ["Cartagena"],
},
"Peru": {
"Lima": ["Lima"],
"Cusco": ["Cusco"],
"Arequipa": ["Arequipa"],
"La Libertad": ["Trujillo"],
},
"New Zealand": {
"Auckland": ["Auckland"],
"Wellington": ["Wellington"],
"Canterbury": ["Christchurch"],
"Otago": ["Dunedin", "Queenstown"],
},
"Ireland": {
"Leinster": ["Dublin"],
"Munster": ["Cork", "Limerick"],
"Connacht": ["Galway"],
},
"Sweden": {
"Stockholms län": ["Stockholm"],
"Västra Götalands län": ["Gothenburg"],
"Skåne län": ["Malmö"],
"Uppsala län": ["Uppsala"],
},
"Norway": {
"Oslo": ["Oslo"],
"Vestland": ["Bergen"],
"Troms og Finnmark": ["Tromsø"],
"Trøndelag": ["Trondheim"],
},
"Denmark": {
"Hovedstaden": ["Copenhagen"],
"Midtjylland": ["Aarhus"],
"Syddanmark": ["Odense"],
},
"Finland": {
"Uusimaa": ["Helsinki", "Espoo"],
"Pirkanmaa": ["Tampere"],
"Southwest Finland": ["Turku"],
"Lapland": ["Rovaniemi"],
},
"Poland": {
"Mazowieckie": ["Warsaw"],
"Małopolskie": ["Kraków"],
"Wielkopolskie": ["Poznań"],
"Dolnośląskie": ["Wrocław"],
"Pomorskie": ["Gdańsk"],
},
"Czech Republic": {
"Praha": ["Prague"],
"Jihomoravský": ["Brno"],
"Moravskoslezský": ["Ostrava"],
"Plzeňský": ["Plzeň"],
},
"Hungary": {
"Budapest": ["Budapest"],
"Pest": ["Szentendre"],
"Baranya": ["Pécs"],
"Hajdú-Bihar": ["Debrecen"],
},
"Israel": {
"Tel Aviv District": ["Tel Aviv", "Jaffa"],
"Jerusalem District": ["Jerusalem"],
"Haifa District": ["Haifa"],
"Southern District": ["Eilat"],
},
"Saudi Arabia": {
"Riyadh": ["Riyadh"],
"Makkah": ["Mecca", "Jeddah"],
"Eastern Province": ["Dammam", "Dhahran"],
"Medina": ["Medina"],
},
"Qatar": {
"Doha": ["Doha"],
},
"Kenya": {
"Nairobi": ["Nairobi"],
"Mombasa": ["Mombasa"],
"Nakuru": ["Nakuru"],
},
"Nigeria": {
"Lagos": ["Lagos"],
"Abuja": ["Abuja"],
"Kano": ["Kano"],
},
"Pakistan": {
"Punjab": ["Lahore", "Faisalabad"],
"Sindh": ["Karachi"],
"Islamabad": ["Islamabad"],
"Khyber Pakhtunkhwa": ["Peshawar"],
},
"Bangladesh": {
"Dhaka Division": ["Dhaka"],
"Chittagong Division": ["Chittagong"],
"Sylhet Division": ["Sylhet"],
},
"Sri Lanka": {
"Western Province": ["Colombo"],
"Central Province": ["Kandy"],
"Southern Province": ["Galle"],
},
"Nepal": {
"Bagmati": ["Kathmandu"],
"Gandaki": ["Pokhara"],
},
}
# Translation layer for dual-language support
CN_TO_EN = {
# Countries (CN -> EN)
"中国": "China",
"美国": "United States",
"日本": "Japan",
"英国": "United Kingdom",
"法国": "France",
"德国": "Germany",
"意大利": "Italy",
"西班牙": "Spain",
"澳大利亚": "Australia",
"加拿大": "Canada",
"韩国": "South Korea",
"新加坡": "Singapore",
"印度": "India",
"俄罗斯": "Russia",
"巴西": "Brazil",
"墨西哥": "Mexico",
"荷兰": "Netherlands",
"比利时": "Belgium",
"瑞士": "Switzerland",
"奥地利": "Austria",
"葡萄牙": "Portugal",
"希腊": "Greece",
"土耳其": "Turkey",
"阿联酋": "United Arab Emirates",
"泰国": "Thailand",
"越南": "Vietnam",
"印度尼西亚": "Indonesia",
"马来西亚": "Malaysia",
"菲律宾": "Philippines",
"埃及": "Egypt",
"南非": "South Africa",
"摩洛哥": "Morocco",
"阿根廷": "Argentina",
"智利": "Chile",
"哥伦比亚": "Colombia",
"秘鲁": "Peru",
"新西兰": "New Zealand",
"爱尔兰": "Ireland",
"瑞典": "Sweden",
"挪威": "Norway",
"丹麦": "Denmark",
"芬兰": "Finland",
"波兰": "Poland",
"捷克": "Czech Republic",
"匈牙利": "Hungary",
"以色列": "Israel",
"沙特阿拉伯": "Saudi Arabia",
"卡塔尔": "Qatar",
"肯尼亚": "Kenya",
"尼日利亚": "Nigeria",
"巴基斯坦": "Pakistan",
"孟加拉国": "Bangladesh",
"斯里兰卡": "Sri Lanka",
"尼泊尔": "Nepal",
# Chinese Provinces (CN -> EN)
"北京": "Beijing",
"上海": "Shanghai",
"天津": "Tianjin",
"重庆": "Chongqing",
"广东": "Guangdong",
"浙江": "Zhejiang",
"江苏": "Jiangsu",
"山东": "Shandong",
"四川": "Sichuan",
"湖北": "Hubei",
"湖南": "Hunan",
"河南": "Henan",
"河北": "Hebei",
"福建": "Fujian",
"安徽": "Anhui",
"江西": "Jiangxi",
"陕西": "Shaanxi",
"山西": "Shanxi",
"辽宁": "Liaoning",
"吉林": "Jilin",
"黑龙江": "Heilongjiang",
"云南": "Yunnan",
"贵州": "Guizhou",
"甘肃": "Gansu",
"海南": "Hainan",
"广西": "Guangxi",
"内蒙古": "Inner Mongolia",
"新疆": "Xinjiang",
"西藏": "Tibet",
"宁夏": "Ningxia",
"青海": "Qinghai",
"香港": "Hong Kong",
"澳门": "Macau",
"台湾": "Taiwan",
# Japanese Regions
"関東": "Kanto",
"関西": "Kansai",
"中部": "Chubu",
"北海道": "Hokkaido",
"九州": "Kyushu",
"東北": "Tohoku",
"四国": "Shikoku",
"沖縄": "Okinawa",
# Chinese Cities (CN -> EN)
"广州": "Guangzhou",
"深圳": "Shenzhen",
"东莞": "Dongguan",
"佛山": "Foshan",
"珠海": "Zhuhai",
"惠州": "Huizhou",
"中山": "Zhongshan",
"汕头": "Shantou",
"湛江": "Zhanjiang",
"江门": "Jiangmen",
"梧州": "Wuzhou",
"杭州": "Hangzhou",
"宁波": "Ningbo",
"温州": "Wenzhou",
"绍兴": "Shaoxing",
"嘉兴": "Jiaxing",
"金华": "Jinhua",
"台州": "Taizhou",
"湖州": "Huzhou",
"南京": "Nanjing",
"苏州": "Suzhou",
"无锡": "Wuxi",
"常州": "Changzhou",
"南通": "Nantong",
"徐州": "Xuzhou",
"扬州": "Yangzhou",
"镇江": "Zhenjiang",
"济南": "Jinan",
"青岛": "Qingdao",
"烟台": "Yantai",
"威海": "Weihai",
"潍坊": "Weifang",
"临沂": "Linyi",
"济宁": "Jining",
"淄博": "Zibo",
"成都": "Chengdu",
"绵阳": "Mianyang",
"德阳": "Deyang",
"宜宾": "Yibin",
"泸州": "Luzhou",
"南充": "Nanchong",
"乐山": "Leshan",
"武汉": "Wuhan",
"宜昌": "Yichang",
"襄阳": "Xiangyang",
"荆州": "Jingzhou",
"黄石": "Huangshi",
"十堰": "Shiyan",
"长沙": "Changsha",
"株洲": "Zhuzhou",
"湘潭": "Xiangtan",
"衡阳": "Hengyang",
"岳阳": "Yueyang",
"常德": "Changde",
"郑州": "Zhengzhou",
"洛阳": "Luoyang",
"开封": "Kaifeng",
"新乡": "Xinxiang",
"安阳": "Anyang",
"焦作": "Jiaozuo",
"石家庄": "Shijiazhuang",
"唐山": "Tangshan",
"秦皇岛": "Qinhuangdao",
"邯郸": "Handan",
"保定": "Baoding",
"沧州": "Cangzhou",
"福州": "Fuzhou",
"厦门": "Xiamen",
"泉州": "Quanzhou",
"漳州": "Zhangzhou",
"莆田": "Putian",
"龙岩": "Longyan",
"合肥": "Hefei",
"芜湖": "Wuhu",
"蚌埠": "Bengbu",
"马鞍山": "Ma'anshan",
"安庆": "Anqing",
"黄山": "Huangshan",
"南昌": "Nanchang",
"九江": "Jiujiang",
"景德镇": "Jingdezhen",
"赣州": "Ganzhou",
"上饶": "Shangrao",
"吉安": "Ji'an",
"西安": "Xi'an",
"咸阳": "Xianyang",
"宝鸡": "Baoji",
"延安": "Yan'an",
"榆林": "Yulin",
"汉中": "Hanzhong",
"太原": "Taiyuan",
"大同": "Datong",
"临汾": "Linfen",
"运城": "Yuncheng",
"晋中": "Jinzhong",
"长治": "Changzhi",
"沈阳": "Shenyang",
"大连": "Dalian",
"鞍山": "Anshan",
"抚顺": "Fushun",
"本溪": "Benxi",
"营口": "Yingkou",
"长春": "Changchun",
"四平": "Siping",
"通化": "Tonghua",
"延边": "Yanbian",
"哈尔滨": "Harbin",
"齐齐哈尔": "Qiqihar",
"牡丹江": "Mudanjiang",
"佳木斯": "Jiamusi",
"大庆": "Daqing",
"昆明": "Kunming",
"大理": "Dali",
"丽江": "Lijiang",
"西双版纳": "Xishuangbanna",
"曲靖": "Qujing",
"贵阳": "Guiyang",
"遵义": "Zunyi",
"安顺": "Anshun",
"六盘水": "Liupanshui",
"毕节": "Bijie",
"兰州": "Lanzhou",
"天水": "Tianshui",
"嘉峪关": "Jiayuguan",
"酒泉": "Jiuquan",
"张掖": "Zhangye",
"海口": "Haikou",
"三亚": "Sanya",
"儋州": "Danzhou",
"琼海": "Qionghai",
"南宁": "Nanning",
"桂林": "Guilin",
"柳州": "Liuzhou",
"北海": "Beihai",
"玉林": "Yulin",
"呼和浩特": "Hohhot",
"包头": "Baotou",
"鄂尔多斯": "Ordos",
"赤峰": "Chifeng",
"呼伦贝尔": "Hulunbuir",
"乌鲁木齐": "Urumqi",
"喀什": "Kashgar",
"吐鲁番": "Turpan",
"阿克苏": "Aksu",
"伊宁": "Yining",
"拉萨": "Lhasa",
"日喀则": "Shigatse",
"林芝": "Nyingchi",
"昌都": "Qamdo",
"银川": "Yinchuan",
"石嘴山": "Shizuishan",
"吴忠": "Wuzhong",
"固原": "Guyuan",
"西宁": "Xining",
"格尔木": "Golmud",
"玉树": "Yushu",
"海东": "Haidong",
"台北": "Taipei",
"高雄": "Kaohsiung",
"台中": "Taichung",
"台南": "Tainan",
"新竹": "Hsinchu",
"基隆": "Keelung",
# Theme Names - MOVED TO EN_TO_CN
}
# English name to Chinese (for when UI/Poster is in Chinese)
EN_TO_CN = {
"China": "中国",
"USA": "美国",
"United States": "美国",
"Japan": "日本",
"UK": "英国",
"United Kingdom": "英国",
"France": "法国",
"Germany": "德国",
"Italy": "意大利",
"Spain": "西班牙",
"Australia": "澳大利亚",
"Canada": "加拿大",
"South Korea": "韩国",
"Singapore": "新加坡",
"India": "印度",
"Russia": "俄罗斯",
"Brazil": "巴西",
"Mexico": "墨西哥",
"Netherlands": "荷兰",
"Belgium": "比利时",
"Switzerland": "瑞士",
"Austria": "奥地利",
"Portugal": "葡萄牙",
"Greece": "希腊",
"Turkey": "土耳其",
"UAE": "阿联酋",
"United Arab Emirates": "阿联酋",
"Thailand": "泰国",
"Vietnam": "越南",
"Indonesia": "印度尼西亚",
"Malaysia": "马来西亚",
"Philippines": "菲律宾",
"Egypt": "埃及",
"South Africa": "南非",
"Morocco": "摩洛哥",
"Argentina": "阿根廷",
"Chile": "智利",
"Colombia": "哥伦比亚",
"Peru": "秘鲁",
"New Zealand": "新西兰",
"Ireland": "爱尔兰",
"Sweden": "瑞典",
"Norway": "挪威",
"Denmark": "丹麦",
"Finland": "芬兰",
"Poland": "波兰",
"Czech Republic": "捷克",
"Hungary": "匈牙利",
"Israel": "以色列",
"Saudi Arabia": "沙特阿拉伯",
"Qatar": "卡塔尔",
"Kenya": "肯尼亚",
"Nigeria": "尼日利亚",
"Pakistan": "巴基斯坦",
"Bangladesh": "孟加拉国",
"Sri Lanka": "斯里兰卡",
"Nepal": "尼泊尔",
# Common Cities
"New York City": "纽约",
"London": "伦敦",
"Paris": "巴黎",
"Tokyo": "东京",
"Guangzhou": "广州",
"Shenzhen": "深圳",
"Beijing": "北京",
"Shanghai": "上海",
# US States
"California": "加利福尼亚州",
"New York": "纽约州",
"Texas": "得克萨斯州",
"Florida": "佛罗里达州",
"Illinois": "伊利诺伊州",
"Pennsylvania": "宾夕法尼亚州",
"Arizona": "亚利桑那州",
"Nevada": "内华达州",
"Washington": "华盛顿州",
"Massachusetts": "马萨诸塞州",
"Colorado": "科罗拉多州",
"Georgia": "佐治亚州",
"North Carolina": "北卡罗来纳州",
"Michigan": "密歇根州",
"Oregon": "俄勒冈州",
"District of Columbia": "华盛顿哥伦比亚特区",
"Hawaii": "夏威夷州",
# Major Cities
"Los Angeles": "洛杉矶",
"San Francisco": "旧金山",
"San Diego": "圣地亚哥",
"San Jose": "圣何塞",
"Sacramento": "萨克拉门托",
"Oakland": "奥克兰",
"Fresno": "弗雷斯诺",
"Buffalo": "布法罗",
"Rochester": "罗切斯特",
"Albany": "奥尔巴尼",
"Syracuse": "雪城",
"Houston": "休斯敦",
"Dallas": "达拉斯",
"Austin": "奥斯汀",
"San Antonio": "圣安东尼奥",
"Fort Worth": "沃思堡",
"El Paso": "埃尔帕索",
"Miami": "迈阿密",
"Orlando": "奥兰多",
"Tampa": "坦帕",
"Jacksonville": "杰克逊维尔",
"Fort Lauderdale": "堡劳德代尔",
"Chicago": "芝加哥",
"Aurora": "奥罗拉",
"Philadelphia": "费城",
"Pittsburgh": "匹兹堡",
"Harrisburg": "哈里斯堡",
"Phoenix": "菲尼克斯",
"Tucson": "图森",
"Las Vegas": "拉斯维加斯",
"Reno": "里诺",
"Seattle": "西雅图",
"Tacoma": "塔科马",
"Spokane": "斯波坎",
"Boston": "波士顿",
"Denver": "丹佛",
"Atlanta": "亚特兰大",
"Detroit": "底特律",
"Portland": "波特兰",
"Osaka": "大阪",
"Kyoto": "京都",
"Kobe": "神户",
"Nagoya": "名古屋",
"Sapporo": "札幌",
"Fukuoka": "福冈",
"Hiroshima": "广岛",
"Liverpool": "利物浦",
"Manchester": "曼彻斯特",
"Birmingham": "伯明翰",
"Leeds": "利兹",
"Edinburgh": "爱丁堡",
"Glasgow": "格拉斯哥",
"Marseille": "马赛",
"Lyon": "里昂",
"Nice": "尼斯",
"Berlin": "柏林",
"Munich": "慕尼黑",
"Hamburg": "汉堡",
"Frankfurt": "法兰克福",
"Rome": "罗马",
"Milan": "米兰",
"Venice": "威尼斯",
"Florence": "佛罗伦萨",
"Naples": "那不勒斯",
"Madrid": "马德里",
"Barcelona": "巴塞罗那",
"Seville": "塞维利亚",
"Sydney": "悉尼",
"Melbourne": "墨尔本",
"Brisbane": "布里斯班",
"Perth": "珀斯",
"Toronto": "多伦多",
"Montreal": "蒙特利尔",
"Vancouver": "温哥华",
"Seoul": "首尔",
"Busan": "釜山",
"Mumbai": "孟买",
"New Delhi": "新德里",
"Bangalore": "班加罗尔",
"Moscow": "莫斯科",
"Saint Petersburg": "圣彼得堡",
"Sao Paulo": "圣保罗",
"Rio de Janeiro": "里约热内卢",
"Mexico City": "墨西哥城",
"Amsterdam": "阿姆斯特丹",
"Brussels": "布鲁塞尔",
"Zurich": "苏黎世",
"Vienna": "维也纳",
"Lisbon": "里斯本",
"Athens": "雅典",
"Istanbul": "伊斯坦布尔",
"Dubai": "迪拜",
"Bangkok": "曼谷",
"Hanoi": "河内",
"Ho Chi Minh City": "胡志明市",
"Jakarta": "雅加达",
"Kuala Lumpur": "吉隆坡",
"Manila": "马尼拉",
"Cairo": "开罗",
"Johannesburg": "约翰内斯堡",
"Cape Town": "开普敦",
# Theme Names
"Feature-Based Shading": "特征着色",
"Japanese Ink": "日式水墨",
"Midnight Blue": "午夜蓝",
"Terracotta": "陶土色",
"Autumn": "秋日",
"Blueprint": "蓝图",
"Contrast Zones": "高对比度",
"Copper Patina": "铜绿",
"Forest": "森林",
"Gradient Roads": "渐变道路",
"Monochrome Blue": "单色蓝",
"Neon Cyberpunk": "霓虹赛博",
"Noir": "诺尔黑白",
"Ocean": "海洋",
"Pastel Dream": "蜡笔梦幻",
"Sunset": "日落",
"Warm Beige": "温暖米色",
}
# Add inverse mappings to ensure completeness
for k, v in CN_TO_EN.items():
if v not in EN_TO_CN:
EN_TO_CN[v] = k
# Expose helpers
__all__ = [
"CITIES",
"get_countries",
"get_provinces",
"get_cities",
"get_districts",
"get_city_full_name",
"translate",
"get_country_key",
"get_manual_coordinates",
"get_china_adcode",
]
# Manual overrides for city centers to ensure logical centering (e.g. Tiananmen for Beijing)
CITY_CENTERS = {
"北京": (39.9042, 116.4074),
"Beijing": (39.9042, 116.4074),
"上海": (31.2304, 121.4737),
"Shanghai": (31.2304, 121.4737),
"广州": (23.1291, 113.2644),
"Guangzhou": (23.1291, 113.2644),
"深圳": (22.5422, 114.0579),
"Shenzhen": (22.5422, 114.0579),
"杭州": (30.2741, 120.1551),
"Hangzhou": (30.2741, 120.1551),
"成都": (30.6570, 104.0660),
"Chengdu": (30.6570, 104.0660),
"南京": (32.0603, 118.7969),
"Nanjing": (32.0603, 118.7969),
"武汉": (30.5928, 114.3055),
"Wuhan": (30.5928, 114.3055),
"西安": (34.3416, 108.9398),
"Xi'an": (34.3416, 108.9398),
"苏州": (31.2990, 120.5853),
"Suzhou": (31.2990, 120.5853),
"重庆": (29.5630, 106.5516),
"Chongqing": (29.5630, 106.5516),
"天津": (39.1255, 117.1901),
"Tianjin": (39.1255, 117.1901),
"香港": (22.3193, 114.1694),
"Hong Kong": (22.3193, 114.1694),
"台北": (25.0330, 121.5654),
"Taipei": (25.0330, 121.5654),
"New York City": (40.7128, -74.0060),
"纽约": (40.7128, -74.0060),
"London": (51.5074, -0.1278),
"伦敦": (51.5074, -0.1278),
"Paris": (48.8566, 2.3522),
"巴黎": (48.8566, 2.3522),
"Tokyo": (35.6895, 139.6917),
"东京": (35.6895, 139.6917),
}
def get_manual_coordinates(name, parent_adcode=None):
"""Return manual coordinates if available, else None."""
if not name:
return None
# Priority 1: Hardcoded overrides
coords = CITY_CENTERS.get(name)
if coords:
return coords
# Priority 2: Local China data if parent_adcode is known
if parent_adcode:
info = _load_china_info()
parent_entry = info.get(str(parent_adcode))
if parent_entry and "children" in parent_entry:
for child in parent_entry["children"]:
if child["name"] == name:
# center is [lng, lat]
return (child["center"][1], child["center"][0])
return None
def get_china_adcode(name, parent_adcode=None):
"""Search for adcode by name in local China data."""
if not name:
return None
# Strip convention-based suffixes
search_name = name.replace("_WHOLE", "")
info = _load_china_info()
if not parent_adcode:
# Check provinces
china_entry = info.get("100000")
if china_entry:
for p in china_entry.get("children", []):
if p["name"] == search_name or p["name"].rstrip("省").rstrip("市") == search_name:
return p["adcode"]
else:
parent_entry = info.get(str(parent_adcode))
if parent_entry:
for child in parent_entry.get("children", []):
if child["name"] == search_name:
return child["adcode"]
return None
def translate(text, target_lang="en"):
"""
Simple translation helper.
target_lang: 'en' or 'cn'
"""
if not text:
return text
if target_lang == "en":
# If text is already in CN_TO_EN keys (Chinese), return the English value
res = CN_TO_EN.get(text)
if res:
return res
# Try stripping suffixes
suffixes = ["市", "省", "自治区", "特别行政区", "壮族自治区", "回族自治区", "维吾尔自治区", "地区", "盟", "自治州"]
for suffix in suffixes:
if text.endswith(suffix):
stripped = text[: -len(suffix)]
res = CN_TO_EN.get(stripped)
if res:
return res
return text
if target_lang == "cn":
# If text is in EN_TO_CN keys (English), return the Chinese value
return EN_TO_CN.get(text, text)
return text
def get_countries(lang="en"):
"""Return list of (Display, Key) tuples for countries."""
choices = []
for country_key in sorted(CITIES.keys()):
display = translate(country_key, lang)
choices.append((display, country_key))
# Sort by display name
return sorted(choices, key=lambda x: x[0])
def get_country_key(name):
"""Map a localized name back to the CITIES dictionary key."""
# Check if it's already a key
if name in CITIES:
return name
# Check if it's an English name that needs to be Chinese key
cn_name = EN_TO_CN.get(name)
if cn_name in CITIES:
return cn_name
# Check if it's a Chinese name that needs to be English key
en_name = CN_TO_EN.get(name)
if en_name in CITIES:
return en_name
return name
def get_provinces(country_name, lang="en"):
"""Return list of (Display, Key) tuples for provinces/states."""
country_key = get_country_key(country_name)
# Special handling for China using local data
if country_key == "中国":
info = _load_china_info()
china_entry = info.get("100000")
if china_entry:
choices = []
for p in china_entry.get("children", []):
name = p["name"]
# For UI display, maybe strip '省' if in English?
# Actually following current project style, we use Chinese keys for China
display = translate(name, lang)
choices.append((display, name))
return sorted(choices, key=lambda x: x[0])
if country_key in CITIES:
choices = []
for p_key in CITIES[country_key].keys():
display = translate(p_key, lang)
choices.append((display, p_key))
return sorted(choices, key=lambda x: x[0])
return []
def get_cities(country_name, province_name, lang="en"):
"""Return list of (Display, Key) tuples for cities."""
country_key = get_country_key(country_name)
all_choices = []
# Add "Whole Province" option for China
if country_key == "中国":
whole_province_display = "整个省" if lang == "cn" else "Whole Province"
all_choices.append((whole_province_display, province_name + "_WHOLE"))
# Try local China GeoJSON data (info.json)
if country_key == "中国":
p_adcode = get_china_adcode(province_name)
if p_adcode:
info = _load_china_info()
p_entry = info.get(str(p_adcode))
if p_entry:
# For municipalities, return [Whole Province, Municipality Name]
if (
p_entry.get("children")
and p_entry["children"][0].get("level") == "district"
):
all_choices.append((translate(province_name, lang), province_name))
return all_choices
for c in p_entry.get("children", []):
name = c["name"]
display = translate(name, lang)
all_choices.append((display, name))
# If we found cities in info.json, we return them (plus Whole Province)
if len(all_choices) > 1:
# Keep "Whole Province" at top, then sorted cities
return [all_choices[0]] + sorted(all_choices[1:], key=lambda x: x[0])
# Fallback to CITIES dictionary
province_key = province_name
if country_key in CITIES:
if province_key not in CITIES[country_key]:
for p_key in CITIES[country_key].keys():
transl_p = translate(p_key, lang)
if (transl_p == province_key or
(country_key == "中国" and (transl_p in province_key or province_key in transl_p))):
province_key = p_key
break
if province_key in CITIES[country_key]:
start_idx = len(all_choices)
for c_name in CITIES[country_key][province_key]:
display = translate(c_name, lang)
all_choices.append((display, c_name))
if country_key == "中国":
# With Whole Province at top
return [all_choices[0]] + sorted(all_choices[1:], key=lambda x: x[0])
else:
return sorted(all_choices, key=lambda x: x[0])
return all_choices
def get_districts(country_name, province_name, city_name, lang="en"):
"""Return list of (Display, Key) tuples for districts."""
country_key = get_country_key(country_name)
if country_key != "中国" or not city_name or city_name.endswith("_WHOLE"):
return []
p_adcode = get_china_adcode(province_name)
if not p_adcode:
return []
c_adcode = get_china_adcode(city_name, p_adcode)
if not c_adcode:
# Case: City is the same as Province (Municipality)
if city_name == province_name:
c_adcode = p_adcode
else:
return []
info = _load_china_info()
c_entry = info.get(str(c_adcode))
if c_entry:
choices = []
# Add "Whole City" option
whole_city_display = "整个城市" if lang == "cn" else "Whole City"
choices.append(
(whole_city_display, city_name)
) # Use city name as key for 'whole city'
for d in c_entry.get("children", []):
name = d["name"]
display = translate(name, lang)
choices.append((display, name))
# Keep "Whole City" at top, then sorted districts
return [choices[0]] + sorted(choices[1:], key=lambda x: x[0])
return []
def search_cities(query):
"""
Search for cities matching the query (checks both Native and English names).
Returns list of tuples: (city, province, country)
"""
if not query or len(query) < 2:
return []
query = query.lower()
results = []
for country, provinces in CITIES.items():
for province, cities in provinces.items():
for city in cities:
city_en = CN_TO_EN.get(city, "").lower()
# Match against native name or English name
if query in city.lower() or (city_en and query in city_en):
results.append((city, province, country))
# Sort by city name
return sorted(results, key=lambda x: x[0])[:20]
def get_city_full_name(city, province, country, target_lang=None):
"""
Return formatted full name.
If target_lang is provided ('en' or 'cn'), translates components.
"""
if target_lang:
city = translate(city, target_lang)
province = translate(province, target_lang)
country = translate(country, target_lang)
if province == city: # Direct-controlled municipalities
return f"{city}, {country}"
return f"{city}, {province}, {country}"