503 lines
17 KiB
C#
503 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using UnityEngine;
|
|
using UnityEngine.Networking;
|
|
using UnityEngine.UIElements;
|
|
|
|
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
|
|
public class Map : MonoBehaviour, IMapDataCollector
|
|
{
|
|
[Header("Overpass settings")]
|
|
public string overpassUrl = "https://mapz.honzuvkod.dev/api/interpreter";
|
|
public float queryRadiusMeters = 200f; // radius around lat/lon to query
|
|
|
|
[Header("Location (lat, lon)")]
|
|
public double latitude = 50.7727878f; // example Prague
|
|
public double longitude = 15.0768714f;
|
|
|
|
[Header("Building settings")]
|
|
public Material buildingMaterial;
|
|
public float defaultFloorHeight = 3.0f; // meters per level
|
|
public float defaultBuildingHeight = 6.0f; // if no tags
|
|
|
|
[Header("Road settings")]
|
|
public Material roadMaterial;
|
|
public float defaultRoadWidth = 4.0f; // meters
|
|
|
|
[Header("Misc")]
|
|
public float metersPerUnit = 1f; // scale: 1 unit = 1 meter
|
|
public bool autoStart = true;
|
|
class Way
|
|
{
|
|
public long id;
|
|
public List<long> nodeRefs = new List<long>();
|
|
public Dictionary<string, string> tags = new Dictionary<string, string>();
|
|
}
|
|
|
|
private static readonly HttpClient client = new HttpClient();
|
|
|
|
List<Way> parsedWays = new List<Way>();
|
|
Dictionary<long, Vector2> nodes = new Dictionary<long, Vector2>(); // id -> latlon
|
|
|
|
void Start()
|
|
{
|
|
CallApi();
|
|
}
|
|
|
|
void ClearChildren()
|
|
{
|
|
// remove existing generated children
|
|
List<GameObject> toDestroy = new List<GameObject>();
|
|
foreach (Transform t in transform)
|
|
toDestroy.Add(t.gameObject);
|
|
foreach (var g in toDestroy)
|
|
DestroyImmediate(g);
|
|
}
|
|
|
|
public async Task<string> CallOverpassApi(FormUrlEncodedContent query)
|
|
{
|
|
Debug.Log("Calling Overpass API...");
|
|
|
|
|
|
var response = await client.PostAsync(overpassUrl, query);
|
|
Debug.Log("Received response from Overpass API.");
|
|
return await response.Content.ReadAsStringAsync();
|
|
}
|
|
|
|
public FormUrlEncodedContent QueryBuilder(double[] GPS)
|
|
{
|
|
Debug.Log("Building Overpass API query...");
|
|
string query = @$"
|
|
[out:xml]
|
|
[timeout:90];
|
|
(
|
|
way(around:{queryRadiusMeters}, {GPS[0].ToString().Replace(",", ".")}, {GPS[1].ToString().Replace(",", ".")});
|
|
);
|
|
out geom;";
|
|
Dictionary<string, string> values = new Dictionary<string, string>()
|
|
{
|
|
{"data", query}
|
|
};
|
|
Debug.Log("Overpass API query built.");
|
|
|
|
return new FormUrlEncodedContent(values);
|
|
}
|
|
|
|
public async void CallApi()
|
|
{
|
|
Debug.Log("Testing Overpass API...");
|
|
double[] GPS = new double[] {latitude, longitude};
|
|
string result = await CallOverpassApi(QueryBuilder(GPS));
|
|
Debug.Log("Overpass API response received:");
|
|
ParseOverpassXml(result);
|
|
|
|
GameObject buildingsRoot = new GameObject("Buildings");
|
|
buildingsRoot.transform.parent = this.transform;
|
|
GameObject roadsRoot = new GameObject("Roads");
|
|
roadsRoot.transform.parent = this.transform;
|
|
foreach (var w in parsedWays)
|
|
{
|
|
if (w.tags.ContainsKey("building"))
|
|
{
|
|
//Debug.Log("Building");
|
|
GameObject b = BuildBuildingMesh(w);
|
|
b.transform.parent = buildingsRoot.transform;
|
|
}
|
|
else if (w.tags.ContainsKey("highway"))
|
|
{
|
|
//Debug.Log("Highway");
|
|
GameObject r = BuildRoadMesh(w);
|
|
r.transform.parent = roadsRoot.transform;
|
|
}
|
|
}
|
|
|
|
}
|
|
void ParseOverpassXml(string xmlText)
|
|
{
|
|
nodes.Clear();
|
|
parsedWays.Clear();
|
|
|
|
XmlDocument doc = new XmlDocument();
|
|
doc.LoadXml(xmlText);
|
|
|
|
XmlNode osm = doc.SelectSingleNode("/osm");
|
|
if (osm == null) return;
|
|
|
|
// parse nodes
|
|
foreach (XmlNode node in osm.SelectNodes("node"))
|
|
{
|
|
long id = long.Parse(node.Attributes["id"].Value, CultureInfo.InvariantCulture);
|
|
double lat = double.Parse(node.Attributes["lat"].Value, CultureInfo.InvariantCulture);
|
|
double lon = double.Parse(node.Attributes["lon"].Value, CultureInfo.InvariantCulture);
|
|
nodes[id] = new Vector2((float)lat, (float)lon);
|
|
}
|
|
|
|
// parse ways
|
|
foreach (XmlNode wayNode in osm.SelectNodes("way"))
|
|
{
|
|
Way w = new Way();
|
|
w.id = long.Parse(wayNode.Attributes["id"].Value, CultureInfo.InvariantCulture);
|
|
foreach (XmlNode child in wayNode.ChildNodes)
|
|
{
|
|
if (child.Name == "nd")
|
|
{
|
|
long r = long.Parse(child.Attributes["ref"].Value, CultureInfo.InvariantCulture);
|
|
w.nodeRefs.Add(r);
|
|
}
|
|
else if (child.Name == "tag")
|
|
{
|
|
string k = child.Attributes["k"].Value;
|
|
string v = child.Attributes["v"].Value;
|
|
w.tags[k] = v;
|
|
}
|
|
}
|
|
parsedWays.Add(w);
|
|
}
|
|
}
|
|
#region Utilities: latlon to local meters
|
|
// Convert latitude/longitude to local XY meters relative to center point
|
|
Vector3 LatLonToLocal(double lat, double lon)
|
|
{
|
|
// Use simple equirectangular projection around center (latitude, longitude)
|
|
double lat0 = latitude;
|
|
double lon0 = longitude;
|
|
double dLat = (lat - lat0) * Mathf.Deg2Rad;
|
|
double dLon = (lon - lon0) * Mathf.Deg2Rad;
|
|
double R = 6378137.0; // Earth radius in meters
|
|
double x = R * dLon * Math.Cos(lat0 * Mathf.Deg2Rad);
|
|
double y = R * dLat;
|
|
return new Vector3((float)x / metersPerUnit, 0f, (float)y / metersPerUnit);
|
|
}
|
|
Vector3 NodeIdToLocal(long nodeId)
|
|
{
|
|
if (!nodes.ContainsKey(nodeId))
|
|
return Vector3.zero;
|
|
Vector2 latlon = nodes[nodeId];
|
|
return LatLonToLocal(latlon.x, latlon.y);
|
|
}
|
|
#endregion
|
|
|
|
#region Mesh builders
|
|
GameObject BuildBuildingMesh(Way w)
|
|
{
|
|
// gather polygon points
|
|
List<Vector3> poly = new List<Vector3>();
|
|
foreach (var id in w.nodeRefs)
|
|
{
|
|
Vector3 p = NodeIdToLocal(id);
|
|
poly.Add(p);
|
|
}
|
|
|
|
// ensure closed
|
|
if (poly.Count < 3) return null;
|
|
if ((poly[0] - poly[poly.Count - 1]).sqrMagnitude > 0.0001f)
|
|
poly.Add(poly[0]);
|
|
|
|
// determine height
|
|
float height = defaultBuildingHeight;
|
|
if (w.tags.ContainsKey("height"))
|
|
{
|
|
if (TryParseHeight(w.tags["height"], out float h)) height = h;
|
|
}
|
|
else if (w.tags.ContainsKey("building:levels"))
|
|
{
|
|
if (float.TryParse(w.tags["building:levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
|
|
height = Mathf.Max(0.5f, levels * defaultFloorHeight);
|
|
}
|
|
else if (w.tags.ContainsKey("levels"))
|
|
{
|
|
if (float.TryParse(w.tags["levels"], NumberStyles.Float, CultureInfo.InvariantCulture, out float levels))
|
|
height = Mathf.Max(0.5f, levels * defaultFloorHeight);
|
|
}
|
|
|
|
// create GameObject
|
|
GameObject go = new GameObject("Building_" + w.id);
|
|
MeshFilter mf = go.AddComponent<MeshFilter>();
|
|
MeshRenderer mr = go.AddComponent<MeshRenderer>();
|
|
mr.material = buildingMaterial;
|
|
|
|
// generate mesh: roof (triangulated polygon) + walls (extruded quads)
|
|
Mesh mesh = new Mesh();
|
|
mesh.name = "BuildingMesh_" + w.id;
|
|
|
|
// Convert poly to 2D points (XZ plane)
|
|
List<Vector2> poly2D = new List<Vector2>();
|
|
for (int i = 0; i < poly.Count - 1; i++) // omit last repeated point
|
|
poly2D.Add(new Vector2(poly[i].x, poly[i].z));
|
|
|
|
// triangulate roof
|
|
List<int> roofTris = Triangulate(poly2D);
|
|
if (roofTris == null || roofTris.Count == 0)
|
|
{
|
|
Debug.LogWarning("Triangulation failed for building " + w.id);
|
|
return go;
|
|
}
|
|
|
|
// Build vertices: roof vertices at y=height, walls vertices (2 per poly vertex)
|
|
int n = poly2D.Count;
|
|
// Build vertices and triangles with NO SHARED VERTICES (flat shading)
|
|
List<Vector3> verts = new List<Vector3>();
|
|
List<int> triangles = new List<int>();
|
|
List<Vector2> uvs = new List<Vector2>();
|
|
|
|
// Roof triangles - each triangle gets its own vertices
|
|
for (int i = 0; i < roofTris.Count; i += 3)
|
|
{
|
|
int idx0 = roofTris[i];
|
|
int idx1 = roofTris[i + 1];
|
|
int idx2 = roofTris[i + 2];
|
|
|
|
Vector2 p0 = poly2D[idx0];
|
|
Vector2 p1 = poly2D[idx1];
|
|
Vector2 p2 = poly2D[idx2];
|
|
|
|
int baseIdx = verts.Count;
|
|
verts.Add(new Vector3(p0.x, height / metersPerUnit, p0.y));
|
|
verts.Add(new Vector3(p1.x, height / metersPerUnit, p1.y));
|
|
verts.Add(new Vector3(p2.x, height / metersPerUnit, p2.y));
|
|
|
|
triangles.Add(baseIdx);
|
|
triangles.Add(baseIdx + 1);
|
|
triangles.Add(baseIdx + 2);
|
|
|
|
uvs.Add(new Vector2(p0.x, p0.y));
|
|
uvs.Add(new Vector2(p1.x, p1.y));
|
|
uvs.Add(new Vector2(p2.x, p2.y));
|
|
}
|
|
|
|
// Walls - each quad gets its own 4 vertices
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
int iNext = (i + 1) % n;
|
|
Vector2 p0 = poly2D[i];
|
|
Vector2 p1 = poly2D[iNext];
|
|
|
|
int baseIdx = verts.Count;
|
|
verts.Add(new Vector3(p0.x, height / metersPerUnit, p0.y)); // top left
|
|
verts.Add(new Vector3(p0.x, 0, p0.y)); // bottom left
|
|
verts.Add(new Vector3(p1.x, 0, p1.y)); // bottom right
|
|
verts.Add(new Vector3(p1.x, height / metersPerUnit, p1.y)); // top right
|
|
|
|
triangles.Add(baseIdx);
|
|
triangles.Add(baseIdx + 1);
|
|
triangles.Add(baseIdx + 2);
|
|
|
|
triangles.Add(baseIdx);
|
|
triangles.Add(baseIdx + 2);
|
|
triangles.Add(baseIdx + 3);
|
|
|
|
uvs.Add(new Vector2(0, 1));
|
|
uvs.Add(new Vector2(0, 0));
|
|
uvs.Add(new Vector2(1, 0));
|
|
uvs.Add(new Vector2(1, 1));
|
|
}
|
|
|
|
mesh.SetVertices(verts);
|
|
mesh.SetTriangles(triangles, 0);
|
|
mesh.SetUVs(0, uvs);
|
|
mesh.RecalculateNormals();
|
|
mesh.RecalculateBounds();
|
|
|
|
mf.mesh = mesh;
|
|
|
|
// Center object (using first roof vertex as reference)
|
|
Vector3 centroid = Vector3.zero;
|
|
for (int i = 0; i < roofTris.Count; i += 3)
|
|
{
|
|
centroid += verts[i];
|
|
}
|
|
centroid /= (roofTris.Count / 3);
|
|
go.transform.position = centroid * -1f;
|
|
|
|
// Move the roof/walls vertices back to local space
|
|
Vector3[] adjustedVerts = mesh.vertices;
|
|
for (int i = 0; i < adjustedVerts.Length; i++) adjustedVerts[i] += centroid;
|
|
mesh.vertices = adjustedVerts;
|
|
mesh.RecalculateNormals();
|
|
mesh.RecalculateBounds();
|
|
|
|
return go;
|
|
}
|
|
|
|
GameObject BuildRoadMesh(Way w)
|
|
{
|
|
// build polyline
|
|
List<Vector3> pts = new List<Vector3>();
|
|
foreach (var id in w.nodeRefs)
|
|
pts.Add(NodeIdToLocal(id));
|
|
if (pts.Count < 2) return null;
|
|
|
|
float width = defaultRoadWidth;
|
|
if (w.tags.ContainsKey("width") && float.TryParse(w.tags["width"], NumberStyles.Float, CultureInfo.InvariantCulture, out float wv))
|
|
width = wv;
|
|
else if (w.tags.ContainsKey("highway"))
|
|
{
|
|
// simple heuristic
|
|
string h = w.tags["highway"];
|
|
if (h == "motorway") width = 10f;
|
|
else if (h == "primary") width = 8f;
|
|
else if (h == "secondary") width = 6f;
|
|
else if (h == "tertiary") width = 5f;
|
|
else width = defaultRoadWidth;
|
|
}
|
|
|
|
GameObject go = new GameObject("Road_" + w.id);
|
|
MeshFilter mf = go.AddComponent<MeshFilter>();
|
|
MeshRenderer mr = go.AddComponent<MeshRenderer>();
|
|
mr.material = roadMaterial;
|
|
|
|
Mesh mesh = new Mesh();
|
|
mesh.name = "RoadMesh_" + w.id;
|
|
|
|
List<Vector3> verts = new List<Vector3>();
|
|
List<int> tris = new List<int>();
|
|
List<Vector2> uvs = new List<Vector2>();
|
|
|
|
// build quad strip
|
|
for (int i = 0; i < pts.Count; i++)
|
|
{
|
|
Vector3 p = pts[i];
|
|
Vector3 dir;
|
|
if (i == 0) dir = (pts[i + 1] - p).normalized;
|
|
else if (i == pts.Count - 1) dir = (p - pts[i - 1]).normalized;
|
|
else dir = (pts[i + 1] - pts[i - 1]).normalized;
|
|
|
|
Vector3 normal = Vector3.Cross(dir, Vector3.up).normalized;
|
|
Vector3 left = p + normal * (width * 0.5f / metersPerUnit);
|
|
Vector3 right = p - normal * (width * 0.5f / metersPerUnit);
|
|
verts.Add(left);
|
|
verts.Add(right);
|
|
uvs.Add(new Vector2(0, i));
|
|
uvs.Add(new Vector2(1, i));
|
|
|
|
if (i > 0)
|
|
{
|
|
int baseIdx = verts.Count - 4;
|
|
tris.Add(baseIdx + 0);
|
|
tris.Add(baseIdx + 2);
|
|
tris.Add(baseIdx + 1);
|
|
|
|
tris.Add(baseIdx + 1);
|
|
tris.Add(baseIdx + 2);
|
|
tris.Add(baseIdx + 3);
|
|
}
|
|
}
|
|
|
|
mesh.SetVertices(verts);
|
|
mesh.SetTriangles(tris, 0);
|
|
mesh.SetUVs(0, uvs);
|
|
mesh.RecalculateNormals();
|
|
mesh.RecalculateBounds();
|
|
|
|
mf.mesh = mesh;
|
|
go.transform.position = Vector3.zero;
|
|
return go;
|
|
}
|
|
#endregion
|
|
|
|
#region Helpers
|
|
bool TryParseHeight(string s, out float meters)
|
|
{
|
|
// try to parse heights like "12", "12.5m", "40 ft"
|
|
s = s.Trim();
|
|
meters = 0f;
|
|
if (s.EndsWith("m")) s = s.Substring(0, s.Length - 1).Trim();
|
|
if (float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out float v))
|
|
{
|
|
meters = v;
|
|
return true;
|
|
}
|
|
// fallback: try to extract number
|
|
StringBuilder num = new StringBuilder();
|
|
foreach (char c in s)
|
|
if ((c >= '0' && c <= '9') || c == '.' || c == ',') num.Append(c == ',' ? '.' : c);
|
|
if (num.Length > 0 && float.TryParse(num.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out v))
|
|
{
|
|
meters = v; return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Basic ear clipping triangulation for simple polygons (2D)
|
|
List<int> Triangulate(List<Vector2> poly)
|
|
{
|
|
List<int> indices = new List<int>();
|
|
int n = poly.Count;
|
|
if (n < 3) return indices;
|
|
|
|
List<int> V = new List<int>();
|
|
for (int i = 0; i < n; i++) V.Add(i);
|
|
|
|
int guard = 0;
|
|
while (V.Count > 3 && guard < 10000)
|
|
{
|
|
bool earFound = false;
|
|
for (int i = 0; i < V.Count; i++)
|
|
{
|
|
int prev = V[(i - 1 + V.Count) % V.Count];
|
|
int curr = V[i];
|
|
int next = V[(i + 1) % V.Count];
|
|
|
|
Vector2 a = poly[prev];
|
|
Vector2 b = poly[curr];
|
|
Vector2 c = poly[next];
|
|
|
|
if (!IsConvex(a, b, c)) continue;
|
|
|
|
bool hasPointInside = false;
|
|
for (int j = 0; j < V.Count; j++)
|
|
{
|
|
int vi = V[j];
|
|
if (vi == prev || vi == curr || vi == next) continue;
|
|
if (PointInTriangle(poly[vi], a, b, c)) { hasPointInside = true; break; }
|
|
}
|
|
if (hasPointInside) continue;
|
|
|
|
// ear found
|
|
indices.Add(prev);
|
|
indices.Add(curr);
|
|
indices.Add(next);
|
|
V.RemoveAt(i);
|
|
earFound = true;
|
|
break;
|
|
}
|
|
if (!earFound) break;
|
|
guard++;
|
|
}
|
|
|
|
if (V.Count == 3)
|
|
{
|
|
indices.Add(V[0]); indices.Add(V[1]); indices.Add(V[2]);
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
|
|
bool IsConvex(Vector2 a, Vector2 b, Vector2 c)
|
|
{
|
|
return ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)) < 0f; // changed > to
|
|
}
|
|
bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c)
|
|
{
|
|
float area = TriangleArea(a, b, c);
|
|
float area1 = TriangleArea(p, b, c);
|
|
float area2 = TriangleArea(a, p, c);
|
|
float area3 = TriangleArea(a, b, p);
|
|
return Mathf.Abs(area - (area1 + area2 + area3)) < 1e-3f;
|
|
}
|
|
|
|
float TriangleArea(Vector2 a, Vector2 b, Vector2 c)
|
|
{
|
|
return Mathf.Abs((a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)) * 0.5f);
|
|
}
|
|
#endregion
|
|
}
|
|
|