osm generate code fix
This commit is contained in:
@@ -3711,57 +3711,98 @@ pub struct BboxOut {
|
|||||||
const OSM_HIGHWAY_CLASSES: &str =
|
const OSM_HIGHWAY_CLASSES: &str =
|
||||||
"motorway|trunk|primary|secondary|tertiary|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link|residential|unclassified|service";
|
"motorway|trunk|primary|secondary|tertiary|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link|residential|unclassified|service";
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct GenerateOsmResult {
|
||||||
|
pub ways_kept: usize,
|
||||||
|
pub source: String,
|
||||||
|
pub points_used: usize,
|
||||||
|
pub videos_used: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn generate_osm_near_assets(
|
pub async fn generate_osm_near_assets(
|
||||||
radius_m: Option<f64>,
|
min_lat: f64,
|
||||||
max_samples: Option<usize>,
|
min_lng: f64,
|
||||||
|
max_lat: f64,
|
||||||
|
max_lng: f64,
|
||||||
|
keep_threshold_m: Option<f64>,
|
||||||
path: String,
|
path: String,
|
||||||
overpass_url: Option<String>,
|
overpass_url: Option<String>,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<usize, String> {
|
) -> Result<GenerateOsmResult, String> {
|
||||||
// Tighter defaults — only roads the assets are *on*, not nearby:
|
// New flow: Overpass query is the visible bbox (gives every way in view,
|
||||||
// radius_m = 60 m (capture buffer for GPS drift + side offset)
|
// including highways that earlier around: + min_votes=2 dropped). The
|
||||||
// keep_threshold = 30 m (post-filter: an asset must be within 30 m of
|
// returned ways are then filtered against a unified reference set built
|
||||||
// the road centerline to count as "on" the road)
|
// from per-video metadata polyline points + live asset coords. A way is
|
||||||
// min_votes = 2 (a road must collect at least this many asset
|
// kept if any of its segments comes within `keep_threshold_m` of any
|
||||||
// hits to be kept; one stray asset isn't enough)
|
// reference point. This covers highways with sparse assets via the
|
||||||
let radius = radius_m.unwrap_or(60.0);
|
// metadata polyline (the GPS track always touches the highway it drove)
|
||||||
let keep_threshold_m: f64 = 30.0;
|
// and urban side streets via the dense asset cloud.
|
||||||
let min_votes: usize = 2;
|
let threshold_m = keep_threshold_m.unwrap_or(50.0).max(1.0);
|
||||||
let cap = max_samples.unwrap_or(800).max(1);
|
if !(min_lat < max_lat) || !(min_lng < max_lng) {
|
||||||
// Pull all live asset centres. The point bbox columns are populated at
|
return Err("invalid bbox: min must be < max for both lat and lng".into());
|
||||||
// import time so the SELECT is cheap.
|
}
|
||||||
let coords: Vec<(f64, f64)> = sqlx::query_as::<_, (f64, f64)>(
|
|
||||||
"SELECT lat, lng FROM assets
|
// --- Build the reference-point set (lng, lat). ---
|
||||||
|
// Pull asset centres for the post-filter votes.
|
||||||
|
let asset_coords: Vec<(f64, f64)> = sqlx::query_as::<_, (f64, f64)>(
|
||||||
|
"SELECT lng, lat FROM assets
|
||||||
WHERE deleted = 0 AND lat IS NOT NULL AND lng IS NOT NULL",
|
WHERE deleted = 0 AND lat IS NOT NULL AND lng IS NOT NULL",
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
if coords.is_empty() {
|
// Pull every metadata polyline point (DB-persisted, stored as
|
||||||
return Err("no live assets in DB to generate roads near".to_string());
|
// [[lng, lat], ...] to match deck.gl convention).
|
||||||
|
let metadata_rows: Vec<(String,)> = sqlx::query_as(
|
||||||
|
"SELECT track_json FROM video_metadata",
|
||||||
|
)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let mut ref_points: Vec<(f64, f64)> = Vec::new();
|
||||||
|
let mut videos_used = 0usize;
|
||||||
|
for (json,) in &metadata_rows {
|
||||||
|
let track: Vec<[f64; 2]> = match serde_json::from_str(json) {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if track.is_empty() {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Stride-sample for the around: query body. We use the *full* asset
|
videos_used += 1;
|
||||||
// list further down for vote counting, so the sample is purely a
|
for p in track {
|
||||||
// query-size optimisation.
|
ref_points.push((p[0], p[1]));
|
||||||
let stride = (coords.len().div_ceil(cap)).max(1);
|
|
||||||
let sampled: Vec<&(f64, f64)> = coords.iter().step_by(stride).collect();
|
|
||||||
// Build a `(lat,lng,lat,lng,...)` list. Overpass `around:r,...` returns
|
|
||||||
// ways with any node within `r` metres of any of the given points.
|
|
||||||
let mut latlng = String::with_capacity(sampled.len() * 22);
|
|
||||||
for (i, (lat, lng)) in sampled.iter().enumerate() {
|
|
||||||
if i > 0 {
|
|
||||||
latlng.push(',');
|
|
||||||
}
|
}
|
||||||
latlng.push_str(&format!("{},{}", lat, lng));
|
|
||||||
}
|
}
|
||||||
|
for c in &asset_coords {
|
||||||
|
ref_points.push(*c);
|
||||||
|
}
|
||||||
|
if ref_points.is_empty() {
|
||||||
|
return Err(
|
||||||
|
"nothing to filter against. Import per-video metadata or assets first \
|
||||||
|
so the generator knows which Overpass-returned roads to keep."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let source = if videos_used > 0 && !asset_coords.is_empty() {
|
||||||
|
"metadata+assets"
|
||||||
|
} else if videos_used > 0 {
|
||||||
|
"metadata"
|
||||||
|
} else {
|
||||||
|
"assets"
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Overpass: bbox query, no around: list. ---
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"[out:json][timeout:180];\
|
"[out:json][timeout:180];\
|
||||||
(way[\"highway\"~\"{classes}\"](around:{r},{pts}););\
|
(way[\"highway\"~\"{classes}\"]({mn_lat},{mn_lng},{mx_lat},{mx_lng}););\
|
||||||
out body geom;",
|
out body geom;",
|
||||||
classes = OSM_HIGHWAY_CLASSES,
|
classes = OSM_HIGHWAY_CLASSES,
|
||||||
r = radius as i64,
|
mn_lat = min_lat,
|
||||||
pts = latlng,
|
mn_lng = min_lng,
|
||||||
|
mx_lat = max_lat,
|
||||||
|
mx_lng = max_lng,
|
||||||
);
|
);
|
||||||
let url = overpass_url
|
let url = overpass_url
|
||||||
.unwrap_or_else(|| "https://overpass-api.de/api/interpreter".to_string());
|
.unwrap_or_else(|| "https://overpass-api.de/api/interpreter".to_string());
|
||||||
@@ -3843,16 +3884,14 @@ pub async fn generate_osm_near_assets(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post-filter: keep only roads where >= min_votes assets sit within
|
// Post-filter: keep a way if any of its segments comes within
|
||||||
// keep_threshold_m of the centerline. The around: query alone returns
|
// `threshold_m` of any reference point (metadata polyline points or
|
||||||
// every road within `radius` m of any asset — including parallel/cross
|
// asset coords). Permissive — needs only ONE hit, vs the old
|
||||||
// streets that just happen to be close. The vote step picks out the
|
// min_votes=2 which dropped sparse-asset highways.
|
||||||
// road(s) the assets actually follow.
|
let kept = filter_features_by_reference_points(
|
||||||
let kept = filter_features_by_asset_votes(
|
|
||||||
features,
|
features,
|
||||||
&coords,
|
&ref_points,
|
||||||
keep_threshold_m,
|
threshold_m,
|
||||||
min_votes,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let fc = serde_json::json!({
|
let fc = serde_json::json!({
|
||||||
@@ -3863,7 +3902,14 @@ pub async fn generate_osm_near_assets(
|
|||||||
tokio::fs::write(&path, serde_json::to_string_pretty(&fc).map_err(|e| e.to_string())?)
|
tokio::fs::write(&path, serde_json::to_string_pretty(&fc).map_err(|e| e.to_string())?)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
Ok(count)
|
Ok(GenerateOsmResult {
|
||||||
|
ways_kept: count,
|
||||||
|
source: source.to_string(),
|
||||||
|
// Reuse points_used to surface the size of the reference set the
|
||||||
|
// post-filter checked against (asset coords + every metadata vertex).
|
||||||
|
points_used: ref_points.len(),
|
||||||
|
videos_used,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn kept_count(fc: &serde_json::Value) -> usize {
|
fn kept_count(fc: &serde_json::Value) -> usize {
|
||||||
@@ -3905,12 +3951,28 @@ fn point_segment_distance_m(
|
|||||||
(ex * ex + ey * ey).sqrt()
|
(ex * ex + ey * ey).sqrt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filter_features_by_asset_votes(
|
/// Permissive filter: keep a way if any of its segments comes within
|
||||||
|
/// `threshold_m` of any reference point. Reference points are passed as
|
||||||
|
/// `(lng, lat)`. Single hit is enough — designed for the bbox+filter flow
|
||||||
|
/// where we want every road touched by either metadata polylines or assets,
|
||||||
|
/// including highways with sparse asset coverage.
|
||||||
|
///
|
||||||
|
/// Builds a spatial-hash grid keyed by `cell_deg = threshold_m / 111320`,
|
||||||
|
/// then for each way walks its bbox cell + a 1-ring neighbourhood; at most
|
||||||
|
/// one exact distance check per nearby reference point. ~O(W × P_local).
|
||||||
|
fn filter_features_by_reference_points(
|
||||||
features: Vec<serde_json::Value>,
|
features: Vec<serde_json::Value>,
|
||||||
asset_coords: &[(f64, f64)],
|
ref_points: &[(f64, f64)],
|
||||||
threshold_m: f64,
|
threshold_m: f64,
|
||||||
min_votes: usize,
|
|
||||||
) -> Vec<serde_json::Value> {
|
) -> Vec<serde_json::Value> {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
let cell_deg = (threshold_m / 111_320.0).max(1e-7);
|
||||||
|
let mut grid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
|
||||||
|
for (i, (lng, lat)) in ref_points.iter().enumerate() {
|
||||||
|
let gx = (lng / cell_deg).floor() as i64;
|
||||||
|
let gy = (lat / cell_deg).floor() as i64;
|
||||||
|
grid.entry((gx, gy)).or_default().push(i);
|
||||||
|
}
|
||||||
let mut out: Vec<serde_json::Value> = Vec::new();
|
let mut out: Vec<serde_json::Value> = Vec::new();
|
||||||
for feat in features {
|
for feat in features {
|
||||||
let pts: Vec<[f64; 2]> = feat
|
let pts: Vec<[f64; 2]> = feat
|
||||||
@@ -3931,53 +3993,46 @@ fn filter_features_by_asset_votes(
|
|||||||
if pts.len() < 2 {
|
if pts.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Road bbox + buffer in degrees (so we can skip far-away assets cheaply).
|
let mut keep = false;
|
||||||
let mut mn_lng = f64::INFINITY;
|
// Walk segments at sub-cell intervals so a long segment crossing a
|
||||||
let mut mx_lng = f64::NEG_INFINITY;
|
// populated cell can't slip through the grid.
|
||||||
let mut mn_lat = f64::INFINITY;
|
let step_deg = cell_deg * 0.5;
|
||||||
let mut mx_lat = f64::NEG_INFINITY;
|
'segs: for w in pts.windows(2) {
|
||||||
for &[lng, lat] in &pts {
|
let a = w[0];
|
||||||
if lng < mn_lng { mn_lng = lng; }
|
let b = w[1];
|
||||||
if lng > mx_lng { mx_lng = lng; }
|
let dx = b[0] - a[0];
|
||||||
if lat < mn_lat { mn_lat = lat; }
|
let dy = b[1] - a[1];
|
||||||
if lat > mx_lat { mx_lat = lat; }
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
}
|
let steps = ((len / step_deg).ceil() as usize).max(1);
|
||||||
let lat0 = ((mn_lat + mx_lat) * 0.5).to_radians();
|
for s in 0..=steps {
|
||||||
|
let t = s as f64 / steps as f64;
|
||||||
|
let lng = a[0] + dx * t;
|
||||||
|
let lat = a[1] + dy * t;
|
||||||
|
let gx = (lng / cell_deg).floor() as i64;
|
||||||
|
let gy = (lat / cell_deg).floor() as i64;
|
||||||
|
for dy_c in -1..=1 {
|
||||||
|
for dx_c in -1..=1 {
|
||||||
|
if let Some(idxs) = grid.get(&(gx + dx_c, gy + dy_c)) {
|
||||||
|
for &idx in idxs {
|
||||||
|
let p = ref_points[idx];
|
||||||
|
// Cheap equirectangular distance — accurate
|
||||||
|
// enough for 50 m thresholding.
|
||||||
|
let lat0 = ((lat + p.1) * 0.5).to_radians();
|
||||||
let m_per_lng = (111_320.0_f64 * lat0.cos()).max(1.0);
|
let m_per_lng = (111_320.0_f64 * lat0.cos()).max(1.0);
|
||||||
let buf_lat = threshold_m / 111_320.0;
|
let mx = (p.0 - lng) * m_per_lng;
|
||||||
let buf_lng = threshold_m / m_per_lng;
|
let my = (p.1 - lat) * 111_320.0;
|
||||||
|
let d = (mx * mx + my * my).sqrt();
|
||||||
let mut votes: usize = 0;
|
if d <= threshold_m {
|
||||||
for &(a_lat, a_lng) in asset_coords {
|
keep = true;
|
||||||
if a_lng < mn_lng - buf_lng
|
break 'segs;
|
||||||
|| a_lng > mx_lng + buf_lng
|
|
||||||
|| a_lat < mn_lat - buf_lat
|
|
||||||
|| a_lat > mx_lat + buf_lat
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut hit = false;
|
|
||||||
for w in pts.windows(2) {
|
|
||||||
let d = point_segment_distance_m(
|
|
||||||
a_lng, a_lat,
|
|
||||||
w[0][0], w[0][1],
|
|
||||||
w[1][0], w[1][1],
|
|
||||||
);
|
|
||||||
if d < threshold_m {
|
|
||||||
hit = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hit {
|
|
||||||
votes += 1;
|
|
||||||
if votes >= min_votes {
|
|
||||||
// Early exit — we already have enough votes; no need to
|
|
||||||
// count exactly how many more.
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if votes >= min_votes {
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keep {
|
||||||
out.push(feat);
|
out.push(feat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/App.tsx
96
src/App.tsx
@@ -207,7 +207,14 @@ function AppShell({
|
|||||||
range_assets: number;
|
range_assets: number;
|
||||||
osm_roads: number;
|
osm_roads: number;
|
||||||
scope_features: number;
|
scope_features: number;
|
||||||
}>({ fixed_assets: 0, range_assets: 0, osm_roads: 0, scope_features: 0 });
|
video_metadata: number;
|
||||||
|
}>({
|
||||||
|
fixed_assets: 0,
|
||||||
|
range_assets: 0,
|
||||||
|
osm_roads: 0,
|
||||||
|
scope_features: 0,
|
||||||
|
video_metadata: 0,
|
||||||
|
});
|
||||||
const [recentActions, setRecentActions] = useState<ActionRow[]>([]);
|
const [recentActions, setRecentActions] = useState<ActionRow[]>([]);
|
||||||
const [status, setStatus] = useState<string>("");
|
const [status, setStatus] = useState<string>("");
|
||||||
const [busy, setBusy] = useState<boolean>(false);
|
const [busy, setBusy] = useState<boolean>(false);
|
||||||
@@ -1580,6 +1587,7 @@ function AppShell({
|
|||||||
range_assets: number;
|
range_assets: number;
|
||||||
osm_roads: number;
|
osm_roads: number;
|
||||||
scope_features: number;
|
scope_features: number;
|
||||||
|
video_metadata: number;
|
||||||
}>("data_source_counts");
|
}>("data_source_counts");
|
||||||
setDataCounts(c);
|
setDataCounts(c);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1830,46 +1838,82 @@ function AppShell({
|
|||||||
folder.endsWith("/") || folder.endsWith("\\") ? "" : "/";
|
folder.endsWith("/") || folder.endsWith("\\") ? "" : "/";
|
||||||
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
||||||
const hasAssets = dataCounts.fixed_assets + dataCounts.range_assets > 0;
|
const hasAssets = dataCounts.fixed_assets + dataCounts.range_assets > 0;
|
||||||
|
const hasMetadata = (dataCounts.video_metadata ?? 0) > 0
|
||||||
|
|| Object.keys(metadataByVideo).length > 0;
|
||||||
|
if (!hasAssets && !hasMetadata) {
|
||||||
|
setStatus(
|
||||||
|
"Import per-video metadata or assets first — the generator filters Overpass results against them.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// We need a bbox to query Overpass against. Prefer the current visible
|
||||||
|
// viewport (gives the user direct control over scope); fall back to the
|
||||||
|
// bbox of all loaded assets if the viewport isn't valid yet (e.g. user
|
||||||
|
// never panned the map). Either way the bbox is what Overpass filters
|
||||||
|
// on, and the post-filter then narrows by metadata + asset proximity.
|
||||||
|
let b = boundsRef.current;
|
||||||
|
if (!b) {
|
||||||
|
try {
|
||||||
|
const ab = await invoke<{
|
||||||
|
min_lat: number;
|
||||||
|
min_lng: number;
|
||||||
|
max_lat: number;
|
||||||
|
max_lng: number;
|
||||||
|
} | null>("assets_bbox");
|
||||||
|
if (ab) {
|
||||||
|
b = {
|
||||||
|
south: ab.min_lat,
|
||||||
|
north: ab.max_lat,
|
||||||
|
west: ab.min_lng,
|
||||||
|
east: ab.max_lng,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* fall through to error below */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!b) {
|
||||||
|
setStatus(
|
||||||
|
"No visible map bbox and no asset bbox available — pan/zoom the map or import assets first.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setOsmFetchActive(true);
|
setOsmFetchActive(true);
|
||||||
try {
|
try {
|
||||||
if (hasAssets) {
|
const sourceHint = hasMetadata && hasAssets
|
||||||
// Tight per-asset filter: fetch a small candidate set with
|
? "metadata + assets"
|
||||||
// around:60m, then keep only roads where ≥2 assets sit within 30 m
|
: hasMetadata
|
||||||
// of the centerline. That way the output is the road(s) the
|
? "metadata"
|
||||||
// assets are actually on, not every street that happens to be
|
: "assets";
|
||||||
// within bbox or radius.
|
|
||||||
setStatus("Fetching OSM near assets from Overpass…");
|
|
||||||
const path = `${folder}${sep}osm_on_asset_roads_${ts}.geojson`;
|
|
||||||
const n = await invoke<number>("generate_osm_near_assets", {
|
|
||||||
radiusM: 60,
|
|
||||||
maxSamples: 800,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
setStatus(
|
setStatus(
|
||||||
`Generated ${n} road(s) carrying assets → ${path}. Use Import → OSM roads to load.`,
|
`Fetching OSM for visible bbox from Overpass, then filtering by ${sourceHint}…`,
|
||||||
);
|
);
|
||||||
} else if (boundsRef.current) {
|
const path = `${folder}${sep}osm_on_asset_roads_${ts}.geojson`;
|
||||||
const b = boundsRef.current;
|
const r = await invoke<{
|
||||||
const path = `${folder}${sep}osm_${b.south.toFixed(4)}_${b.west.toFixed(4)}_${b.north.toFixed(4)}_${b.east.toFixed(4)}_${ts}.geojson`;
|
ways_kept: number;
|
||||||
setStatus("Fetching OSM for visible viewport from Overpass…");
|
source: string;
|
||||||
const n = await invoke<number>("generate_osm_for_bbox", {
|
points_used: number;
|
||||||
|
videos_used: number;
|
||||||
|
}>("generate_osm_near_assets", {
|
||||||
minLat: b.south,
|
minLat: b.south,
|
||||||
minLng: b.west,
|
minLng: b.west,
|
||||||
maxLat: b.north,
|
maxLat: b.north,
|
||||||
maxLng: b.east,
|
maxLng: b.east,
|
||||||
|
keepThresholdM: 50,
|
||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
|
const refDesc =
|
||||||
|
r.videos_used > 0
|
||||||
|
? `${r.points_used} ref points (${r.videos_used} video track(s) + assets)`
|
||||||
|
: `${r.points_used} asset ref points`;
|
||||||
setStatus(
|
setStatus(
|
||||||
`Generated ${n} OSM way(s) from viewport → ${path}. Use Import → OSM roads to load.`,
|
`Generated ${r.ways_kept} road(s) [bbox + ${r.source}, ${refDesc}] → ${path}. Use Import → OSM roads to load.`,
|
||||||
);
|
);
|
||||||
} else {
|
return;
|
||||||
setStatus(
|
|
||||||
"No assets imported and no map viewport set. Import assets or pan/zoom the map first.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(`Generate OSM failed: ${e}`);
|
setStatus(`Generate OSM failed: ${e}`);
|
||||||
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setOsmFetchActive(false);
|
setOsmFetchActive(false);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user