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