# geo_elevation_visualizer.py
import os
import sys
import logging
import traceback
import numpy as np
import matplotlib
matplotlib.use('TkAgg') # 使用Tkinter兼容的后端
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.colors import BoundaryNorm
import cartopy.crs as ccrs
import rioxarray as rxr
import geopandas as gpd
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import threading
class GeoElevationVisualizer:
def __init__(self, root):
self.root = root
self.root.title("地理高程数据可视化工具")
self.root.geometry("1000x700")
self.root.resizable(True, True)
# 配置日志
self.setup_logging()
# 创建GUI框架
self.create_widgets()
# 默认值
self.default_values()
# 加载初始数据(如果存在)
self.load_last_settings()
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_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
self.logger = logging.getLogger('GeoVisualizer')
self.logger.setLevel(logging.DEBUG)
# 文件处理器
fh = logging.FileHandler(log_file)
fh.setLevel(logging.DEBUG)
# 控制台处理器
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 格式器
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# 添加处理器
self.logger.addHandler(fh)
self.logger.addHandler(ch)
self.logger.info("应用程序启动")
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.Frame(control_frame)
file_frame.pack(fill=tk.X, pady=5)
ttk.Label(file_frame, text="高程数据文件:").pack(anchor=tk.W)
self.file_entry = ttk.Entry(file_frame, width=40)
self.file_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
ttk.Button(file_frame, text="浏览...", command=self.browse_file).pack(side=tk.RIGHT)
# 区域选择部分
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)
ttk.Label(output_frame, text="文件名:").pack(anchor=tk.W)
self.output_file_entry = ttk.Entry(output_frame)
self.output_file_entry.pack(fill=tk.X, padx=5, 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.dpi_var = tk.IntVar(value=600)
ttk.Label(options_frame, text="图像DPI:").pack(anchor=tk.W)
ttk.Scale(options_frame, from_=100, to=1200, variable=self.dpi_var,
orient=tk.HORIZONTAL, length=180).pack(fill=tk.X)
self.dpi_label = ttk.Label(options_frame, text=f"当前DPI: {self.dpi_var.get()}")
self.dpi_label.pack(anchor=tk.E)
self.dpi_var.trace_add("write", self.update_dpi_label)
# 按钮区域
button_frame = ttk.Frame(control_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="预览", command=self.preview).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="生成图像", command=self.generate_image).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack(side=tk.RIGHT, padx=5)
# 预览区域
self.preview_label = ttk.Label(preview_frame, text="预览将在此处显示")
self.preview_label.pack(fill=tk.BOTH, expand=True)
# 状态栏
self.status_var = tk.StringVar()
self.status_var.set("就绪")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def default_values(self):
"""设置默认值"""
self.file_entry.insert(0, r"C:/documents/datasets/remote_sensing/DEM/GMTED2010_land.tif")
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")
self.output_file_entry.insert(0, "elevation_map.png")
def update_dpi_label(self, *args):
"""更新DPI标签"""
self.dpi_label.config(text=f"当前DPI: {self.dpi_var.get()}")
def browse_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)
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)
def validate_inputs(self):
"""验证输入参数"""
required_fields = [
(self.file_entry, "高程数据文件"),
(self.xmin_entry, "最小经度"),
(self.xmax_entry, "最大经度"),
(self.ymin_entry, "最小纬度"),
(self.ymax_entry, "最大纬度"),
(self.output_dir_entry, "输出目录"),
(self.output_file_entry, "输出文件名")
]
for entry, name in required_fields:
if not entry.get().strip():
messagebox.showerror("输入错误", f"请填写{name}!")
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
# 验证文件路径
if not os.path.isfile(self.file_entry.get()):
messagebox.showerror("文件错误", f"高程数据文件不存在: {self.file_entry.get()}")
return False
return True
def get_settings(self):
"""获取当前设置"""
return {
"elevation_file": self.file_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(),
"output_file": self.output_file_entry.get(),
"show_grid": self.grid_var.get(),
"show_city": self.city_marker_var.get(),
"dpi": self.dpi_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.txt")
try:
with open(settings_file, 'w') as f:
for key, value in settings.items():
f.write(f"{key}={value}\n")
messagebox.showinfo("保存成功", f"设置已保存到: {settings_file}")
except Exception as e:
messagebox.showerror("保存失败", f"保存设置时出错: {str(e)}")
def load_last_settings(self):
"""加载上次保存的设置"""
settings_file = os.path.join(os.path.expanduser("~"), "pyMet", "settings", "last_settings.txt")
if not os.path.exists(settings_file):
return
try:
settings = {}
with open(settings_file, 'r') as f:
for line in f:
key, value = line.strip().split('=', 1)
settings[key] = value
# 应用设置
self.file_entry.delete(0, tk.END)
self.file_entry.insert(0, settings.get("elevation_file", ""))
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.output_file_entry.delete(0, tk.END)
self.output_file_entry.insert(0, settings.get("output_file", "elevation_map.png"))
self.grid_var.set(settings.get("show_grid", "True") == "True")
self.city_marker_var.set(settings.get("show_city", "True") == "True")
self.dpi_var.set(int(settings.get("dpi", "600")))
self.logger.info("加载上次的设置")
except Exception as e:
self.logger.error(f"加载设置失败: {str(e)}")
def preview(self):
"""预览地图"""
if not self.validate_inputs():
return
self.status_var.set("正在生成预览...")
self.root.update()
# 在新线程中运行预览
threading.Thread(target=self._generate_preview, daemon=True).start()
def _generate_preview(self):
"""实际生成预览"""
try:
settings = self.get_settings()
# 读取高程数据
self.logger.info("读取高程数据...")
f = rxr.open_rasterio(settings["elevation_file"])
f = f.isel(band=0)
# 裁剪数据
self.logger.info("裁剪数据...")
lon = f.coords["x"]
lat = f.coords["y"]
da = f.loc[dict(
x=lon[(lon >= settings["xmin"]) & (lon <= settings["xmax"])],
y=lat[(lat >= settings["ymin"]) & (lat <= settings["ymax"])]
)]
# 重新提取经纬度
lon = da.coords["x"]
lat = da.coords["y"]
Lon, Lat = np.meshgrid(lon, lat)
# 创建地图
self.logger.info("创建地图...")
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
mapcrs = ccrs.PlateCarree()
fig = plt.figure(figsize=(6, 6))
ax = plt.axes(projection=mapcrs)
# 设置高程色阶
levels = np.arange(50, 2501, 100)
colorMap = plt.colormaps['terrain']
colorNorm = BoundaryNorm(levels, ncolors=colorMap.N, extend='both')
# 绘制高程图
pm = ax.pcolormesh(
Lon, Lat, da,
cmap=colorMap,
norm=colorNorm,
transform=mapcrs
)
# 添加城市标记
if settings["show_city"]:
ax.scatter(
settings["city_lon"], settings["city_lat"],
color='red',
linewidth=2,
marker='^',
transform=mapcrs
)
ax.text(
settings["city_lon"], settings["city_lat"] + 0.15,
settings["city_name"],
horizontalalignment='center',
color="red",
transform=mapcrs
)
# 添加网格线
if settings["show_grid"]:
ax.gridlines(
crs=mapcrs,
draw_labels={"bottom": "x", "left": "y", "right": "y"},
linewidth=0.5,
linestyle='--',
color='black'
)
# 设置标题
ax.set_title(f"{settings['city_name']}区域高程图")
# 保存预览图像
preview_path = os.path.join(os.path.expanduser("~"), "pyMet", "preview.png")
plt.savefig(preview_path, dpi=150, bbox_inches='tight')
plt.close(fig)
# 在GUI中显示预览
img = Image.open(preview_path)
img = img.resize((600, 600), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
self.preview_label.config(image=photo)
self.preview_label.image = photo # 保持引用
self.status_var.set("预览生成完成")
self.logger.info("预览生成完成")
except Exception as e:
self.status_var.set(f"错误: {str(e)}")
self.logger.error(f"生成预览失败: {str(e)}")
messagebox.showerror("生成预览失败", str(e))
def generate_image(self):
"""生成高质量图像"""
if not self.validate_inputs():
return
self.status_var.set("正在生成高质量图像...")
self.root.update()
# 在新线程中运行图像生成
threading.Thread(target=self._generate_image, daemon=True).start()
def _generate_image(self):
"""实际生成高质量图像"""
try:
settings = self.get_settings()
# 确保输出目录存在
os.makedirs(settings["output_dir"], exist_ok=True)
output_path = os.path.join(settings["output_dir"], settings["output_file"])
# 读取高程数据
self.logger.info("读取高程数据...")
f = rxr.open_rasterio(settings["elevation_file"])
f = f.isel(band=0)
# 裁剪数据
self.logger.info("裁剪数据...")
lon = f.coords["x"]
lat = f.coords["y"]
da = f.loc[dict(
x=lon[(lon >= settings["xmin"]) & (lon <= settings["xmax"])],
y=lat[(lat >= settings["ymin"]) & (lat <= settings["ymax"])]
)]
# 重新提取经纬度
lon = da.coords["x"]
lat = da.coords["y"]
Lon, Lat = np.meshgrid(lon, lat)
# 创建地图
self.logger.info("创建地图...")
plt.rcParams['font.family'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False
mapcrs = ccrs.PlateCarree()
fig = plt.figure(figsize=(10, 10))
ax = plt.axes(projection=mapcrs)
# 设置高程色阶
levels = np.arange(50, 2501, 100)
colorMap = plt.colormaps['terrain']
colorNorm = BoundaryNorm(levels, ncolors=colorMap.N, extend='both')
# 绘制高程图
pm = ax.pcolormesh(
Lon, Lat, da,
cmap=colorMap,
norm=colorNorm,
transform=mapcrs
)
# 添加颜色条
cb = fig.colorbar(
pm,
ax=ax,
shrink=0.8,
location="right",
pad=0.05
)
cb.set_label('高程 (米)', fontsize=12)
# 添加城市标记
if settings["show_city"]:
ax.scatter(
settings["city_lon"], settings["city_lat"],
color='red',
s=80,
marker='^',
edgecolor='black',
linewidth=1.5,
transform=mapcrs,
zorder=10
)
ax.text(
settings["city_lon"], settings["city_lat"] + 0.15,
settings["city_name"],
horizontalalignment='center',
verticalalignment='bottom',
color="red",
fontsize=14,
fontweight='bold',
transform=mapcrs,
bbox=dict(facecolor='white', alpha=0.7, boxstyle='round,pad=0.3'),
zorder=10
)
# 添加网格线
if settings["show_grid"]:
gl = ax.gridlines(
crs=mapcrs,
draw_labels={"bottom": "x", "left": "y", "right": "y"},
linewidth=0.5,
linestyle='--',
color='black',
alpha=0.7
)
gl.top_labels = False
gl.right_labels = False
# 设置标题
ax.set_title(f"{settings['city_name']}区域高程地形图", fontsize=16, pad=15)
# 添加比例尺文本
ax.text(
0.02, 0.02,
f"比例尺: 1:{int(1e6 / (settings['xmax'] - settings['xmin']))}",
transform=ax.transAxes,
fontsize=10,
bbox=dict(facecolor='white', alpha=0.7)
)
# 保存图像
plt.savefig(output_path, dpi=settings["dpi"], bbox_inches='tight')
plt.close(fig)
self.status_var.set(f"图像已保存到: {output_path}")
self.logger.info(f"图像已保存到: {output_path}")
messagebox.showinfo("生成成功", f"高程图已保存到:\n{output_path}")
# 更新预览
img = Image.open(output_path)
img.thumbnail((600, 600), Image.LANCZOS)
photo = ImageTk.PhotoImage(img)
self.preview_label.config(image=photo)
self.preview_label.image = photo
except Exception as e:
self.status_var.set(f"错误: {str(e)}")
self.logger.error(f"生成图像失败: {str(e)}")
messagebox.showerror("生成图像失败", str(e))
def main():
root = tk.Tk()
app = GeoElevationVisualizer(root)
root.mainloop()
if __name__ == "__main__":
main()
这个就可以正常打开,但是你的前面的这几个不行
最新发布