initial commit
This commit is contained in:
5
.cargo/config.toml
Normal file
5
.cargo/config.toml
Normal 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
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/target/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
*.log
|
||||
config.json
|
||||
4963
Cargo.lock
generated
Normal file
4963
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal 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
45
build.rs
Normal 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")
|
||||
}
|
||||
BIN
location.ico
Normal file
BIN
location.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
82
src/api.rs
Normal file
82
src/api.rs
Normal 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
51
src/config.rs
Normal 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
329
src/main.rs
Normal 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");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user