refactor: data, ui

This commit is contained in:
Matthieu Bessat 2024-07-06 13:49:59 +02:00
parent bb016884a3
commit 8d48a56812
24 changed files with 527 additions and 333 deletions

2
.rgignore Normal file
View file

@ -0,0 +1,2 @@
tmp
sample_data

View file

@ -11,3 +11,11 @@
- TOML Config file
- `~/.config/bobosm/config.toml`
- LSP
- we need to make a MVP for using LSP
- We can use LSP to provide auto_completions of tags and values in the editor
- custom file extension `.osm.txt` or enabled on demand
- we can use helix or nvim as it's compatible with LSP
- Key sequence feature

View file

@ -75,10 +75,13 @@ Logout with
You can upload your changeset,
Pressing `c` then `u`
Pressing `c`
Your default text editor will appear, you can then preview the changes included in the changeset
and type a changeset message.
The osmChange XML file will be writen in the local state to allow previewing if needed.
At the top you can type the tags of the changeset, the `comment` tag is required, `source` is welcomed.
At the bottom of this file, a list of new of affected objects are printed with comments starting with `//`
Then save and quit the editor.

View file

@ -1,14 +1,14 @@
// handle the data part of changeset managements
use fully_pub::fully_pub;
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use serde::{Serialize};
use quick_xml::se::{to_string_with_root};
use xdg::BaseDirectories;
use crate::{changeset, data::Element, layers::{get_dynamic_data_layer, Layer}, osm_api::OSMApiClient, USER_AGENT};
use crate::{data::Element, layers::Layer, USER_AGENT};
#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize)]
#[fully_pub]
struct Tag {
#[serde(rename = "@k")]
@ -19,6 +19,7 @@ struct Tag {
}
#[derive(Debug, Serialize)]
#[fully_pub]
struct NodeChanges {
#[serde(rename = "@id")]
id: i64,
@ -35,6 +36,7 @@ struct NodeChanges {
}
#[derive(Debug, Serialize)]
#[fully_pub]
struct ModifyChanges {
#[serde(rename = "node")]
nodes: Vec<NodeChanges>
@ -64,64 +66,8 @@ struct ChangesetMeta {
osm: ChangesetMetaInner
}
/*
<osm>
<changeset>
<tag k="created_by" v="JOSM 1.61"/>
<tag k="comment" v="Just adding some streetnames"/>
...
</changeset>
...
</osm>
*/
fn build_osm_change_xml(changeset: &ChangesetContent) -> Result<String> {
Ok(to_string_with_root("osmChange", changeset)?)
}
// fn open_changeset(&OsmC) -> Result<i64>
// {
// }
/// Will handle all the operations to collect the changes and create a changeset and push it
pub fn try_changeset(
xdg_dirs: &BaseDirectories,
layers: Arc<Mutex<Vec<Layer>>>,
osm_client: &OSMApiClient
) -> Result<()> {
let layers_ref = layers.lock().unwrap();
let dynamic_layer = match get_dynamic_data_layer(&layers_ref) {
Some(v) => v,
None => {
return Ok(())
}
};
let changeset_comment = String::from("Add source on POI");
let changeset_meta = changeset::ChangesetMeta {
osm: changeset::ChangesetMetaInner {
tags: vec![
Tag {
key: "comment".to_string(),
value: changeset_comment
},
Tag {
key: "created_by".to_string(),
value: USER_AGENT.to_string()
}
]
}
};
println!("changeset_meta: {:?}", changeset_meta);
// get the changeset id
let changeset_id = osm_client.open_changeset(changeset_meta)?;
println!("changeset_id: {}", changeset_id);
// build osmChange
/// build struct that will be used as osmChange from the dynamic layers
pub fn build_changes_content(changeset_id: i64, dynamic_layer: &Layer) -> ChangesetContent {
let mut nodes_changes: Vec<NodeChanges> = vec![];
for modified_e_id in &dynamic_layer.modified_data {
let modified_e = dynamic_layer.data_source.as_ref().unwrap()
@ -149,25 +95,15 @@ pub fn try_changeset(
_ => continue
}
}
let changeset_content = ChangesetContent {
ChangesetContent {
version: "0.6".to_string(),
generator: USER_AGENT.to_string(),
modify: Some(ModifyChanges {
nodes: nodes_changes
})
};
println!("changeset_content: {changeset_content:?}");
//TODO: store XML files in xdg dirs
println!("CHANGESET: Uploading changeset");
osm_client.upload_changes(changeset_id, changeset_content)
.context("Uploading changeset content")?;
println!("CHANGESET: Closing changeset");
osm_client.close_changeset(changeset_id)
.context("Closing changeset")?;
println!("CHANGESET: Changeset closed");
Ok(())
}
}
pub fn build_osm_change_xml(changeset: &ChangesetContent) -> Result<String> {
Ok(to_string_with_root("osmChange", changeset)?)
}

View file

@ -1,8 +1,11 @@
pub mod changeset;
pub mod session;
use std::collections::HashMap;
use fully_pub::fully_pub;
use raylib::math::Vector2;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
#[fully_pub]
type Tags = Option<HashMap<String, String>>;

40
src/data/session.rs Normal file
View file

@ -0,0 +1,40 @@
use std::fs;
use fully_pub::fully_pub;
use anyhow::{Result, Context, anyhow};
use serde::{Deserialize, Serialize};
use xdg::BaseDirectories;
// sessions models
#[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub]
struct OSMSession {
access_token: String,
scope: String,
token_type: String
}
#[derive(Debug, Serialize, Deserialize)]
#[fully_pub]
pub struct Session {
osm_session: Option<OSMSession>
}
pub fn read_session(xdg_dirs: &BaseDirectories) -> Result<Session> {
// find session file
let config_content =
fs::read_to_string(xdg_dirs.get_data_file("session.json"))
.map_err(|_e| anyhow!("Could not read session JSON file"))?;
let session: Session = serde_json::from_str(&config_content)
.map_err(|_e| anyhow!("Could not deserialize session file"))?;
Ok(session)
}
pub fn write_session(xdg_dirs: &BaseDirectories, session: &Session) -> Result<()> {
let session_path = xdg_dirs.place_data_file("session.json").context("Could not create or find session file")?;
fs::write(
session_path,
serde_json::to_string(session).context("Could not serialize session")?
).context("Could not write session")?;
Ok(())
}

View file

@ -40,33 +40,6 @@ pub fn get_objects_in_area(
elements_ids_per_layer
}
pub fn tags_to_text(tags: &Tags) -> String {
let tags_v = match tags {
None => return "".to_string(),
Some(v) => v
};
let mut out = String::new();
for (key, value) in tags_v.iter() {
out += &format!("{} = {}\n", &key, &value);
}
out
}
pub fn text_to_tags(tags_as_text: &str) -> Result<Tags> {
let mut tags: HashMap<String, String> = HashMap::new();
for line in tags_as_text.trim().split('\n') {
let line_content = line.to_string();
let compos: Vec<&str> = line_content.split('=').collect();
if compos.len() != 2 {
return Err(anyhow!("Found invalid tags syntax"));
}
let key = compos.first().unwrap().trim();
let val = compos.get(1).unwrap().trim();
tags.insert(key.to_string(), val.to_string());
}
Ok(Some(tags))
}
pub fn edit_tags(
xdg_dirs: &BaseDirectories,
app_state: Arc<Mutex<AppState>>,
@ -106,35 +79,44 @@ pub fn edit_tags(
/// open external editor
pub fn edit_tags_external(xdg_dirs: &BaseDirectories, initial_tags: &Tags) -> Result<Tags>
{
// write tags file
let tags_file_location = xdg_dirs.place_state_file("tags.txt")?;
let new_text = open_external_text_editor(
xdg_dirs,
"tags.txt",
&tags_to_text(initial_tags)
)?;
let new_tags = text_to_tags(&new_text)
.context("Parsing text tags")?;
Ok(new_tags)
}
fn open_external_text_editor(xdg_dirs: &BaseDirectories, file_name: &str, source_text: &str) -> Result<String> {
// write tmp file
let file_path = xdg_dirs.place_state_file(file_name)?;
fs::write(
&tags_file_location,
tags_to_text(initial_tags)
).context("Writing tags to file")?;
&file_path,
source_text
).context("Writing temp text to file")?;
// open cmd
// TODO: make the editor configurable
let _ = Command::new("alacritty")
.arg("--command")
.arg("helix")
.arg("--config")
.arg("/home/mbess/.config/helix/config.toml")
.arg(tags_file_location.clone())
.arg("~/.config/helix/config.toml")
.arg(&file_path)
.spawn()
.context("Failed to start external editor")?
.wait();
// wait for editor to exit
// parse new tags
let new_tags_content =
fs::read_to_string(&tags_file_location)
.context("Reading static osm data file")?;
let new_text_content =
fs::read_to_string(&file_path)
.context("Reading temp text file")?;
println!("{:?}", new_tags_content);
println!("new_text_content: {:?}", new_text_content);
let new_tags = text_to_tags(&new_tags_content)
.context("Parsing text tags")?;
println!("{:?}", new_tags);
Ok(new_tags)
Ok(new_text_content)
}

View file

@ -54,8 +54,6 @@ enum LayerKind {
DynamicOSMData
}
#[fully_pub]
struct Layer {
id: u64,
@ -109,7 +107,7 @@ pub fn build_displayed_pois(indexed_data: &IndexedData) -> Result<RTree<Displaye
let mut importance = -1;
// only show some amenities for now
if let Some(shop_class) = tags.get("shop") {
if ["bakery", "hairdresser"].contains(&shop_class.as_str()) {
if ["vacant", "bakery", "hairdresser", "butcher"].contains(&shop_class.as_str()) {
importance = 3;
}
}
@ -180,7 +178,7 @@ pub fn build_displayed_ways(indexed_data: &IndexedData) -> Result<RTree<Displaye
d_ways.push(DisplayedWay {
id: way.id,
name: final_name,
importance: 0,
importance: 1,
positions: nodes_pos,
bbox
});

View file

@ -1,9 +1,7 @@
mod data;
mod ui;
mod utils;
mod changeset;
mod layers;
mod editor;
mod osm_api;
#[cfg(test)]
@ -11,79 +9,29 @@ mod test_data;
#[cfg(test)]
mod test_layers;
use std::{collections::{HashMap, HashSet}, fs, sync::{Arc, Mutex}, thread::{self, JoinHandle}, time::{SystemTime, UNIX_EPOCH}};
use std::{collections::{HashMap, HashSet}, fs, sync::{Arc, Mutex}, thread::{self, JoinHandle}};
use anyhow::{Context, Result, anyhow};
use data::{bbox_center, IndexedData};
use editor::{edit_tags, get_objects_in_area};
use data::{bbox_center, session::{read_session, write_session, Session}, IndexedData};
use fully_pub::fully_pub;
use layers::{load_osm_data, load_osm_zone, load_static_cities_layer, Layer, LayerKind};
use osm_api::OSMApiClient;
use raylib::prelude::*;
use ui::messages::render_messages;
use utils::deg2num;
use ui::{messages::{render_messages, UiMessage, UiMessageLevel}, selection::get_objects_in_area};
use utils::{deg2num, get_envelope_from_bounds};
use xdg::BaseDirectories;
use std::io::Read;
use rstar::AABB;
use serde::{Deserialize, Serialize};
use serde::{Deserialize};
use ureq::Agent;
use rand::Rng;
use crate::utils::CanvasPos;
use crate::utils::Invertion;
use crate::utils::ToSlice;
use crate::utils::ToAABB;
pub const USER_AGENT: &str = "BobOSM dev";
trait Invertion {
fn invert_for_canvas(&self) -> Vector2;
}
impl Invertion for Vector2 {
fn invert_for_canvas(&self) -> Vector2 {
Vector2::new(self.x, -self.y)
}
}
trait CanvasPos {
fn to_canvas_pos(&self, camera: &Camera) -> Vector2;
}
impl CanvasPos for Vector2 {
fn to_canvas_pos(&self, camera: &Camera) -> Vector2 {
Vector2::new(
self.x - camera.bounds.0.x,
camera.bounds.1.y - self.y
).scale_by(camera.real_to_canvas_factor)
}
}
pub trait ToSlice {
fn to_slice(&self) -> [f32; 2];
}
impl ToSlice for Vector2 {
fn to_slice(&self) -> [f32; 2] {
[self.x, self.y]
}
}
pub trait ToAABB {
fn to_aabb(&self) -> AABB<[f32; 2]>;
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]>;
}
impl ToAABB for Vector2 {
fn to_aabb(&self) -> AABB<[f32; 2]> {
AABB::from_point([self.x, self.y])
}
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]> {
AABB::from_corners(
[self.x - margin, self.y - margin],
[self.x + margin, self.y + margin]
)
}
}
#[derive(Debug)]
struct Camera {
bounds: (Vector2, Vector2),
@ -101,61 +49,6 @@ enum UiMode {
Edit
}
#[derive(Debug)]
#[fully_pub]
enum UiMessageLevel {
Success,
Info,
Warning,
Error
}
#[derive(Debug)]
#[fully_pub]
struct UiMessage {
level: UiMessageLevel,
text: String,
created_at: u128, // unix epoch in millis
ttl: u128 // time to live in millis
}
impl UiMessage {
pub fn new(level: UiMessageLevel, text: &str) -> UiMessage {
let since_the_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
UiMessage {
level,
text: text.to_string(),
created_at: since_the_epoch.as_millis(),
ttl: 5000
}
}
pub fn with_ttl(level: UiMessageLevel, text: String, ttl: u128) -> UiMessage {
let mut msg = UiMessage::new(level, &text);
msg.ttl = ttl;
msg
}
pub fn success(text: &str) -> UiMessage {
UiMessage::new(UiMessageLevel::Success, text)
}
pub fn error(text: &str) -> UiMessage {
UiMessage::new(UiMessageLevel::Error, text)
}
fn is_dead(&self) -> bool {
let current_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis();
(current_epoch - self.created_at) > self.ttl
}
}
/// store all the app state (UI state)
#[derive(Debug)]
#[fully_pub]
@ -183,35 +76,6 @@ struct Config {
static_layers: Option<StaticLayers>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
struct OSMSession {
access_token: String,
scope: String,
token_type: String
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Session {
osm_session: Option<OSMSession>
}
// #[derive(Debug, Serialize, Deserialize)]
// struct Cache {
// osm_data: HashMap
// }
fn get_envelope_from_bounds(bounds: (Vector2, Vector2), margin_percent: f32) -> AABB<[f32; 2]> {
let center = bounds.1.lerp(bounds.0, 0.5);
let mut new_bounds = bounds;
new_bounds.0 = new_bounds.0 - (center - new_bounds.0).scale_by(margin_percent);
new_bounds.1 = new_bounds.1 + (new_bounds.1 - center).scale_by(margin_percent);
AABB::from_corners(
[new_bounds.0.x, new_bounds.0.y],
[new_bounds.1.x, new_bounds.1.y]
)
}
fn get_raster_tile(
raster_cache: &mut HashMap<(u32, u32, u32), Vec<u8>>,
http_agent: &Agent,
@ -258,16 +122,6 @@ fn get_raster_tile(
.map_err(|e| anyhow!("Cannot decode PNG, {:?}", e))
}
// fn get_osm_data(http_agent: &Agent, bounds: (Vector2, Vector2)) -> Result<OSMData> {
// /// download local data
// // curl -v https://api.openstreetmap.org/api/0.6/map.json -G -d "bbox=1.32495,49.16409,1.34585,49.18485" > map.json
// let response = http_agent.get("https://api.openstreetmap.org/api/0.6/map.json")
// .query("bbox", format!("{},{},{},{}", bounds.0.x, bounds.0.y, bounds.1.x, bounds.1.y))
// .call()
// .context("Requesting OSM local data")?;
// }
fn read_config(xdg_dirs: &BaseDirectories) -> Result<Config> {
let config_content =
fs::read_to_string(xdg_dirs.get_config_file("config.toml"))
@ -277,25 +131,6 @@ fn read_config(xdg_dirs: &BaseDirectories) -> Result<Config> {
Ok(config)
}
fn read_session(xdg_dirs: &BaseDirectories) -> Result<Session> {
// find session file
let config_content =
fs::read_to_string(xdg_dirs.get_data_file("session.json"))
.map_err(|_e| anyhow!("Could not read session JSON file"))?;
let session: Session = serde_json::from_str(&config_content)
.map_err(|_e| anyhow!("Could not deserialize session file"))?;
Ok(session)
}
fn write_session(xdg_dirs: &BaseDirectories, session: &Session) -> Result<()> {
let session_path = xdg_dirs.place_data_file("session.json").context("Could not create or find session file")?;
fs::write(
session_path,
serde_json::to_string(session).context("Could not serialize session")?
).context("Could not write session")?;
Ok(())
}
fn compute_bounds_from_center_zoom(screen_def: (i32, i32), target_center: Vector2, target_zoom: f32) -> (Vector2, Vector2)
{
let f = 0.00004;
@ -313,7 +148,9 @@ fn main() -> Result<()> {
let (mut rl, thread) = raylib::init()
.vsync()
.title(USER_AGENT)
.resizable() // disable floating window
.build();
rl.set_exit_key(None);
let xdg_dirs = xdg::BaseDirectories::with_prefix("bobosm")
.context("Locating XDG dirs")?;
@ -385,22 +222,6 @@ fn main() -> Result<()> {
let mut move_state = MoveState::Done;
// first point is left bottom, second point is top right point
// let mut current_bounds: (Vector2, Vector2) = ((-4.0, -4.0).into(), (4.0, 4.0).into());
// let mut current_bounds: (Vector2, Vector2) = ((-6., 37.0).into(), (12., 55.0).into());
// additional
// let points_repr: Vec<(f32, f32)> = vec![
// (0.0, 0.0), (1.0, 1.0), (0.5, 0.5), (-2.0, -2.0), (0.0, -1.0), (1.0, -0.5)
// ];
// for (i, repr) in points_repr.iter().enumerate() {
// tree.insert(LabeledPoint {
// importance: 1,
// name: format!("{}", i),
// pos: Vector2::new(repr.0, repr.1)
// });
// }
// load a geojson list of all bigs cities in france
// draw the cities points
@ -449,10 +270,7 @@ fn main() -> Result<()> {
let mut zoom_factor = 1.0 + -0.2*rl.get_mouse_wheel_move();
let mut zoom_focus = real_mouse_pos;
let is_left_shift_down = rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT);
let is_mouse_left_down = rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT);
let is_mouse_right_down = rl.is_mouse_button_down(MouseButton::MOUSE_BUTTON_RIGHT);
let is_mouse_right_released = rl.is_mouse_button_released(MouseButton::MOUSE_BUTTON_RIGHT);
// handle INPUT
let mut v = Vector2::zero();
@ -462,6 +280,10 @@ fn main() -> Result<()> {
v1 = 0.33;
v2 = 3.0;
}
if rl.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) {
v1 = 0.01;
v2 = 0.5;
}
if rl.is_key_down(KeyboardKey::KEY_EQUAL) {
zoom_focus = camera.center;
zoom_factor -= v2*0.01;
@ -477,7 +299,11 @@ fn main() -> Result<()> {
// changeset mode
// for now, generate Osm Change in local data dir
println!("try to create and push a changeset");
let _ = changeset::try_changeset(&xdg_dirs, layers.clone(), &osm_client);
let _ = ui::changeset::try_changeset(
&xdg_dirs,
layers.clone(),
&osm_client
);
}
if rl.is_key_pressed(KeyboardKey::KEY_E) {
// toggle edit mode
@ -488,16 +314,18 @@ fn main() -> Result<()> {
}
if ui_mode == UiMode::Edit && rl.is_key_pressed(KeyboardKey::KEY_T) {
// check if an object is selected
let _ = edit_tags(&xdg_dirs, app_state.clone(), layers.clone());
let _ = ui::edit_tags(&xdg_dirs, app_state.clone(), layers.clone());
}
if rl.is_key_pressed(KeyboardKey::KEY_D) {
download_zone_asked = true;
}
if rl.is_key_pressed(KeyboardKey::KEY_UP) || rl.is_key_pressed(KeyboardKey::KEY_K) {
// up navigation
let bounds_height = (camera.bounds.0.y - camera.bounds.1.y).abs();
v = Vector2::new(0.0, v1*bounds_height);
}
if rl.is_key_pressed(KeyboardKey::KEY_DOWN) || rl.is_key_pressed(KeyboardKey::KEY_J) {
// down navigation
let bounds_height = (camera.bounds.0.y - camera.bounds.1.y).abs();
v = Vector2::new(0.0, -v1*bounds_height);
}
@ -563,7 +391,7 @@ fn main() -> Result<()> {
println!("Selected {} new objects", object_count);
let mut state = app_state.lock().unwrap();
// if no object was selected, then end selection mode
if is_left_shift_down {
if rl.is_key_down(KeyboardKey::KEY_LEFT_SHIFT) {
// when using SHIFT, merge the new selected elements into the existings
// for each object in new selection
for (i,l) in intersection_objects.iter().enumerate() {
@ -657,7 +485,12 @@ fn main() -> Result<()> {
}
let bbox_center_canvas = bbox_center(way.bbox).to_canvas_pos(&camera);
// draw name in center of polygon
if !way.name.is_empty() {
if !way.name.is_empty() && (
(camera.zoom <= 0.9 && way.importance >= 10) ||
(camera.zoom >= 1.0 && way.importance >= 9) ||
camera.zoom >= 1.2
)
{
d.draw_text(
&way.name.to_string(),
bbox_center_canvas.x.ceil() as i32 - ((way.name.len()/2)*6) as i32,
@ -722,7 +555,7 @@ fn main() -> Result<()> {
{
d.draw_text(
&point.name.to_string(),
canvas_pos.x.ceil() as i32,
canvas_pos.x.ceil() as i32 + 8,
canvas_pos.y.ceil() as i32,
match point.importance {
10 => 17,

View file

@ -1,4 +1,4 @@
// all the functions to interact with the OSM API
// all the functions to interact with the OSM HTTP API
// https://wiki.openstreetmap.org/wiki/API_v0.6
use std::{time::Duration};
@ -9,7 +9,7 @@ use quick_xml::se::to_string_with_root;
use raylib::math::Vector2;
use ureq::{Agent, AgentBuilder};
use crate::{changeset::{ChangesetContent, ChangesetMeta}, data, OSMSession, Session};
use crate::{data::{self, changeset::{ChangesetContent, ChangesetMeta}, session::OSMSession}, Session};
#[derive(Debug, Clone)]
#[fully_pub]

92
src/ui/changeset.rs Normal file
View file

@ -0,0 +1,92 @@
use std::{collections::HashMap, sync::{Arc, Mutex}};
use anyhow::{Context, Result};
use xdg::BaseDirectories;
use crate::{data::changeset::{build_changes_content, ChangesetMeta, ChangesetMetaInner, Tag}, layers::{get_dynamic_data_layer, Layer}, osm_api::OSMApiClient, ui::text_editor::{open_external_text_editor, text_to_tags}, USER_AGENT};
/// Global UI method that manage changeset operation
/// Will handle all the operations to collect the changes and create a changeset and push it
pub fn try_changeset(
xdg_dirs: &BaseDirectories,
layers: Arc<Mutex<Vec<Layer>>>,
osm_client: &OSMApiClient
) -> Result<()> {
let layers_ref = layers.lock().unwrap();
let dynamic_layer = match get_dynamic_data_layer(&layers_ref) {
Some(v) => v,
None => {
return Ok(())
}
};
let preview_changes_content = build_changes_content(0, dynamic_layer);
if (
match &preview_changes_content.modify {
None => 0,
Some(modify) => modify.nodes.len()
} == 0
) {
println!("No changeset to be made, no changes detected");
// no changes to be made
return Ok(());
}
let mut changeset_text: String = "comment = \nsource = survey".to_string();
if let Some(node_changes) = &preview_changes_content.modify {
changeset_text.push_str(&format!("\n// {} node(s) changeds\n", &node_changes.nodes.len()));
for node in &node_changes.nodes {
changeset_text.push_str(&format!("// Node, id: {}, version: {}\n", node.id, node.version));
}
}
let new_text = open_external_text_editor(
xdg_dirs,
"changeset.txt",
&changeset_text
)?;
let user_tags: Vec<Tag> = text_to_tags(&new_text)
.context("Parsing changeset text tags")?
.unwrap_or(HashMap::new())
.iter().map(|(key, value)| {
Tag {
key: key.to_string(),
value: value.to_string()
}
}).collect();
let changeset_meta = ChangesetMeta {
osm: ChangesetMetaInner {
tags: [
user_tags,
vec![
Tag {
key: "created_by".to_string(),
value: USER_AGENT.to_string()
}
]
].concat()
}
};
println!("changeset_meta: {:?}", changeset_meta);
// get the changeset id
let changeset_id = osm_client.open_changeset(changeset_meta)?;
println!("changeset_id: {}", changeset_id);
let changeset_content = build_changes_content(changeset_id, dynamic_layer);
println!("changeset_content: {changeset_content:?}");
//TODO: store XML files in xdg dirs
println!("CHANGESET: Uploading changeset");
osm_client.upload_changes(changeset_id, changeset_content)
.context("Uploading changeset content")?;
println!("CHANGESET: Closing changeset");
osm_client.close_changeset(changeset_id)
.context("Closing changeset")?;
println!("CHANGESET: Changeset closed");
Ok(())
}

4
src/ui/inputs.rs Normal file
View file

@ -0,0 +1,4 @@
/// entry point to handle UI inputs
fn handle_input() {
}

View file

@ -1,7 +1,62 @@
use std::time::{SystemTime, UNIX_EPOCH};
use fully_pub::fully_pub;
use raylib::{color::Color, drawing::{RaylibDraw, RaylibDrawHandle}, math::Vector2};
use crate::{UiMessage, UiMessageLevel};
#[derive(Debug)]
#[fully_pub]
enum UiMessageLevel {
Success,
Info,
Warning,
Error
}
#[derive(Debug)]
#[fully_pub]
struct UiMessage {
level: UiMessageLevel,
text: String,
created_at: u128, // unix epoch in millis
ttl: u128 // time to live in millis
}
impl UiMessage {
pub fn new(level: UiMessageLevel, text: &str) -> UiMessage {
let since_the_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
UiMessage {
level,
text: text.to_string(),
created_at: since_the_epoch.as_millis(),
ttl: 5000
}
}
pub fn with_ttl(level: UiMessageLevel, text: String, ttl: u128) -> UiMessage {
let mut msg = UiMessage::new(level, &text);
msg.ttl = ttl;
msg
}
pub fn success(text: &str) -> UiMessage {
UiMessage::new(UiMessageLevel::Success, text)
}
pub fn error(text: &str) -> UiMessage {
UiMessage::new(UiMessageLevel::Error, text)
}
fn is_dead(&self) -> bool {
let current_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis();
(current_epoch - self.created_at) > self.ttl
}
}
pub fn render_messages(
messages: &Vec<UiMessage>,
@ -41,5 +96,4 @@ pub fn render_messages(
);
vertical_offset += 30;
}
}

View file

@ -1 +1,67 @@
pub mod messages;
pub mod changeset;
pub mod inputs;
pub mod selection;
pub mod text_editor;
pub mod renderer;
use std::{collections::HashSet, sync::{Arc, Mutex}};
use anyhow::{anyhow, Context, Result};
use text_editor::{open_external_text_editor, tags_to_text, text_to_tags};
use xdg::BaseDirectories;
use crate::{data::Tags, layers::Layer, AppState};
fn edit_tags_on_element(
xdg_dirs: &BaseDirectories,
modified_data_layer_ref_mut: &mut HashSet<i64>,
id: i64,
tags: &mut Tags,
) -> Result<()> {
let new_text = open_external_text_editor(
xdg_dirs,
"tags.txt",
&tags_to_text(tags)
)?;
let new_tags = text_to_tags(&new_text)
.context("Parsing text tags")?;
// mark element as modified
modified_data_layer_ref_mut.insert(id);
*tags = new_tags;
Ok(())
}
pub fn edit_tags(
xdg_dirs: &BaseDirectories,
app_state: Arc<Mutex<AppState>>,
layers: Arc<Mutex<Vec<Layer>>>
) -> Result<()> {
let app_state_ref = app_state.lock().unwrap();
let mut layers_ref = layers.lock().unwrap();
// find the first object selected and open editor
for (layer_i, layer_selections) in app_state_ref.selected_elements_per_layer.iter().enumerate() {
for e_id in layer_selections {
let layer_mut_ref = layers_ref.get_mut(layer_i)
.ok_or(anyhow!("Could not find layer"))?;
let object_element = layer_mut_ref
.data_source.as_mut()
.ok_or(anyhow!("Could not access layer data source"))?
.elements
.get_mut(e_id)
.ok_or(anyhow!("Could not access selected object source"))?;
match object_element {
crate::data::Element::Node(node) => {
edit_tags_on_element(xdg_dirs, &mut layer_mut_ref.modified_data, node.id, &mut node.tags)?;
},
crate::data::Element::Way(way) => {
edit_tags_on_element(xdg_dirs, &mut layer_mut_ref.modified_data, way.id, &mut way.tags)?;
},
_ => ()
};
return Ok(());
}
}
Err(anyhow!("No object founds"))
}

4
src/ui/renderer.rs Normal file
View file

@ -0,0 +1,4 @@
/// takes the pre-processed data to be rendered and turn it into vectors instructions sent to raylib
fn map_render() {
}

34
src/ui/selection.rs Normal file
View file

@ -0,0 +1,34 @@
use std::sync::{Arc, Mutex};
use rstar::AABB;
use crate::layers::Layer;
/// return for each layers, the objects id in a given area
pub fn get_objects_in_area(
layers: Arc<Mutex<Vec<Layer>>>,
selection_envelope: AABB<[f32; 2]>
) -> Vec<Vec<i64>> {
let mut elements_ids_per_layer: Vec<Vec<i64>> = vec![];
for layer in layers.lock().unwrap().iter() {
let mut elements_ids: Vec<i64> = vec![];
if let Some(tree) = &layer.ways_tree {
// find a better way to verify if it's intersecting
// the better way:
// we first get the objects in boundin box + margin
// for each way
// for each line in way
// verify if the point is close to the line
for way in tree.locate_in_envelope_intersecting(&selection_envelope) {
// elements_ids.push(way.id);
}
}
if let Some(tree) = &layer.pois_tree {
for point in tree.locate_in_envelope(&selection_envelope) {
elements_ids.push(point.id);
}
}
elements_ids_per_layer.push(elements_ids);
}
elements_ids_per_layer
}

69
src/ui/text_editor.rs Normal file
View file

@ -0,0 +1,69 @@
use std::{collections::HashMap, fs, process::Command};
use crate::data::Tags;
use anyhow::{Context, Result, anyhow};
use xdg::BaseDirectories;
pub fn tags_to_text(tags: &Tags) -> String {
let tags_v = match tags {
None => return "".to_string(),
Some(v) => v
};
let mut out = String::new();
for (key, value) in tags_v.iter() {
out += &format!("{} = {}\n", &key, &value);
}
out
}
pub fn text_to_tags(tags_as_text: &str) -> Result<Tags> {
let mut tags: HashMap<String, String> = HashMap::new();
for line in tags_as_text.trim().split('\n') {
let line_content = line.to_string();
if line_content.is_empty() || line_content.starts_with("//") {
continue;
}
let inline_comment_compos: Vec<&str> = line_content.split(" //").collect();
let line_true_content = inline_comment_compos
.iter().next()
.expect("Expected at least one component");
let compos: Vec<&str> = line_true_content.split('=').collect();
if compos.len() != 2 {
return Err(anyhow!("Found invalid tags syntax"));
}
let key = compos.first().unwrap().trim();
let val = compos.get(1).unwrap().trim();
tags.insert(key.to_string(), val.to_string());
}
Ok(Some(tags))
}
pub fn open_external_text_editor(xdg_dirs: &BaseDirectories, file_name: &str, source_text: &str) -> Result<String> {
// write tmp file
let file_path = xdg_dirs.place_state_file(file_name)?;
fs::write(
&file_path,
source_text
).context("Writing temp text to file")?;
// open cmd
// TODO: make the editor configurable
let _ = Command::new("alacritty")
.arg("--command")
.arg("helix")
.arg("--config")
.arg("/home/mbess/.config/helix/config.toml")
.arg(&file_path)
.spawn()
.context("Failed to start external editor")?
.wait();
// wait for editor to exit
let new_text_content =
fs::read_to_string(&file_path)
.context("Reading temp text file")?;
// println!("new_text_content: {:?}", new_text_content);
Ok(new_text_content)
}

View file

@ -1,4 +1,7 @@
use raylib::consts;
use raylib::{consts, math::Vector2};
use rstar::AABB;
use crate::Camera;
pub fn deg2num(lat_deg: f64, lon_deg: f64, zoom: u32) -> (u32, u32) {
// Web mercator projection
@ -9,3 +12,66 @@ pub fn deg2num(lat_deg: f64, lon_deg: f64, zoom: u32) -> (u32, u32) {
(xtile, ytile)
}
pub fn get_envelope_from_bounds(bounds: (Vector2, Vector2), margin_percent: f32) -> AABB<[f32; 2]> {
let center = bounds.1.lerp(bounds.0, 0.5);
let mut new_bounds = bounds;
new_bounds.0 = new_bounds.0 - (center - new_bounds.0).scale_by(margin_percent);
new_bounds.1 = new_bounds.1 + (new_bounds.1 - center).scale_by(margin_percent);
AABB::from_corners(
[new_bounds.0.x, new_bounds.0.y],
[new_bounds.1.x, new_bounds.1.y]
)
}
pub trait Invertion {
fn invert_for_canvas(&self) -> Vector2;
}
impl Invertion for Vector2 {
fn invert_for_canvas(&self) -> Vector2 {
Vector2::new(self.x, -self.y)
}
}
pub trait CanvasPos {
fn to_canvas_pos(&self, camera: &Camera) -> Vector2;
}
impl CanvasPos for Vector2 {
fn to_canvas_pos(&self, camera: &Camera) -> Vector2 {
Vector2::new(
self.x - camera.bounds.0.x,
camera.bounds.1.y - self.y
).scale_by(camera.real_to_canvas_factor)
}
}
pub trait ToSlice {
fn to_slice(&self) -> [f32; 2];
}
impl ToSlice for Vector2 {
fn to_slice(&self) -> [f32; 2] {
[self.x, self.y]
}
}
pub trait ToAABB {
fn to_aabb(&self) -> AABB<[f32; 2]>;
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]>;
}
impl ToAABB for Vector2 {
fn to_aabb(&self) -> AABB<[f32; 2]> {
AABB::from_point([self.x, self.y])
}
fn to_aabb_with_margin(&self, margin: f32) -> AABB<[f32; 2]> {
AABB::from_corners(
[self.x - margin, self.y - margin],
[self.x + margin, self.y + margin]
)
}
}