initial commit

This commit is contained in:
2026-05-12 17:27:18 +08:00
commit 38fc33cabf
10 changed files with 5505 additions and 0 deletions

5
.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
[build]
target = "x86_64-pc-windows-gnu"
[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link_args=-Wl,--subsystem,windows"]

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
/target/
*.pyc
__pycache__/
*.egg-info/
dist/
build/
.env
.venv/
venv/
*.log
config.json

4963
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "location2address"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.29"
egui = "0.29"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
env_logger = "0.11"
[profile.release]
opt-level = 2
lto = true
strip = true

45
build.rs Normal file
View File

@@ -0,0 +1,45 @@
use std::path::PathBuf;
use std::process::Command;
fn main() {
let ico_path = PathBuf::from("location.ico");
if !ico_path.exists() {
println!(
"cargo:warning=location.ico 不存在,将不嵌入图标。请将图标文件放入项目根目录。"
);
return;
}
println!("cargo:rerun-if-changed=location.ico");
println!("cargo:rerun-if-changed=icon.rc");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let res_output = out_dir.join("icon.res");
let windres_path = find_windres();
let status = Command::new(&windres_path)
.args([
"icon.rc",
"-O", "coff",
"-o",
])
.arg(&res_output)
.status()
.expect("执行 windres 失败");
if !status.success() {
panic!("windres 编译资源文件失败");
}
println!("cargo:rustc-link-arg={}", res_output.display());
}
fn find_windres() -> PathBuf {
let mingw_windres = PathBuf::from("C:\\msys64\\mingw64\\bin\\windres.exe");
if mingw_windres.exists() {
return mingw_windres;
}
PathBuf::from("windres")
}

1
icon.rc Normal file
View File

@@ -0,0 +1 @@
APP_ICON ICON "location.ico"

BIN
location.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

82
src/api.rs Normal file
View File

@@ -0,0 +1,82 @@
use serde::Deserialize;
const CONVERT_URL: &str = "https://restapi.amap.com/v3/assistant/coordinate/convert";
const REGEO_URL: &str = "https://restapi.amap.com/v3/geocode/regeo";
#[derive(Deserialize)]
struct ConvertResponse {
status: String,
info: String,
locations: Option<String>,
}
#[derive(Deserialize)]
struct RegeoResponse {
status: String,
info: String,
regeocode: Option<RegeoCode>,
}
#[derive(Deserialize)]
struct RegeoCode {
formatted_address: Option<String>,
}
pub fn convert_coordinates(
lng: f64,
lat: f64,
coordsys: &str,
api_key: &str,
) -> Result<(f64, f64), String> {
let url = format!(
"{}?locations={:.6},{:.6}&coordsys={}&key={}",
CONVERT_URL, lng, lat, coordsys, api_key
);
let resp: ConvertResponse = reqwest::blocking::get(&url)
.map_err(|e| format!("网络请求失败: {}", e))?
.json()
.map_err(|e| format!("解析响应失败: {}", e))?;
if resp.status != "1" {
return Err(format!("{}", resp.info));
}
let locations = resp.locations.ok_or("响应中没有坐标数据")?;
let parts: Vec<&str> = locations.split(',').collect();
if parts.len() != 2 {
return Err(format!("坐标格式异常: {}", locations));
}
let converted_lng: f64 = parts[0]
.parse()
.map_err(|_| format!("转换后经度解析失败: {}", parts[0]))?;
let converted_lat: f64 = parts[1]
.parse()
.map_err(|_| format!("转换后纬度解析失败: {}", parts[1]))?;
Ok((converted_lng, converted_lat))
}
pub fn reverse_geocode(lng: f64, lat: f64, api_key: &str) -> Result<String, String> {
let url = format!(
"{}?location={:.6},{:.6}&key={}&extensions=base",
REGEO_URL, lng, lat, api_key
);
let resp: RegeoResponse = reqwest::blocking::get(&url)
.map_err(|e| format!("网络请求失败: {}", e))?
.json()
.map_err(|e| format!("解析响应失败: {}", e))?;
if resp.status != "1" {
return Err(format!("{}", resp.info));
}
let regeocode = resp.regeocode.ok_or("响应中没有地址数据")?;
let address = regeocode
.formatted_address
.ok_or("响应中没有格式化地址")?;
Ok(address)
}

51
src/config.rs Normal file
View File

@@ -0,0 +1,51 @@
use log::info;
use std::path::PathBuf;
fn config_path() -> PathBuf {
let mut path = std::env::current_exe()
.expect("获取exe路径失败")
.parent()
.expect("获取exe目录失败")
.to_path_buf();
path.push("config.json");
path
}
pub fn load_api_key() -> String {
let path = config_path();
if !path.exists() {
return String::new();
}
match std::fs::read_to_string(&path) {
Ok(content) => {
#[derive(serde::Deserialize)]
struct Config {
api_key: String,
}
match serde_json::from_str::<Config>(&content) {
Ok(cfg) => cfg.api_key,
Err(_) => String::new(),
}
}
Err(_) => String::new(),
}
}
pub fn save_api_key(api_key: &str) -> Result<(), String> {
let path = config_path();
#[derive(serde::Serialize)]
struct Config<'a> {
api_key: &'a str,
}
let cfg = Config { api_key };
let content =
serde_json::to_string_pretty(&cfg).map_err(|e| format!("序列化失败: {}", e))?;
std::fs::write(&path, content).map_err(|e| format!("写入文件失败: {}", e))?;
info!("API Key 已保存到: {:?}", path);
Ok(())
}

329
src/main.rs Normal file
View File

@@ -0,0 +1,329 @@
#![windows_subsystem = "windows"]
mod api;
mod config;
use eframe::egui;
use log::info;
fn main() {
env_logger::init();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([520.0, 480.0])
.with_title("经纬度地址查询"),
..Default::default()
};
eframe::run_native(
"经纬度地址查询",
options,
Box::new(|cc| {
setup_chinese_fonts(&cc.egui_ctx);
Ok(Box::new(LocationApp::new()))
}),
)
.expect("启动应用失败");
}
fn setup_chinese_fonts(ctx: &egui::Context) {
let mut fonts = egui::FontDefinitions::default();
let font_paths = [
"C:\\Windows\\Fonts\\msyh.ttc",
"C:\\Windows\\Fonts\\msyh.ttf",
"C:\\Windows\\Fonts\\simsun.ttc",
"C:\\Windows\\Fonts\\simhei.ttf",
];
for path in &font_paths {
if let Ok(data) = std::fs::read(path) {
info!("加载中文字体: {}", path);
fonts.font_data.insert(
"chinese_font".to_owned(),
egui::FontData::from_owned(data),
);
fonts
.families
.get_mut(&egui::FontFamily::Proportional)
.unwrap()
.insert(0, "chinese_font".to_owned());
fonts
.families
.get_mut(&egui::FontFamily::Monospace)
.unwrap()
.insert(0, "chinese_font".to_owned());
break;
}
}
ctx.set_fonts(fonts);
}
enum Screen {
Main,
Config,
}
struct CoordinateSystem {
label: &'static str,
value: &'static str,
}
const COORD_SYSTEMS: &[CoordinateSystem] = &[
CoordinateSystem {
label: "GPS (WGS-84)",
value: "gps",
},
CoordinateSystem {
label: "高德 (GCJ-02)",
value: "autonavi",
},
CoordinateSystem {
label: "百度 (BD-09)",
value: "baidu",
},
CoordinateSystem {
label: "Mapbar",
value: "mapbar",
},
];
struct LocationApp {
screen: Screen,
longitude: String,
latitude: String,
coordsys_index: usize,
status_message: String,
result_address: String,
api_key: String,
config_key_input: String,
config_status: String,
}
impl LocationApp {
fn new() -> Self {
let api_key = config::load_api_key();
Self {
screen: Screen::Main,
longitude: String::new(),
latitude: String::new(),
coordsys_index: 1,
status_message: String::new(),
result_address: String::new(),
api_key,
config_key_input: String::new(),
config_status: String::new(),
}
}
fn do_query(&mut self) {
self.status_message.clear();
self.result_address.clear();
let lng = self.longitude.trim();
let lat = self.latitude.trim();
if lng.is_empty() || lat.is_empty() {
self.status_message = "请输入经度和纬度".to_string();
return;
}
let lng_val: f64 = match lng.parse() {
Ok(v) => v,
Err(_) => {
self.status_message = "经度格式错误".to_string();
return;
}
};
let lat_val: f64 = match lat.parse() {
Ok(v) => v,
Err(_) => {
self.status_message = "纬度格式错误".to_string();
return;
}
};
if self.api_key.is_empty() {
self.status_message = "请先在配置页面设置高德地图 API Key".to_string();
return;
}
let coordsys = COORD_SYSTEMS[self.coordsys_index].value;
let (final_lng, final_lat) = if coordsys != "autonavi" {
match api::convert_coordinates(lng_val, lat_val, coordsys, &self.api_key) {
Ok((clng, clat)) => {
self.status_message = format!(
"坐标转换成功: {:.6},{:.6}{:.6},{:.6}",
lng_val, lat_val, clng, clat
);
(clng, clat)
}
Err(e) => {
self.status_message = format!("坐标转换失败: {}", e);
return;
}
}
} else {
self.status_message = format!("查询坐标: {:.6},{:.6}", lng_val, lat_val);
(lng_val, lat_val)
};
match api::reverse_geocode(final_lng, final_lat, &self.api_key) {
Ok(address) => {
self.result_address = address;
}
Err(e) => {
self.status_message = format!("地址查询失败: {}", e);
}
}
}
fn save_config(&mut self) {
let key = self.config_key_input.trim();
if key.is_empty() {
self.config_status = "请输入 API Key".to_string();
return;
}
match config::save_api_key(key) {
Ok(()) => {
self.api_key = key.to_string();
self.config_status = "保存成功".to_string();
}
Err(e) => {
self.config_status = format!("保存失败: {}", e);
}
}
}
}
impl eframe::App for LocationApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
match self.screen {
Screen::Main => self.render_main(ctx),
Screen::Config => self.render_config(ctx),
}
}
}
impl LocationApp {
fn render_main(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
ui.heading("经纬度地址查询");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("配置").clicked() {
self.screen = Screen::Config;
self.config_key_input = self.api_key.clone();
self.config_status.clear();
}
});
});
ui.separator();
egui::Grid::new("input_grid")
.num_columns(2)
.spacing([8.0, 8.0])
.show(ui, |ui| {
ui.label("经度:");
ui.text_edit_singleline(&mut self.longitude);
ui.end_row();
ui.label("纬度:");
ui.text_edit_singleline(&mut self.latitude);
ui.end_row();
ui.label("坐标系:");
egui::ComboBox::from_id_salt("coordsys")
.selected_text(COORD_SYSTEMS[self.coordsys_index].label)
.show_ui(ui, |ui| {
for (i, cs) in COORD_SYSTEMS.iter().enumerate() {
ui.selectable_value(
&mut self.coordsys_index,
i,
cs.label,
);
}
});
ui.end_row();
});
ui.add_space(12.0);
if ui.button("查询").clicked() {
self.do_query();
}
ui.add_space(8.0);
if !self.status_message.is_empty() {
ui.label(&self.status_message);
}
if !self.result_address.is_empty() {
ui.separator();
ui.horizontal(|ui| {
ui.label("查询结果:");
if ui.button("复制").clicked() {
ui.output_mut(|o| o.copied_text = self.result_address.clone());
self.status_message = "已复制到剪贴板".to_string();
}
});
ui.add_space(4.0);
ui.colored_label(
egui::Color32::from_rgb(0, 128, 0),
&self.result_address,
);
}
});
}
fn render_config(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("配置");
ui.separator();
ui.add_space(8.0);
ui.label("高德地图 API Key:");
ui.add(
egui::TextEdit::singleline(&mut self.config_key_input)
.hint_text("请输入高德地图 Web API Key"),
);
ui.add_space(12.0);
ui.horizontal(|ui| {
if ui.button("保存").clicked() {
self.save_config();
}
if ui.button("返回").clicked() {
self.screen = Screen::Main;
self.config_status.clear();
}
});
if !self.config_status.is_empty() {
ui.add_space(8.0);
if self.config_status.contains("成功") {
ui.colored_label(
egui::Color32::from_rgb(0, 128, 0),
&self.config_status,
);
} else {
ui.colored_label(
egui::Color32::from_rgb(200, 0, 0),
&self.config_status,
);
}
}
ui.add_space(16.0);
ui.separator();
ui.label("说明:");
ui.label(" 请前往高德开放平台 https://lbs.amap.com 申请 Web 服务 API Key");
});
}
}