<think>我们注意到之前的代码在`draw_admin_labels`函数中尝试使用字体时中断了。我们需要完成这个函数,并确保整个程序能够正常运行。此外,我们还需要处理字体加载失败的情况,以及确保在绘制文本时使用正确的字体。我们将完成`draw_admin_labels`函数,并修复其他可能存在的问题。以下是完整的代码,包括修复字体加载和文本绘制部分:```python#geo_elevation_visualizer_with_real_data.pyimportosimportjsonimportloggingimporttkinterastkfromtkinterimportttk,filedialog,messagebox,simpledialogimportnumpyasnpfromPILimportImage,ImageDraw,ImageTk,ImageFontimporttimeimportrasteriofromrasterio.windowsimportWindowimporttracebackclassGeoElevationVisualizer:def__init__(self,root):self.root=rootself.root.title("高程图生成工具(真实数据版)")self.root.geometry("1000x750")#设置日志系统self.setup_logging()#创建主界面self.create_widgets()#设置默认值self.default_values()极#加载上次设置self.load_last_settings()#初始化状态self.is_generating=Falseself.elevation_data=Noneself.logger.info("应用程序启动完成")defsetup_logging(self):"""配置日志记录系统"""log_dir=os.path.join(os.path.expanduser("~"),"pyMet","logs")os.makedirs(log_dir,exist_ok=True)log_file=os.path.join(log_dir,f"geo_visualizer_{time.strftime('%Y%m%d_%H%M%S')}.log")self.logger=logging.getLogger('GeoVisualizer')self.logger.setLevel(logging.INFO)#文件处理器fh=logging.FileHandler(log_file)fh.setLevel(logging.INFO)formatter=logging.Formatter('%(asctime)s-%(name)s-%(levelname)s-%(message)s')fh.setFormatter(formatter)self.logger.addHandler(fh)#控制台处理器ch=logging.StreamHandler()ch.setLevel(logging.INFO)ch.setFormatter(formatter)self.logger.addHandler(ch)defcreate_widgets(self):"""创建GUI界面"""#创建主框架main_frame=ttk.Frame(self.root,padding="10")main_frame.pack(fill=tk.BOTH,expand=True)#左侧控制面板control_frame=ttk.LabelFrame(main_frame,text="控制面板",padding="10")control_frame.pack(side=tk.LEFT,fill=tk.Y,padx=(0,10))#右侧预览面板preview_frame=ttk.LabelFrame(main_frame,text="预览",padding="10")preview_frame.pack(side=tk.RIGHT,fill=tk.BOTH,expand=True)#高程数据文件选择file_frame=ttk.LabelFrame(control_frame,text="高程数据",padding="5")file_frame.pack(fill=tk.X,pady=5)ttk.Label(file_frame,text="高程文件(TIFF):").pack(anchor=tk.W)self.file_entry=ttk.Entry(file_frame)self.file_entry.pack(fill=tk.X,padx=5,pady=2)ttk.Button(file_frame,text="浏览...",command=self.browse_elevation_file).pack(anchor=tk.E,pady=2)#行政区划选择部分admin_frame=ttk.LabelFrame(control_frame,text="行政区划",padding="5")admin_frame.pack(fill=tk.X,pady=5)#省、市、县三级选择province_frame=ttk.Frame(admin_frame)province_frame.pack(fill=tk.X,pady=2)ttk.Label(province_frame,text="省份:").pack(side=tk.LEFT,padx=5)self.province_entry=ttk.Entry(province_frame,width=20)self.province_entry.pack(side=tk.LEFT,fill=tk.X,expand=True,pad极=5)ttk.Button(province_frame,text="选择",command=lambda:self.select_admin_level("省")).pack(side=tk.RIGHT,padx=5)city_frame=ttk.Frame(admin_frame)city_frame.pack(fill=tk.X,pady=2)ttk.Label(city_frame,text="城市:").pack(side=tk.LEFT,padx=5)self.city_entry=ttk.Entry(city_frame,width=20)self.city_entry.pack(side=tk.LEFT,fill=tk.X,expand=True,padx=5)ttk.Button(city_frame,text="选择",command=lambda:self.select_admin_level("市")).pack(side=tk.RIGHT,padx=5)county_frame=ttk.Frame(admin_frame)county_frame.pack(fill=tk.X,pady=2)ttk.Label(county_frame,text="区县:").pack(side=tk.LEFT,padx=5)self.county_entry=ttk.Entry(county_frame,width=20)self.county_entry.pack(side=tk.LEFT,fill=tk.X,expand=True,padx=5)ttk.Button(county_frame,text="选择",command=lambda:self.select_admin_level("县")).pack(side=tk.RIGHT,padx=5)#地理区域选择部分region_frame=ttk.LabelFrame(control_frame,text="地理区域",padding="5")region_frame.pack(fill=tk.X,pady=5)#坐标输入coord_frame=ttk.Frame(region_frame)coord_frame.pack(fill=tk.X,pady=5)ttk.Label(coord_frame,text="最小经度(xmin):").grid(row=0,column=0,sticky=tk.W,padx=5)self.xmin_entry=ttk.Entry(coord_frame,width=10)self.xmin_entry.grid(row=0,column=1,padx=5)ttk.Label(coord_frame,text="最大经度(xmax):").grid(row=0,column=2,sticky=tk.W,padx=5)self.xmax_entry=ttk.Entry(coord_frame,width=10)self.xmax_entry.grid(row=0,column=3,padx=5)ttk.Label(coord_frame,text="最小纬度(ymin):").grid(row=1,column=0,sticky=tk.W,padx=5)self.ymin_entry=ttk.Entry(coord_frame,width=10)self.ymin_entry.grid(row=1,column=1,padx=5)ttk.Label(coord_frame,text="最大纬度(ymax):").grid(row=1,column=2,sticky=tk.W,padx=5)self.ymax_entry=ttk.Entry(coord_frame,width=10)self.ymax_entry.grid(row=1,column=3,padx=5)#城市标记city_frame=ttk.Frame(region_frame)city_frame.pack(fill=tk.X,pady=5)ttk.Label(city_frame,text="城市名称:").grid(row=0,column=0,sticky=tk.W,padx=5)self.city_name_entry=ttk.Entry(city_frame,width=15)self.city_name_entry.grid(row=0,column=1,padx=5)ttk.Label(city_frame,text="经度:").grid(row=0,column=2,sticky=tk.W,padx=5)self.city_lon_entry=ttk.Entry(city_frame,width=10)self.city_lon_entry.grid(row=0,column=3,padx=5)ttk.Label(city_frame,text="纬度:").grid(row=0,column=4,sticky=tk.W,padx=5)self.city_lat_entry=ttk.Entry(city_frame,width=10)self.city_lat_entry.grid(row=0,column=5,padx=5)#输出设置output_frame=ttk.LabelFrame(control_frame,text="输出设置",padding="5")output_frame.pack(fill=tk.X,pady=5)ttk.Label(output_frame,text="输出目录:").pack(anchor=tk.W)self.output_dir_entry=ttk.Entry(output_frame)self.output_dir_entry.pack(fill=tk.X,padx=5,pady=2)ttk.Button(output_frame,text="浏览...",command=self.browse_output_dir).pack(anchor=tk.E,pady=2)#选项设置options_frame=ttk.LabelFrame(control_frame,text="选项",padding="5")options_frame.pack(fill=tk.X,pady=5)self.grid_var=tk.BooleanVar(value=True)ttk.Checkbutton(options_frame,text="显示网格",variable=self.grid_var).pack(anchor=tk.W)self.city_marker_var=tk.BooleanVar(value=True)ttk.Checkbutton(options_frame,text="显示城市标记",variable=self.city_marker_var).pack(anchor=tk.W)self.admin_label_var=tk.BooleanVar(value=True)ttk.Checkbutton(options_frame,text="标注行政区划",variable=self.admin_label_var).pack(anchor=tk.W)#图像尺寸设置size_frame=ttk.Frame(options_frame)size_frame.pack(fill=tk.X,pady=5)ttk.Label(size_frame,text="图像宽度:").pack(side=tk.LEFT,padx=5)self.width_var=tk.IntVar(value=800)width_spin=ttk.Spinbox(size_frame,from_=400,to=2000,width=5,textvariable=self.width_var)width_spin.pack(side=tk.LEFT,padx=5)ttk.Label(size_frame,text="高度:").pack(side=tk.LEFT,padx=5)self.height_var=tk.IntVar(value=600)height_spin=ttk.Spinbox(size_frame,from_=300,to=2000,width=5,textvariable=self.height_var)height_spin.pack(side=tk.LEFT,padx=5)#按钮区域button_frame=ttk.Frame(control_frame)button_frame.pack(fill=tk.X,pady=10)self.preview_btn=ttk.Button(button_frame,text="预览基础图",command=lambda:self.generate_image("basic"))self.preview_btn.pack(side=tk.LEFT,padx=5)self.preview_detail_btn=ttk.Button(button_frame,text="预览详细图",command=lambda:self.generate_image("detail"))self.preview_detail_btn.pack(side=tk.LEFT,padx=5)self.generate_btn=ttk.Button(button_frame,text="保存两张图像",command=self.save_images)self.generate_btn.pack(side=tk.LEFT,padx=5)ttk.Button(button_frame,text="保存设置",command=self.save_settings).pack(side=tk.RIGHT,padx=5)#预览区域self.preview_canvas=tk.Canvas(preview_frame,bg="white",width=600,height=500)self.preview_canvas.pack(fill=tk.BOTH,expand=True)#状态栏self.status_bar=ttk.Frame(self.root,relief=tk.SUNKEN)self.status_bar.pack(side=tk.BOTTOM,fill=tk.X)self.status_var=tk.StringVar(value="就绪")status_label=ttk.Label(self.status_bar,textvariable=self.status_var,anchor=tk.W)status_label.pack(side=tk.LEFT,fill=tk.X,expand=True,padx=5)#进度条self.progress_var=tk.DoubleVar(value=0)progress_bar=ttk.Progressbar(self.status_bar,variable=self.progress_var,maximum=100)progress_bar.pack(side=tk.RIGHT,padx=5)defbrowse_elevation_file(self):"""浏览高程数据文件"""file_path=filedialog.askopenfilename(title="选择高程数据文件",filetypes=[("TIFF文件","*.tif*.tiff"),("所有文件","*.*")])iffile_path:self.file_entry.delete(0,tk.END)self.file_entry.insert(0,file_path)self.logger.info(f"选择高程文件:{file_path}")defselect_admin_level(self,level):"""选择行政区划级别"""iflevel=="省":selected=simpledialog.askstring("选择省份","输入省份名称:",initialvalue=self.province_entry.get(),parent=self.root)ifselected:self.province_entry.delete(0,tk.END)self.province_entry.insert(0,selected)eliflevel=="市":selected=simpledialog.askstring("选择城市","输入城市名称:",initialvalue=self.city_entry.get(),parent=self.root)ifselected:self.city_entry.delete(0,tk.END)self.city_entry.insert(0,selected)eliflevel=="县":selected=simpledialog.askstring("选择区县","输入区县名称:",initialvalue=self.county_entry.get(),parent=self.root)ifselected:self.county_entry.delete(0,tk.END)self.county_entry.insert(0,selected)defget_admin_title(self):"""获取行政区划标题"""province=self.province_entry.get().strip()city=self.city_entry.get().strip()county=self.county_entry.get().strip()ifprovinceandcityandcounty:returnf"{province}{city}{county}区域高程图"elifprovinceandcity:returnf"{province}{city}区域高程图"elifprovince:returnf"{province}区域高程图"else:return"区域高程图"defget_output_filename(self,suffix=""):"""获取输出文件名"""province=self.province_entry.get().strip()city=self.city_entry.get().strip()county=self.county_entry.get().strip()city_name=self.city_name_entry.get().strip()ifprovinceandcityandcounty:base_name=f"{province}_{city}_{county}_高程图"elifprovinceandcity:base_name=f"{province}_{city}_高程图"elifprovince:base_name=f"{province}_高程图"elifcity_name:base_name=f"{city_name}_高程图"else:base_name="elevation_map"ifsuffix:returnf"{base_name}_{suffix}.png"else:returnf"{base_name}.png"defdefault_values(self):"""设置默认值"""self.file_entry.insert(0,r"C:\datasets\dem\GMTED2010_land.tif")self.province_entry.insert(0,"山西省")self.city_entry.insert(0,"太原市")self.county_entry.insert(0,"小店区")self.xmin_entry.insert(0,"110.0")self.xmax_entry.insert(0,"115.0")self.ymin_entry.insert(0,"34.5")self.ymax_entry.insert(0,"41.0")self.city_name_entry.insert(0,"太原")self.city_lon_entry.insert(0,"112.55")self.city_lat_entry.insert(0,"37.87")self.output_dir_entry.insert(0,r"C:\pyMet\figures")defbrowse_output_dir(self):"""浏览输出目录"""dir_path=filedialog.askdirectory(title="选择输出目录")ifdir_path:self.output_dir_entry.delete(0,tk.END)self.output_dir_entry.insert(0,dir极ath)self.logger.info(f"设置输出目录:{dir_path}")defvalidate_inputs(self):"""验证输入参数"""#验证高程文件elevation_file=self.file_entry.get().strip()ifnotelevation_file:messagebox.showerror("输入错误","请选择高程数据文件!")returnFalseifnotos.path.isfile(elevation_file):messagebox.showerror("文件错误",f"高程数据文件不存在:{elevation_file}")returnFalse#验证行政区划ifnotself.province_entry.get().strip():messagebox.showerror("输入错误","请填写省份!")returnFalse#验证坐标值try:xmin=float(self.xmin_entry.get())xmax=float(self.xmax_entry.get())ymin=float(self.ymin_entry.get())ymax=float(self.ymax_entry.get())ifxmin>=xmaxorymin>=ymax:messagebox.showerror("坐标错误","最小坐标值必须小于最大坐标值!")returnFalseexceptValueError:messagebox.showerror("坐标错误","坐标值必须是有效的数字!")returnFalse#验证城市位置ifself.city_marker_var.get():try:float(self.city_lon_entry.get())float(self.city_lat_entry.get())exceptValueError:messagebox.showerror("坐标错误","城市坐标必须是有效的数字!")returnFalsereturnTruedefget_settings(self):"""获取当前设置"""return{"elevation_file":self.file_entry.get(),"province":self.province_entry.get(),"city":self.city_entry.get(),"county":self.county_entry.get(),"xmin":float(self.xmin_entry.get()),"xmax":float(self.xmax_entry.get()),"ymin":float(self.ymin_entry.get()),"ymax":float(self.ymax_entry.get()),"city_name":self.city_name_entry.get(),"city_lon":float(self.city_lon_entry.get()),"city_lat":float(self.city_lat_entry.get()),"output_dir":self.output_dir_entry.get(),"show_grid":self.grid_var.get(),"show_city":self.city_marker_var.get(),"show_admin_label":self.admin_label_var.get(),"width":self.width_var.get(),"height":self.height_var.get()}defsave_settings(self):"""保存当前设置到文件"""settings=self.get_settings()settings_dir=os.path.join(os.path.expanduser("~"),"pyMet","settings")os.makedirs(settings_dir,exist_ok=True)settings_file=os.path.join(settings_dir,"last_settings.json")try:withopen(settings_file,'w')asf:json.dump(settings,f,indent=2)messagebox.showinfo("保存成功",f"设置已保存到:{settings_file}")self.logger.info(f"设置已保存:{settings_file}")exceptExceptionase:messagebox.showerror("保存失败",f极"保存设置时出错:{str(e)}")self.logger.error(f"保存设置失败:{str(e)}")defload_last_settings(self):"""加载上次保存的设置"""settings_file=os.path.join(os.path.expanduser("~"),"pyMet","settings","last_settings.json")ifnotos.path.exists(settings_file):returntry:withopen(settings_file,'r')asf:settings=json.load(f)#应用设置self.file_entry.delete(0,tk.END)self.file_entry.insert(0,settings.get("elevation_file",r"C:\datasets\dem\GMTED2010_land.tif"))self.province_entry.delete(0,tk.END)self.province_entry.insert(0,settings.get("province","山西省"))self.city_entry.delete(0,tk.END)self.city_entry.insert(0,settings.get("city","太原市"))self.county_entry.delete(0,tk.END)self.county_entry.insert(0,settings.get("county","小店区"))self.xmin_entry.delete(0,tk.END)self.xmin_entry.insert(0,settings.get("xmin","110.0"))self.xmax_entry.delete(0,tk.END)self.xmax_entry.insert(0,settings.get("xmax","115.0"))self.ymin_entry.delete(0,tk.END)self.ymin_entry.insert(0,settings.get("ymin","34.5"))self.ymax_entry.delete(0,tk.END)self.ymax_entry.insert(0,settings.get("ymax","41.0"))self.city_name_entry.delete(0,tk.END)self.city_name_entry.insert(0,settings.get("city_name","太原"))self.city_lon_entry.delete(0,tk.END)self.city_lon_entry.insert(0,settings.get("city_lon","112.55"))self.city_lat_entry.delete(0,tk.END)self.city_lat_entry.insert(0,settings.get("city_lat","37.87"))self.output_dir_entry.delete(0,tk.END)self.output_dir_entry.insert(0,settings.get("output_dir",r"C:\pyMet\figures"))self.grid_var.set(settings.get("show_grid",True))self.city_marker_var.set(settings.get("show_city",True))self.admin_label_var.set(settings.get("show_admin_label",True))self.width_var.set(settings.get("width",800))self.height_var.set(settings.get("height",600))self.logger.info("加载上次的设置")exceptExceptionase:self.logger.error(f"加载设置失败:{str(e)}")defgenerate_image(self,image_type="basic"):"""生成并显示图像"""ifnotself.validate_inputs():returnifself.is_generating:messagebox.showwarning("警告","当前正在生成图像,请稍候")returntry:self.is_generating=Trueself.status_var.set(f"正在生成{image_type}图像...")self.root.update()settings=self.get_settings()width=settings["width"]height=settings["height"]#创建图像对象img=Image.new('RGB',(width,height),(240,240,240))#绘制地形self.draw_terrain(img,settings)#添加城市标记ifsettings["show_city"]:self.draw_city_marker(img,settings)#添加网格线ifsettings["show_grid"]:self.draw_grid(img,settings)#添加行政区划标注ifsettings["show_admin_label"]:self.draw_admin_labels(img,settings,image_type)#在画布上显示图像self.show_on_canvas(img)self.status_var.set(f"{image_type}图像生成完成")self.logger.info(f"生成预览:{image_type}")exceptExceptionase:self.status_var.set(f"错误:{str(e)}")self.logger.error(f"生成图像失败:{str(e)}\n{traceback.format_exc()}")messagebox.showerror("生成图像失败",str(e))finally:self.is_generating=Falsedefsave_images(self):"""保存两张图像到文件"""ifnotself.validate_inputs():returnifself.is_generating:messagebox.showwarning("警告","当前正在生成图像,请稍候")returntry:self.is_generating=Trueself.status_var.set("正在保存图像...")self.root.update()settings=self.get_settings()output_dir=settings["output_dir"]os.makedirs(output_dir,exist_ok=True)#生成基础图basic_path=os.path.join(output_dir,self.get_output_filename("基础图"))self.generate_and_save_image("basic",basic_path)#生成详细图detail_path=os.path.join(output_dir,self.get_output_filename("详细图"))self.generate_and_save_image("detail",detail_path)#显示最后生成的图像self.status_var.set(f"图像已保存到:{basic_path}和{detail_path}")self.logger.info(f"图像已保存到:{basic_path}和{detail_path}")messagebox.showinfo("保存成功",f"图像已保存到:\n{basic_path}\n和\n{detail_path}")exceptExceptionase:self.status_var.set(f"错误:{str(e)}")self.logger.error(f"保存图像失败:{str(e)}\n{traceback.format_exc()}")messagebox.showerror("保存图像失败",str(e))finally:self.is_generating=Falsedefgenerate_and_save_image(self,image_type,output_path):"""生成并保存图像到文件"""settings=self.get_settings()width=settings["width"]height=settings["height"]#创建图像对象img=Image.new('RGB',(width,height),(240,240,240))#绘制地形self.draw_terrain(img,settings)#添加城市标记ifsettings["show_city"]:self.draw_city_marker(img,settings)#添加网格线ifsettings["show_grid"]:self.draw_grid(img,settings)#添加行政区划标注ifsettings["show_admin_label"]:self.draw_admin_labels(img,settings,image_type)#保存图像img.save(output_path)self.logger.info(f"保存图像到:{output_path}")#显示在预览区域self.show_on_canvas(img)defload_elevation_data(self,file_path,bounds):"""加载高程数据"""self.status_var.set("正在加载高程数据...")self.root.update()try:withrasterio.open(file_path)assrc:#获取地理边界xmin,xmax=bounds["xmin"],bounds["xmax"]ymin,ymax=bounds["ymin"],bounds["ymax"]#计算像素位置row_min,col_min=src.index(xmin,ymax)#左上角row_max,col_max=src.index(xmax,ymin)#右下角#确保坐标在图像范围内rows=min(src.height,max(1,abs(row_max-row_min)))cols=min(src.width,max(1,abs(col_max-col_min)))#读取数据窗口window=Window(col_off=min(col_min,col_max),row_off=min(row_min,row_max),width=cols,height=rows)#读取数据data=src.read(1,window=window)#更新实际边界(可能因像素对齐而调整)actual_bounds=src.window_bounds(window)returndata,actual_boundsexceptExceptionase:self.logger.error(f"加载高程数据失败:{str(e)}\n{traceback.format_exc()}")messagebox.showerror("加载高程数据失败",f"无法加载高程数据:{str(e)}")returnNone,Nonedefdraw_terrain(self,img,settings):"""绘制地形高程图"""width,height=img.sizedraw=ImageDraw.Draw(img)#获取高程数据elevation_file=settings["elevation_file"]bounds={"xmin":settings["xmin"],"xmax":settings["xmax"],"ymin":settings["ymin"],"ymax":settings["ymax"]}#尝试加载高程数据elevation_data,actual_bounds=self.load_elevation_data(elevation_file,bounds)ifelevation_dataisNone:#使用模拟数据作为后备self.status_var.set("使用模拟高程数据...")self.root.update()returnself.draw_simulated_terrain(img,settings)#更新实际边界ifactual_bounds:settings["xmin"]=actual_bounds[0]settings["ymin"]=actual_bounds[1]settings["xmax"]=actual_bounds[2]settings["ymax"]=actual_bounds[3]#获取高程数据范围min_elev=np.nanmin(elevation_data)max_elev=np.nanmax(elevation_data)elevation_range=max_elev-min_elev#计算缩放比例data_height,data_width=elevation_data.shapescale_x=data_width/widthscale_y=data_height/height#绘制地形foryinrange(height):forxinrange(width):#计算数据位置data_x=min(int(x*scale_x),data_width-1)data_y=min(int(y*scale_y),data_height-1)#获取高程值elev=elevation_data[data_y,data_x]#跳过无效值ifnp.isnan(elev):continue#根据高程值确定颜色ifelev<0:#海洋depth=min(1.0,abs(elev)/1000)r=int(0*depth)g=int(0*depth)b=int(128+127*depth)else:#陆地normalized=(elev-min_elev)/elevation_rangeifnormalized<0.2:#海滩/平原r=int(210*normalized+45)g=int(180*normalized+75)b=int(140*normalized+115)elifnormalized<0.5:#丘陵r=int(34*normalized+100)g=int(139*normalized+50)b=int(34*normalized)elifnormalized<0.8:#山脉r=int(139*normalized)g=int(115*normalized)b=int(85*normalized)else:#高山/雪山r=int(255*normalized)g=int(255*normalized)b=int(255*normalized)draw.point((x,y),fill=(r,g,b))#更新进度progress=(y/height)*100self.progress_var.set(progress)self.status_var.set(f"绘制地形...{int(progress)}%")self.root.update()defdraw_simulated_terrain(self,img,settings):"""绘制模拟地形(后备方案)"""width,height=img.sizedraw=ImageDraw.Draw(img)#计算坐标映射xmin,xmax=settings["xmin"],settings["xmax"]ymin,ymax=settings["ymin"],settings["ymax"]#模拟高程数据foryinrange(height):forxinrange(width):#计算地理坐标lon=xmin+(x/width)*(xmax-xmin)lat=ymin+(1-y/height)*(ymax-ymin)#反转Y轴#模拟高程值elevation=500+1000*np.sin(lon*0.1)*np.cos(lat*0.1)#根据高程值确定颜色ifelevation<100:color=(0,0,150)#深蓝-海洋elifelevation<300:color=(0,100,200)#浅蓝-浅海elifelevation<500:color=(210,180,140)#沙滩色elifelevation<800:color=(34,139,34)#森林绿elifelevation<1200:color=(139,115,85)#山地棕else:color=(255,255,255)#雪山白draw.point((x,y),fill=color)#更新进度progress=(y/height)*100self.progress_var.set(progress)self.status_var.set(f"绘制模拟地形...{int(progress)}%")self.root.update()defdraw_city_marker(self,img,settings):"""绘制城市标记"""width,height=img.sizedraw=ImageDraw.Draw(img)#计算坐标映射xmin,xmax=settings["xmin"],settings["xmax"]ymin,ymax=settings["ymin"],settings["ymax"]#计算城市在图像中的位置city_x=int(((settings["city_lon"]-xmin)/(xmax-xmin))*width)city_y=int(height-((settings["city_lat"]-ymin)/(ymax-ymin))*height)#绘制城市标记(三角形)marker_size=15points=[(city_x,city_y-marker_size),(city_x-marker_size,city_y+marker_size),(city_x+marker_size,city_y+marker_size)]draw.polygon(points,fill="red",outline="black")#绘制城市名称text=settings["city_name"]try:#尝试使用系统字体font=ImageFont.truetype("arial",16)except:#后备方案font=ImageFont.load_default()text_width=font.getsize(text)[0]draw.text((city_x-text_width//2,city_y-marker_size-25),text,fill="red",font=font)defdraw_grid(self,img,settings):"""绘制网格线"""width,height=img.sizedraw=ImageDraw.Draw(img)#计算坐标映射xmin,xmax=settings["xmin"],settings["xmax"]ymin,ymax=settings["ymin"],settings["ymax"]#绘制经线forloninnp.arange(np.floor(xmin),np.ceil(xmax)+0.5,0.5):x=int(((lon-xmin)/(xmax-xmin))*width)draw.line([(x,0),(x,height)],fill=(0,0,0,128),width=1)#绘制纬线forlatinnp.arange(np.floor(ymin),np.ceil(ymax)+0.5,0.5):y=int(height-((lat-ymin)/(ymax-ymin))*height)draw.line([(0,y),(width,y)],fill=(0,0,0,128),width=1)defdraw_admin_labels(self,img,settings,image_type):"""绘制行政区划标注"""width,height=img.sizedraw=ImageDraw.Draw(img)try:#尝试使用系统字体title_font=ImageFont.truetype("arial",20)label_font=ImageFont.truetype("arial",16)except:#后备方案title_font=ImageFont.load_default()label_font=ImageFont.load_default()#绘制标题title=self.get_admin_title()title_width=title_font.getsize(title)[0]draw.text((width//2-title_width//2,20),title,fill="black",font=title_font)ifimage_type=="detail":#绘制详细行政区信息province=settings["province"]city=settings["city"]county=settings["county"]draw.text((20,50),f"省份:{province}",fill="black",font=label_font)draw.text((20,80),f"城市:{city}",fill="black",font=label_font)draw.text((20,110),f"区县:{county}",fill="black",font=label_font)defshow_on_canvas(self,img):"""在画布上显示图像"""#调整图像大小以适应画布canvas_width=self.preview_canvas.winfo_width()canvas_height=self.preview_canvas.winfo_height()ifcanvas_width<=1orcanvas_height<=1:canvas_width=600canvas_height=500#保持宽高比img_ratio=img.width/img.heightcanvas_ratio=canvas_width/canvas_heightifimg_ratio>canvas_ratio:new_width=canvas_widthnew_height=int(canvas_width/img_ratio)else:new_height=canvas_heightnew_width=int(canvas_height*img_ratio)#缩放图像scaled_img=img.resize((new_width,new_height),Image.LANCZOS)photo=ImageTk.PhotoImage(scaled_img)#清除画布上的旧图像self.preview_canvas.delete("all")#将图像放置在画布中央x=(canvas_width-new_width)//2y=(canvas_height-new_height)//2self.preview_canvas.create_image(x,y,anchor=tk.NW,image=photo)self.preview_canvas.image=photo#保持引用defmain():root=tk.Tk()app=GeoElevationVisualizer(root)root.mainloop()if__name__=="__main__":main()```##关键改进1.**完成`draw_admin_labels`函数**:-添加了标题和详细行政区信息的绘制-使用不同的字体大小区分标题和详细标签-添加了字体加载的异常处理,确保在没有系统字体时使用默认字体2.**修复了`draw_city_marker`中的字体问题**:-使用`ImageFont.truetype`加载字体,并添加后备方案3.**优化了高程数据加载**:-使用`rasterio`读取高程数据-根据用户指定的地理边界加载数据窗口-处理无效高程值(NaN)4.**模拟地形作为后备**:-当高程数据加载失败时,使用模拟数据生成地形5.**进度和状态更新**:-在长时间操作中更新进度条和状态栏-允许GUI在生成过程中刷新##使用说明1.**安装依赖**:```bashpipinstallpillownumpyrasterio```2.**运行程序**:```bashpythongeo_elevation_visualizer_with_real_data.py```3.**功能**:-选择高程数据文件(TIFF格式)-设置行政区划和地理区域-预览基础图和详细图-保存两张高程图到指定目录这个版本集成了真实高程数据,并确保在数据加载失败时有后备方案,同时修复了字体加载问题,使文本标注能够正常显示。§§相关问题§§:1.如何处理高程数据文件中的坐标系统不一致问题?2.如何优化大型高程数据文件的读取性能?3.如何添加图例和比例尺到高程图中?4.如何实现高程数据的动态着色方案(如根据用户选择的不同色带)?</think>我注意到代码没有完全完成,特别是在`draw_admin_labels`函数中。让我修复并完成代码,确保所有部分都完整且可运行。以下是完整的、修复后的高程图生成工具代码:
```python
# geo_elevation_visualizer_with_real_data.py
import os
import json
import logging
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, simpledialog
import numpy as np
from PIL import Image, ImageDraw, ImageTk, ImageFont
import time
import rasterio
from rasterio.windows import Window
import traceback
class GeoElevationVisualizer:
def __init__(self, root):
self.root = root
self.root.title("高程图生成工具 (真实数据版)")
self.root.geometry("1000x750")
# 设置日志系统
self.setup_logging()
# 创建主界面
self.create_widgets()
# 设置默认值
self.default_values()
# 加载上次设置
self.load_last_settings()
# 初始化状态
self.is_generating = False
self.elevation_data = None
self.logger.info("应用程序启动完成")
def setup_logging(self):
"""配置日志记录系统"""
log_dir = os.path.join(os.path.expanduser("~"), "pyMet", "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f"geo_visualizer_{time.strftime('%Y%m%d_%H%M%S')}.log")
self.logger = logging.getLogger('GeoVisualizer')
self.logger.setLevel(logging.INFO)
# 文件处理器
fh = logging.FileHandler(log_file)
f极.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
self.logger.addHandler(fh)
# 控制台处理器
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
self.logger.addHandler(ch)
def create_widgets(self):
"""创建GUI界面"""
# 创建主框架
main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
# 左侧控制面板
control_frame = ttk.LabelFrame(main_frame, text="控制面板", padding="10")
control_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
# 右侧预览面板
preview_frame = ttk.LabelFrame(main_frame, text="预览", padding="10")
preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
# 高程数据文件选择
file_frame = ttk.LabelFrame(control_frame, text="高程数据", padding="5")
file_frame.pack(fill=tk.X, pady=5)
ttk.Label(file_frame, text="高程文件 (TIFF):").pack(anchor=tk.W)
self.file_entry = ttk.Entry(file_frame)
self.file_entry.pack(fill=tk.X, padx=5, pady=2)
ttk.Button(file_frame, text="浏览...", command=self.browse_elevation_file).pack(anchor=tk.E, pady=2)
# 行政区划选择部分
admin_frame = ttk.LabelFrame(control_frame, text="行政区划", padding极=5")
admin_frame.pack(fill=tk.X, pady=5)
# 省、市、县三级选择
province_frame = ttk.Frame(admin_frame)
province_frame.pack(fill=tk.X, pady=2)
ttk.Label(province_frame, text="省份:").pack(side=tk.LEFT, padx=5)
self.province_entry = ttk.Entry(province_frame, width=20)
self.province_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
ttk.Button(province_frame, text="选择", command=lambda: self.select_admin_level("省")).pack(side=tk.RIGHT, padx=5)
city_frame = ttk.Frame(admin_frame)
city_frame.pack(fill=tk.X, pady=2)
ttk.Label(city_frame, text="城市:").pack(side=tk.LEFT, padx=5)
self.city_entry = ttk.Entry(city_frame, width=20)
self.city_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
ttk.Button(city_frame, text="选择", command=lambda: self.select_admin_level("市")).pack(side=tk.RIGHT, padx=5)
county_frame = ttk.Frame(admin_frame)
county_frame.pack(fill=tk.X, pady=2)
ttk.Label(county_frame, text="区县:").pack(side=tk.LEFT, padx=5)
self.county_entry = ttk.Entry(county_frame, width=20)
self.county_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
ttk.Button(county_frame, text="选择", command=lambda: self.select_admin_level("县")).pack(side=tk.RIGHT, padx=5)
# 地理区域选择部分
region_frame = ttk.LabelFrame(control_frame, text="地理区域", padding="5")
region_frame.pack(fill=tk.X, pady=5)
# 坐标输入
coord_frame = ttk.Frame(region_frame)
coord_frame.pack(fill=tk.X, pady=5)
ttk.Label(coord_frame, text="最小经度 (xmin):").grid(row=0, column=0, sticky=tk.W, padx=5)
self.xmin_entry = ttk.Entry(coord_frame, width=10)
self.xmin_entry.grid(row=0, column=1, padx=5)
ttk.Label(coord_frame, text="最大经度 (xmax):").grid(row=0, column=2, sticky=tk.W, padx=5)
self.xmax_entry = ttk.Entry(coord_frame, width=10)
self.xmax_entry.grid(row=0, column=3, padx=5)
ttk.Label(coord_frame, text="最小纬度 (ymin):").grid(row=1, column=0, sticky=tk.W, padx=5)
self.ymin_entry = ttk.Entry(coord_frame, width=10)
self.ymin_entry.grid(row=1, column=1, padx=5)
ttk.Label(coord_frame, text="最大纬度 (ymax):").grid(row=1, column=2, sticky=tk.W, padx=5)
self.ymax_entry = ttk.Entry(coord_frame, width=10)
self.ymax_entry.grid(row=1, column=3, padx=5)
# 城市标记
city_frame = ttk.Frame(region_frame)
city_frame.pack(fill=tk.X, pady=5)
ttk.Label(city_frame, text="城市名称:").grid(row=0, column=0, sticky=tk.W, padx=5)
self.city_name_entry = ttk.Entry(city_frame, width=15)
self.city_name_entry.grid(row=0, column=1, padx=5)
ttk.Label(city_frame, text="经度:").grid(row=0, column=2, sticky=tk.W, padx=5)
self.city_lon_entry = ttk.Entry(city_frame, width=10)
self.city_lon_entry.grid(row=0, column=3, padx=5)
ttk.Label(city_frame, text="纬度:").grid(row=0, column=4, sticky=tk.W, padx=5)
self.city_lat_entry = ttk.Entry(city_frame, width=10)
self.city_lat_entry.grid(row=0, column=5, padx=5)
# 输出设置
output_frame = ttk.LabelFrame(control_frame, text="输出设置", padding="5")
output_frame.pack(fill=tk.X, pady=5)
ttk.Label(output_frame, text="输出目录:").pack(anchor=tk.W)
self.output_dir_entry = ttk.Entry(output_frame)
self.output_dir_entry.pack(fill=tk.X, padx=5, pady=2)
ttk.Button(output_frame, text="浏览...", command=self.browse_output_dir).pack(anchor=tk.E, pady=2)
# 选项设置
options_frame = ttk.LabelFrame(control_frame, text="选项", padding="5")
options_frame.pack(fill=tk.X, pady=5)
self.grid_var = tk.BooleanVar(value=True)
ttk.Checkbutton(options_frame, text="显示网格", variable=self.grid_var).pack(anchor=tk.W)
self.city_marker_var = tk.BooleanVar(value=True)
ttk.Checkbutton(options_frame, text="显示城市标记", variable=self.city_marker_var).pack(anchor=tk.W)
self.admin_label_var = tk.Boolean极(value=True)
ttk.Checkbutton(options_frame, text="标注行政区划", variable=self.admin_label_var).pack(anchor=tk.W)
# 图像尺寸设置
size_frame = ttk.Frame(options_frame)
size_frame.pack(fill=tk.X, pady=5)
ttk.Label(size_frame, text="图像宽度:").pack(side=tk.LEFT, padx=5)
self.width_var = tk.IntVar(value=800)
width_spin = ttk.Spinbox(size_frame, from_=400, to=2000, width=5, textvariable=self.width_var)
width_spin.pack(side=tk.LEFT, padx=5)
ttk.Label(size_frame, text="高度:").pack(side=tk.LEFT, padx=5)
self.height_var = tk.IntVar(value=600)
height_spin = ttk.Spinbox(size_frame, from_=300, to=2000, width=5, textvariable=self.height_var)
height_spin.pack(side=tk.LEFT, padx=5)
# 按钮区域
button_frame = ttk.Frame(control_frame)
button_frame.pack(fill=tk.X, pady=10)
self.preview_btn = ttk.Button(button_frame, text="预览基础图", command=lambda: self.generate_image("basic"))
self.preview_btn.pack(side=tk.LEFT, padx=5)
self.preview_detail_btn = ttk.Button(button_frame, text="预览详细图", command=lambda: self.generate_image("detail"))
self.preview_detail_btn.pack(side=tk.LEFT, padx=5)
self.generate_btn = ttk.Button(button_frame, text="保存两张图像", command=self.save_images)
self.generate_btn.pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack(side=tk.RIGHT, padx=5)
# 预览区域
self.preview_canvas = tk.Canvas(preview_frame, bg="white", width=600, height=500)
self.preview_canvas.pack(fill=tk.BOTH, expand=True)
# 状态栏
self.status_bar = ttk.Frame(self.root, relief=tk.SUNKEN)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
self.status_var = tk.StringVar(value="就绪")
status_label = ttk.Label(self.status_bar, textvariable=self.status_var, anchor=tk.W)
status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
# 进度条
self.progress_var = tk.DoubleVar(value=0)
progress_bar = ttk.Progressbar(self.status_bar, variable=self.progress_var, maximum=100)
progress_bar.pack(side=tk.RIGHT, padx=5)
def browse_elevation_file(self):
"""浏览高程数据文件"""
file_path = filedialog.askopenfilename(
title="选择高程数据文件",
filetypes=[("TIFF文件", "*.tif *.tiff"), ("所有文件", "*.*")]
)
if file_path:
self.file_entry.delete(0, tk.END)
self.file_entry.insert(0, file_path)
self.logger.info(f"选择高程文件: {file_path}")
def select_admin_level(self, level):
"""选择行政区划级别"""
if level == "省":
selected = simpledialog.askstring("选择省份", "输入省份名称:",
initialvalue=self.province_entry.get(),
parent=self.root)
if selected:
self.province_entry.delete(0, tk.END)
self.province_entry.insert(0, selected)
elif level == "市":
selected = simpledialog.askstring("选择城市", "输入城市名称:",
initialvalue=self.city_entry.get(),
parent=self.root)
if selected:
self.city_entry.delete(0, tk.END)
self.city_entry.insert(0, selected)
elif level == "县":
selected = simpledialog.askstring("选择区县", "输入区县名称:",
initialvalue=self.county_entry.get(),
parent=self.root)
if selected:
self.county_entry.delete(0, tk.END)
self.county_entry.insert(0, selected)
def get_admin_title(self):
"""获取行政区划标题"""
province = self.province_entry.get().strip()
city = self.city_entry.get().strip()
county = self.county_entry.get().strip()
if province and city and county:
return f"{province}{city}{county}区域高程图"
elif province and city:
return f"{province}{city}区域高程图"
elif province:
return f"{province}区域高程图"
else:
return "区域高程图"
def get_output_filename(self, suffix=""):
"""获取输出文件名"""
province = self.province_entry.get().strip()
city = self.city_entry.get().strip()
county = self.county_entry.get().strip()
city_name = self.city_name_entry.get().strip()
if province and city and county:
base_name = f"{province}_{city}_{county}_高程图"
elif province and city:
base_name = f"{province}_{city}_高程图"
elif province:
base_name = f"{province}_高程图"
elif city_name:
base_name = f"{city_name}_高程图"
else:
base_name = "elevation_map"
if suffix:
return f"{base_name}_{suffix}.png"
else:
return f"{base_name}.png"
def default_values(self):
"""设置默认值"""
self.file_entry.insert(0, r"C:\datasets\dem\GMTED2010_land.tif")
self.province_entry.insert(0, "山西省")
self.city_entry.insert(0, "太原市")
self.county_entry.insert(0, "小店区")
self.xmin_entry.insert(0, "110.0")
self.xmax_entry.insert(0, "115.0")
self.ymin_entry.insert(0, "34.5")
self.ymax_entry.insert(0, "41.0")
self.city_name_entry.insert(0, "太原")
self.city_lon_entry.insert(0, "112.55")
self.city_lat_entry.insert(0, "37.87")
self.output_dir_entry.insert(0, r"C:\pyMet\figures")
def browse_output_dir(self):
"""浏览输出目录"""
dir_path = filedialog.askdirectory(title="选择输出目录")
if dir_path:
self.output_dir_entry.delete(0, tk.END)
self.output_dir_entry.insert(0, dir_path)
self.logger.info(f"设置输出目录: {dir_path}")
def validate_inputs(self):
"""验证输入参数"""
# 验证高程文件
elevation_file = self.file_entry.get().strip()
if not elevation_file:
messagebox.showerror("输入错误", "请选择高程数据文件!")
return False
if not os.path.isfile(elevation_file):
messagebox.showerror("文件错误", f"高程数据文件不存在: {elevation_file}")
return False
# 验证行政区划
if not self.province_entry.get().strip():
messagebox.showerror("输入错误", "请填写省份!")
return False
# 验证坐标值
try:
xmin = float(self.xmin_entry.get())
xmax = float(self.xmax_entry.get())
ymin = float(self.ymin_entry.get())
ymax = float(self.ymax_entry.get())
if xmin >= xmax or ymin >= ymax:
messagebox.showerror("坐标错误", "最小坐标值必须小于最大坐标值!")
return False
except ValueError:
messagebox.showerror("坐标错误", "坐标值必须是有效的数字!")
return False
# 验证城市位置
if self.city_marker_var.get():
try:
float(self.city_lon_entry.get())
float(self.city_lat_entry.get())
except ValueError:
messagebox.showerror("坐标错误", "城市坐标必须是有效的数字!")
return False
return True
def get_settings(self):
"""获取当前设置"""
return {
"elevation_file": self.file_entry.get(),
"province": self.province_entry.get(),
"city": self.city_entry.get(),
"county": self.county_entry.get(),
"xmin": float(self.xmin_entry.get()),
"xmax": float(self.xmax_entry.get()),
"ymin": float(self.ymin_entry.get()),
"ymax": float(self.ymax_entry.get()),
"city_name": self.city_name_entry.get(),
"city_lon": float(self.city_lon_entry.get()),
"city_lat": float(self.city_lat_entry.get()),
"output_dir": self.output_dir_entry.get(),
"show_grid": self.grid_var.get(),
"show_city": self.city_marker_var.get(),
"show_admin_label": self.admin_label_var.get(),
"width": self.width_var.get(),
"height": self.height_var.get()
}
def save_settings(self):
"""保存当前设置到文件"""
settings = self.get_settings()
settings_dir = os.path.join(os.path.expanduser("~"), "pyMet", "settings")
os.makedirs(settings_dir, exist_ok=True)
settings_file = os.path.join(settings_dir, "last_settings.json")
try:
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
messagebox.showinfo("保存成功", f"设置已保存到: {settings_file}")
self.logger.info(f"设置已保存: {settings_file}")
except Exception as e:
messagebox.showerror("保存失败", f"保存设置时出错: {str(e)}")
self.logger.error(f"保存设置失败: {str(e)}")
def load_last_settings(self):
"""加载上次保存的设置"""
settings_file = os.path.join(os.path.expanduser("~"), "pyMet", "settings", "last_settings.json")
if not os.path.exists(settings_file):
return
try:
with open(settings_file, 'r') as f:
settings = json.load(f)
# 应用设置
self.file_entry.delete(0, tk.END)
self.file_entry.insert(0, settings.get("elevation_file", r"C:\datasets\dem\GMTED2010_land.tif"))
self.province_entry.delete(0, tk.END)
self.province_entry.insert(0, settings.get("province", "山西省"))
self.city_entry.delete(0, tk.END)
self.city_entry.insert(0, settings.get("city", "太原市"))
self.county_entry.delete(0, tk.END)
self.county_entry.insert(0, settings.get("county", "小店区"))
self.xmin_entry.delete(0, tk.END)
self.xmin_entry.insert(0, settings.get("xmin", "110.0"))
self.xmax_entry.delete(0, tk.END)
self.xmax_entry.insert(0, settings.get("xmax", "115.0"))
self.ymin_entry.delete(0, tk.END)
self.ymin_entry.insert(0, settings.get("ymin", "34.5"))
self.ymax_entry.delete(0, tk.END)
self.ymax_entry.insert(0, settings.get("ymax", "41.0"))
self.city_name_entry.delete(0, tk.END)
self.city_name_entry.insert(0, settings.get("city_name", "太原"))
self.city_lon_entry.delete(0, tk.END)
self.city_lon_entry.insert(0, settings.get("city_lon", "112.55"))
self.city_lat_entry.delete(0, tk.END)
self.city_lat_entry.insert(0, settings.get("city_lat", "37.87"))
self.output_dir_entry.delete(0, tk.END)
self.output_dir_entry.insert(0, settings.get("output_dir", r"C:\pyMet\figures"))
self.grid_var.set(settings.get("show_grid", True))
self.city_marker_var.set(settings.get("show_city", True))
self.admin_label_var.set(settings.get("show_admin_label", True))
self.width_var.set(settings.get("width", 800))
self.height_var.set(settings.get("height", 600))
self.logger.info("加载上次的设置")
except Exception as e:
self.logger.error(f"加载设置失败: {str(e)}")
def generate_image(self, image_type="basic"):
"""生成并显示图像"""
if not self.validate_inputs():
return
if self.is_generating:
messagebox.showwarning("警告", "当前正在生成图像,请稍候")
return
try:
self.is_generating = True
self.status_var.set(f"正在生成{image_type}图像...")
self.root.update()
settings = self.get_settings()
width = settings["width"]
height = settings["height"]
# 创建图像对象
img = Image.new('RGB', (width, height), (240, 240, 240))
# 绘制地形
self.draw_terrain(img, settings)
# 添加城市标记
if settings["show_city"]:
self.draw_city_marker(img, settings)
# 添加网格线
if settings["show_grid"]:
self.draw_grid(img, settings)
# 添加行政区划标注
if settings["show_admin_label"]:
self.draw_admin_labels(img, settings, image_type)
# 在画布上显示图像
self.show_on_canvas(img)
self.status_var.set(f"{image_type}图像生成完成")
self.logger.info(f"生成预览: {image_type}")
except Exception as e:
self.status_var.set(f"错误: {str(e)}")
self.logger.error(f"生成图像失败: {str(e)}\n{traceback.format_exc()}")
messagebox.showerror("生成图像失败", str(e))
finally:
self.is_generating = False
def save_images(self):
"""保存两张图像到文件"""
if not self.validate_inputs():
return
if self.is_generating:
messagebox.showwarning("警告", "当前正在生成图像,请极候")
return
try:
self.is_generating = True
self.status_var.set("正在保存图像...")
self.root.update()
settings = self.get_settings()
output_dir = settings["output_dir"]
os.makedirs(output_dir, exist_ok=True)
# 生成基础图
basic_path = os.path.join(output_dir, self.get_output_filename("基础图"))
self.generate_and_save_image("basic", basic_path)
# 生成详细图
detail_path = os.path.join(output_dir, self.get_output_filename("详细图"))
self.generate_and_save_image("detail", detail_path)
# 显示最后生成的图像
self.status_var.set(f"图像已保存到: {basic_path} 和 {detail_path}")
self.logger.info(f"图像已保存到: {basic_path} 和 {detail_path}")
messagebox.showinfo("保存成功", f"图像已保存到:\n{basic_path}\n和\n{detail_path}")
except Exception as e:
self.status_var.set(f"错误: {str(e)}")
self.logger.error(f"保存图像失败: {str(e)}\n{traceback.format_exc()}")
messagebox.showerror("保存图像失败", str(e))
finally:
self.is_generating = False
def generate_and_save_image(self, image_type, output_path):
"""生成并保存图像到文件"""
settings = self.get_settings()
width = settings["width"]
height = settings["height"]
# 创建图像对象
img = Image.new('RGB', (width, height), (240, 240, 240))
# 绘制地形
self.draw_terrain(img, settings)
# 添加城市标记
if settings["show_city"]:
self.draw_city_marker(img, settings)
# 添加网格线
if settings["show_grid"]:
self.draw_grid(img, settings)
# 添加行政区划标注
if settings["show_admin_label"]:
self.draw_admin_labels(img, settings, image_type)
# 保存图像
img.save(output_path)
self.logger.info(f"保存图像到: {output_path}")
# 显示在预览区域
self.show_on_canvas(img)
def load_elevation_data(self, file_path, bounds):
"""加载高程数据"""
self.status_var.set("正在加载高程数据...")
self.root.update()
try:
with rasterio.open(file_path) as src:
# 获取地理边界
xmin, xmax = bounds["xmin"], bounds["xmax"]
ymin, ymax = bounds["ymin"], bounds["ymax"]
# 计算像素位置
row_min, col_min = src.index(xmin, ymax) # 左上角
row_max, col_max = src.index(xmax, ymin) # 右下角
# 确保坐标在图像范围内
rows = min(src.height, max(1, abs(row_max - row_min)))
cols = min(src.width, max(1, abs(col_max - col_min)))
# 读取数据窗口
window = Window(
col_off=min(col_min, col_max),
row_off=min(row_min, row_max),
width=cols,
height=rows
)
# 读取数据
data = src.read(1, window=window)
# 更新实际边界(可能因像素对齐而调整)
actual_bounds = src.window_bounds(window)
return data, actual_bounds
except Exception as e:
self.logger.error(f"加载高程数据失败: {str(e)}\n{traceback.format_exc()}")
messagebox.showerror("加载高程数据失败", f"无法加载高程数据: {str(e)}")
return None, None
def draw_terrain(self, img, settings):
"""绘制地形高程图"""
width, height = img.size
draw = ImageDraw.Draw(img)
# 获取高程数据
elevation_file = settings["elevation_file"]
bounds = {
"xmin": settings["xmin"],
"xmax": settings["xmax"],
"ymin": settings["ymin"],
"ymax": settings["ymax"]
}
# 尝试加载高程数据
elevation_data, actual_bounds = self.load_elevation_data(elevation_file, bounds)
if elevation_data is None:
# 使用模拟数据作为后备
self.status_var.set("使用模拟高程数据...")
self.root.update()
return self.draw_simulated_terrain(img, settings)
# 更新实际边界
if actual_bounds:
settings["xmin"] = actual_bounds[0]
settings["ymin"] = actual_bounds[1]
settings["xmax"] = actual_bounds[2]
settings["ymax"] = actual_bounds[3]
# 获取高程数据范围
min_elev = np.nanmin(elevation_data)
max_elev = np.nanmax(elevation_data)
elevation_range = max_elev - min_elev
# 计算缩放比例
data_height, data_width = elevation_data.shape
scale_x = data_width / width
scale_y = data_height / height
# 绘制地形
for y in range(height):
for x in range(width):
# 计算数据位置
data_x = min(int(x * scale_x), data_width - 1)
data_y = min(int(y * scale_y), data_height - 1)
# 获取高程值
elev = elevation_data[data_y, data_x]
# 跳过无效值
if np.isnan(elev):
continue
# 根据高程值确定颜色
if elev < 0: # 海洋
depth = min(1.0, abs(elev) / 1000)
r = int(0 * depth)
g = int(0 * depth)
b = int(128 + 127 * depth)
else: # 陆地
normalized = (elev - min_elev) / elevation_range
if normalized < 0.2: # 海滩/平原
r = int(210 * normalized + 45)
g = int(180 * normalized + 75)
b = int(140 * normalized + 115)
elif normalized < 0.5: # 丘陵
r = int(34 * normalized + 100)
g = int(139 * normalized + 50)
b = int(34 * normalized)
elif normalized < 0.8: # 山脉
r = int(139 * normalized)
g = int(115 * normalized)
b = int(85 * normalized)
else: # 高山/雪山
r = int(255 * normalized)
g = int(255 * normalized)
b = int(255 * normalized)
draw.point((x, y), fill=(r, g, b))
# 更新进度
progress = (y / height) * 100
self.progress_var.set(progress)
self.status_var.set(f"绘制地形... {int(progress)}%")
self.root.update()
def draw_simulated_terrain(self, img, settings):
"""绘制模拟地形(后备方案)"""
width, height = img.size
draw = ImageDraw.Draw(img)
# 计算坐标映射
xmin, xmax = settings["xmin"], settings["xmax"]
ymin, ymax = settings["ymin"], settings["ymax"]
# 模拟高程数据
for y in range(height):
for x in range(width):
# 计算地理坐标
lon = xmin + (x / width) * (xmax - xmin)
lat = ymin + (1 - y / height) * (ymax - ymin) # 反转Y轴
# 模拟高程值
elevation = 500 + 1000 * np.sin(lon * 0.1) * np.cos(lat * 0.1)
# 根据高程值确定颜色
if elevation < 100:
color = (0, 0, 150) # 深蓝 - 海洋
elif elevation < 300:
color = (0, 100, 200) # 浅蓝 - 浅海
elif elevation < 500:
color = (210, 180, 140) # 沙滩色
elif elevation < 800:
color = (34, 139, 34) # 森林绿
elif elevation < 1200:
color = (139, 115, 85) # 山地棕
else:
color = (255, 255, 255) # 雪山白
draw.point((x, y), fill=color)
# 更新进度
progress = (y / height) * 100
self.progress_var.set(progress)
self.status_var.set(f"绘制模拟地形... {int(progress)}%")
self.root.update()
def draw_city_marker(self, img, settings):
"""绘制城市标记"""
width, height = img.size
draw = ImageDraw.Draw(img)
# 计算坐标映射
xmin, xmax = settings["xmin"], settings["xmax"]
ymin, ymax = settings["ymin"], settings["ymax"]
# 计算城市在图像中的位置
city_x = int(((settings["city_lon"] - xmin) / (xmax - xmin)) * width)
city_y = int(height - ((settings["city_lat"] - ymin) / (ymax - ymin)) * height)
# 绘制城市标记 (三角形)
marker_size = 15
points = [
(city_x, city_y - marker_size),
(city_x - marker_size, city_y + marker_size),
(city_x + marker_size, city_y + marker_size)
]
draw.polygon(points, fill="red", outline="black")
# 绘制城市名称
text = settings["city_name"]
try:
# 尝试使用系统字体
font = ImageFont.truetype("arial", 16)
except:
# 后备方案
font = ImageFont.load_default()
text_width = font.getsize(text)[0]
draw.text((city_x - text_width//2, city_y - marker_size - 25),
text, fill="red", font=font)
def draw_grid(self, img, settings):
"""绘制网格线"""
width, height = img.size
draw = ImageDraw.Draw(img)
# 计算坐标映射
xmin, xmax = settings["xmin"], settings["x极"]
ymin, ymax = settings["ymin"], settings["ymax"]
# 绘制经线
for lon in np.arange(np.floor(xmin), np.ceil(xmax) + 0.5, 0.5):
x = int(((lon - xmin) / (xmax - xmin)) * width)
draw.line([(x, 0), (x, height)], fill=(0, 0, 0, 128), width=1)
# 绘制纬线
for lat in np.arange(np.floor(ymin), np.ceil(ymax) + 0.5, 0.5):
y = int(height - ((lat - ymin) / (ymax - ymin)) * height)
draw.line([(0, y), (width, y)], fill=(0, 0, 0, 128), width=1)
def draw_admin_labels(self, img, settings, image_type):
"""绘制行政区划标注"""
width, height = img.size
draw = ImageDraw.Draw(img)
try:
# 尝试使用系统字体
title_font = ImageFont.truetype("arial", 20)
label_font = ImageFont