Add OSM Digital Twin
This commit is contained in:
parent
eaf0a6273a
commit
e4ca27541e
Binary file not shown.
Binary file not shown.
1
pxy_city_digital_twins
Submodule
1
pxy_city_digital_twins
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit eaf0a6273ab5924dd1de9460e739d68559b92c5d
|
0
services/__init__.py
Normal file
0
services/__init__.py
Normal file
BIN
services/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/com_con_city.cpython-310.pyc
Normal file
BIN
services/__pycache__/com_con_city.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/layouts.cpython-310.pyc
Normal file
BIN
services/__pycache__/layouts.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/network.cpython-310.pyc
Normal file
BIN
services/__pycache__/network.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/osm_city.cpython-310.pyc
Normal file
BIN
services/__pycache__/osm_city.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/presets.cpython-310.pyc
Normal file
BIN
services/__pycache__/presets.cpython-310.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/random_city.cpython-310.pyc
Normal file
BIN
services/__pycache__/random_city.cpython-310.pyc
Normal file
Binary file not shown.
155
services/com_con_city.py
Normal file
155
services/com_con_city.py
Normal file
@ -0,0 +1,155 @@
|
||||
import random
|
||||
from .network import compute_mst_fiber_paths, compute_network_summary
|
||||
|
||||
GRID_SIZE = 5
|
||||
SPACING = 15 # Distance between objects
|
||||
|
||||
|
||||
def generate_com_con_city_data(lat, long):
|
||||
"""
|
||||
Generate a digital twin for a real-world city (e.g., Concepción).
|
||||
Returns towers, fiber paths, wifi hotspots, and a summary.
|
||||
"""
|
||||
random.seed(f"{lat},{long}")
|
||||
|
||||
center_x = lat
|
||||
center_z = long
|
||||
|
||||
towers = generate_towers(center_x, center_z)
|
||||
fiber_paths = compute_mst_fiber_paths(towers)
|
||||
wifi_hotspots = generate_wifi_hotspots(center_x, center_z)
|
||||
summary = compute_network_summary(towers, fiber_paths, wifi_hotspots)
|
||||
|
||||
return {
|
||||
'towers': towers,
|
||||
'fiber_paths': fiber_paths,
|
||||
'wifi_hotspots': wifi_hotspots,
|
||||
'network_summary': summary,
|
||||
}
|
||||
|
||||
def generate_towers(center_x, center_z, mode="streets"):
|
||||
"""
|
||||
Generate towers either in a 'grid' or at realistic 'streets' (mocked).
|
||||
mode: "grid" | "streets"
|
||||
"""
|
||||
if mode == "streets":
|
||||
return generate_street_corner_towers(center_x, center_z)
|
||||
else:
|
||||
return generate_grid_towers(center_x, center_z)
|
||||
|
||||
|
||||
import osmnx as ox
|
||||
|
||||
def generate_street_corner_towers(center_x, center_z, min_towers=10):
|
||||
"""
|
||||
Get real intersections from OSM and convert them to local x/z positions
|
||||
relative to center_x / center_z (in meters). Fallbacks to mocked layout if needed.
|
||||
"""
|
||||
print("📍 Starting generate_street_corner_towers()")
|
||||
print(f"→ center_x: {center_x}, center_z: {center_z}")
|
||||
|
||||
point = (center_x, center_z)
|
||||
print(f"→ Using real lat/lon: {point}")
|
||||
|
||||
try:
|
||||
for dist in [100, 200, 500, 1000]:
|
||||
print(f"🛰️ Trying OSM download at radius: {dist} meters...")
|
||||
G = ox.graph_from_point(point, dist=dist, network_type='all')
|
||||
G_undirected = G.to_undirected()
|
||||
degrees = dict(G_undirected.degree())
|
||||
intersections = [n for n, d in degrees.items() if d >= 3]
|
||||
print(f" ✅ Found {len(intersections)} valid intersections.")
|
||||
if len(intersections) >= min_towers:
|
||||
break
|
||||
else:
|
||||
raise ValueError("No sufficient intersections found.")
|
||||
|
||||
nodes, _ = ox.graph_to_gdfs(G)
|
||||
origin_lon = nodes.loc[intersections]['x'].mean()
|
||||
origin_lat = nodes.loc[intersections]['y'].mean()
|
||||
print(f"📌 Using origin_lon: {origin_lon:.6f}, origin_lat: {origin_lat:.6f} for local projection")
|
||||
|
||||
def latlon_to_sim(lon, lat):
|
||||
dx = (lon - origin_lon) * 111320
|
||||
dz = (lat - origin_lat) * 110540
|
||||
return center_x + dx, center_z + dz
|
||||
|
||||
towers = []
|
||||
for i, node_id in enumerate(intersections):
|
||||
row = nodes.loc[node_id]
|
||||
x_sim, z_sim = latlon_to_sim(row['x'], row['y'])
|
||||
print(f" 🗼 Tower #{i+1} at sim position: x={x_sim:.2f}, z={z_sim:.2f}")
|
||||
towers.append(make_tower(x_sim, z_sim, i + 1))
|
||||
|
||||
print(f"✅ Done. Total towers returned: {len(towers)}\n")
|
||||
return towers
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ OSM tower generation failed: {e}")
|
||||
print("⚠️ Falling back to mocked tower layout.")
|
||||
|
||||
# Return 3x3 fixed grid as fallback
|
||||
offsets = [(-30, -30), (-30, 0), (-30, 30),
|
||||
(0, -30), (0, 0), (0, 30),
|
||||
(30, -30), (30, 0), (30, 30)]
|
||||
|
||||
towers = []
|
||||
for i, (dx, dz) in enumerate(offsets):
|
||||
x = center_x + dx
|
||||
z = center_z + dz
|
||||
towers.append(make_tower(x, z, i + 1))
|
||||
|
||||
print(f"✅ Fallback returned {len(towers)} towers.\n")
|
||||
return towers
|
||||
|
||||
|
||||
def generate_grid_towers(center_x, center_z):
|
||||
"""Generates a 5×5 grid of towers around the city center."""
|
||||
towers = []
|
||||
for i in range(GRID_SIZE):
|
||||
for j in range(GRID_SIZE):
|
||||
x = center_x + (i - GRID_SIZE // 2) * SPACING
|
||||
z = center_z + (j - GRID_SIZE // 2) * SPACING
|
||||
towers.append({
|
||||
'id': len(towers) + 1,
|
||||
'status': 'Active' if random.random() > 0.2 else 'Inactive',
|
||||
'position_x': x,
|
||||
'position_y': 0,
|
||||
'position_z': z,
|
||||
'height': random.randint(40, 60),
|
||||
'range': random.randint(500, 1000),
|
||||
'color': '#ff4500'
|
||||
})
|
||||
return towers
|
||||
|
||||
|
||||
def generate_wifi_hotspots(center_x, center_z):
|
||||
"""Places 10 Wi-Fi hotspots randomly around the city center."""
|
||||
hotspots = []
|
||||
bound = SPACING * GRID_SIZE / 2
|
||||
for i in range(10):
|
||||
x = center_x + random.uniform(-bound, bound)
|
||||
z = center_z + random.uniform(-bound, bound)
|
||||
hotspots.append({
|
||||
'id': i + 1,
|
||||
'position_x': x,
|
||||
'position_y': 1.5,
|
||||
'position_z': z,
|
||||
'status': 'Online' if random.random() > 0.2 else 'Offline',
|
||||
'radius': random.randint(1, 3),
|
||||
'color': '#32cd32'
|
||||
})
|
||||
return hotspots
|
||||
|
||||
def make_tower(x, z, id):
|
||||
return {
|
||||
'id': id,
|
||||
'status': 'Active',
|
||||
'position_x': x,
|
||||
'position_y': 0,
|
||||
'position_z': z,
|
||||
'height': 50,
|
||||
'range': 1000,
|
||||
'color': '#ff4500'
|
||||
}
|
||||
|
45
services/layouts.py
Normal file
45
services/layouts.py
Normal file
@ -0,0 +1,45 @@
|
||||
import math
|
||||
|
||||
def rectangular_layout(num_elements, max_dimension):
|
||||
grid_size = int(math.sqrt(num_elements))
|
||||
spacing = max_dimension // grid_size
|
||||
return [
|
||||
{
|
||||
'position_x': (i % grid_size) * spacing,
|
||||
'position_z': (i // grid_size) * spacing
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def circular_layout(num_elements, radius):
|
||||
return [
|
||||
{
|
||||
'position_x': radius * math.cos(2 * math.pi * i / num_elements),
|
||||
'position_z': radius * math.sin(2 * math.pi * i / num_elements)
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def diagonal_layout(num_elements, max_position):
|
||||
return [
|
||||
{
|
||||
'position_x': i * max_position // num_elements,
|
||||
'position_z': i * max_position // num_elements
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def triangular_layout(num_elements):
|
||||
positions = []
|
||||
row_length = 1
|
||||
while num_elements > 0:
|
||||
for i in range(row_length):
|
||||
if num_elements <= 0:
|
||||
break
|
||||
positions.append({
|
||||
'position_x': i * 10 - (row_length - 1) * 5, # Spread out each row symmetrically
|
||||
'position_z': row_length * 10
|
||||
})
|
||||
num_elements -= 1
|
||||
row_length += 1
|
||||
return positions
|
63
services/network.py
Normal file
63
services/network.py
Normal file
@ -0,0 +1,63 @@
|
||||
import networkx as nx
|
||||
import math
|
||||
|
||||
def compute_distance(t1, t2):
|
||||
"""
|
||||
Compute Euclidean distance between two towers in the horizontal plane.
|
||||
"""
|
||||
dx = t1['position_x'] - t2['position_x']
|
||||
dz = t1['position_z'] - t2['position_z']
|
||||
return math.sqrt(dx**2 + dz**2)
|
||||
|
||||
def compute_mst_fiber_paths(towers):
|
||||
"""
|
||||
Given a list of tower dictionaries, compute a Minimum Spanning Tree (MST)
|
||||
and return a list of fiber paths connecting the towers.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
# Add towers as nodes
|
||||
for tower in towers:
|
||||
G.add_node(tower['id'], **tower)
|
||||
|
||||
# Add edges: compute pairwise distances
|
||||
n = len(towers)
|
||||
for i in range(n):
|
||||
for j in range(i+1, n):
|
||||
d = compute_distance(towers[i], towers[j])
|
||||
G.add_edge(towers[i]['id'], towers[j]['id'], weight=d)
|
||||
|
||||
# Compute MST
|
||||
mst = nx.minimum_spanning_tree(G)
|
||||
|
||||
fiber_paths = []
|
||||
for edge in mst.edges(data=True):
|
||||
id1, id2, data = edge
|
||||
# Find towers corresponding to these IDs
|
||||
tower1 = next(t for t in towers if t['id'] == id1)
|
||||
tower2 = next(t for t in towers if t['id'] == id2)
|
||||
|
||||
fiber_paths.append({
|
||||
'id': len(fiber_paths) + 1,
|
||||
'start_x': tower1['position_x'],
|
||||
'start_z': tower1['position_z'],
|
||||
'end_x': tower2['position_x'],
|
||||
'end_z': tower2['position_z'],
|
||||
'mid_x': (tower1['position_x'] + tower2['position_x']) / 2,
|
||||
'mid_y': 0.1, # Slightly above the ground
|
||||
'mid_z': (tower1['position_z'] + tower2['position_z']) / 2,
|
||||
'length': data['weight'],
|
||||
# Optionally, compute the angle in degrees if needed:
|
||||
'angle': math.degrees(math.atan2(tower2['position_x'] - tower1['position_x'],
|
||||
tower2['position_z'] - tower1['position_z'])),
|
||||
'status': 'Connected',
|
||||
'color': '#4682b4'
|
||||
})
|
||||
return fiber_paths
|
||||
|
||||
def compute_network_summary(towers, fiber_paths, wifi_hotspots):
|
||||
total_fiber = sum(fiber['length'] for fiber in fiber_paths)
|
||||
return {
|
||||
'num_towers': len(towers),
|
||||
'total_fiber_length': total_fiber,
|
||||
'num_wifi': len(wifi_hotspots),
|
||||
}
|
70
services/osm_city.py
Normal file
70
services/osm_city.py
Normal file
@ -0,0 +1,70 @@
|
||||
import osmnx as ox
|
||||
import shapely
|
||||
import random
|
||||
import uuid
|
||||
|
||||
def generate_osm_city_data(lat, lon, dist=400, scale=1.0):
|
||||
print(f"🏙️ Fetching OSM buildings at ({lat}, {lon})")
|
||||
|
||||
scale_factor = scale # Shrinks the city to make the camera look like a giant
|
||||
|
||||
# ————— BUILDINGS —————
|
||||
tags = {"building": True}
|
||||
gdf = ox.features_from_point((lat, lon), tags=tags, dist=dist)
|
||||
|
||||
gdf = gdf[gdf.geometry.type.isin(["Polygon", "MultiPolygon"])].copy()
|
||||
gdf = gdf.to_crs(epsg=3857)
|
||||
gdf["centroid"] = gdf.geometry.centroid
|
||||
|
||||
status_options = ["OK", "Warning", "Critical", "Offline"]
|
||||
raw_buildings = []
|
||||
|
||||
for i, row in gdf.iterrows():
|
||||
centroid = row["centroid"]
|
||||
polygon = row["geometry"]
|
||||
|
||||
try:
|
||||
height = float(row.get("height", None))
|
||||
except:
|
||||
try:
|
||||
height = float(row.get("building:levels", 3)) * 3.2
|
||||
except:
|
||||
height = 10.0
|
||||
|
||||
building_id = f"BLD-{uuid.uuid4().hex[:6].upper()}"
|
||||
status = random.choice(status_options)
|
||||
|
||||
raw_buildings.append({
|
||||
"id": building_id,
|
||||
"raw_x": centroid.x,
|
||||
"raw_z": centroid.y,
|
||||
"width": polygon.bounds[2] - polygon.bounds[0],
|
||||
"depth": polygon.bounds[3] - polygon.bounds[1],
|
||||
"height": height,
|
||||
"color": "#8a2be2",
|
||||
"status": status,
|
||||
})
|
||||
|
||||
# ————— CENTER AND SCALE —————
|
||||
if raw_buildings:
|
||||
avg_x = sum(b['raw_x'] for b in raw_buildings) / len(raw_buildings)
|
||||
avg_z = sum(b['raw_z'] for b in raw_buildings) / len(raw_buildings)
|
||||
|
||||
buildings = []
|
||||
for b in raw_buildings:
|
||||
buildings.append({
|
||||
"id": b['id'],
|
||||
"position_x": (b['raw_x'] - avg_x) * scale_factor,
|
||||
"position_z": (b['raw_z'] - avg_z) * scale_factor,
|
||||
"width": b['width'] * scale_factor,
|
||||
"depth": b['depth'] * scale_factor,
|
||||
"height": b['height'] * scale_factor,
|
||||
"color": b['color'],
|
||||
"status": b['status'],
|
||||
})
|
||||
else:
|
||||
buildings = []
|
||||
|
||||
return {
|
||||
"buildings": buildings,
|
||||
}
|
25
services/presets.py
Normal file
25
services/presets.py
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
def get_environment_preset(lat, long):
|
||||
"""
|
||||
Determines the A-Frame environment preset based on latitude and longitude.
|
||||
You can adjust the logic to suit your needs.
|
||||
"""
|
||||
# Example logic: adjust these thresholds as needed
|
||||
if lat >= 60 or lat <= -60:
|
||||
return 'snow' # Polar regions: snow environment
|
||||
elif lat >= 30 or lat <= -30:
|
||||
return 'forest' # Mid-latitudes: forest environment
|
||||
elif long >= 100:
|
||||
return 'goldmine' # Arbitrary example: for far east longitudes, a 'goldmine' preset
|
||||
else:
|
||||
return 'desert' # Default to desert for lower latitudes and moderate longitudes
|
||||
|
||||
|
||||
def get_environment_by_lat(lat):
|
||||
if lat > 60 or lat < -60:
|
||||
return 'yeti'
|
||||
elif 30 < lat < 60 or -30 > lat > -60:
|
||||
return 'forest'
|
||||
else:
|
||||
return 'desert'
|
81
services/random_city.py
Normal file
81
services/random_city.py
Normal file
@ -0,0 +1,81 @@
|
||||
import random
|
||||
from .layouts import rectangular_layout, circular_layout, diagonal_layout, triangular_layout
|
||||
|
||||
|
||||
def generate_random_city_data(innovation_pct=100, technology_pct=100, science_pct=100, max_position=100, radius=50):
|
||||
num_buildings = random.randint(5, 35)
|
||||
num_lamps = random.randint(5, 100)
|
||||
num_trees = random.randint(5, 55)
|
||||
|
||||
# Buildings layout distribution
|
||||
num_rectangular_buildings = int(num_buildings * innovation_pct / 100)
|
||||
num_circular_buildings = (num_buildings - num_rectangular_buildings) // 2
|
||||
num_triangular_buildings = num_buildings - num_rectangular_buildings - num_circular_buildings
|
||||
|
||||
building_positions = rectangular_layout(num_rectangular_buildings, max_position) + \
|
||||
circular_layout(num_circular_buildings, radius) + \
|
||||
triangular_layout(num_triangular_buildings)
|
||||
|
||||
# Lamps layout distribution
|
||||
num_triangular_lamps = int(num_lamps * technology_pct / 100)
|
||||
num_circular_lamps = (num_lamps - num_triangular_lamps) // 2
|
||||
num_diagonal_lamps = num_lamps - num_triangular_lamps - num_circular_lamps
|
||||
|
||||
lamp_positions = triangular_layout(num_triangular_lamps) + \
|
||||
circular_layout(num_circular_lamps, radius) + \
|
||||
diagonal_layout(num_diagonal_lamps, max_position)
|
||||
|
||||
# Trees layout distribution
|
||||
num_circular_trees = int(num_trees * science_pct / 100)
|
||||
num_triangular_trees = (num_trees - num_circular_trees) // 2
|
||||
num_diagonal_trees = num_trees - num_circular_trees - num_triangular_trees
|
||||
|
||||
tree_positions = circular_layout(num_circular_trees, radius) + \
|
||||
triangular_layout(num_triangular_trees) + \
|
||||
diagonal_layout(num_diagonal_trees, max_position)
|
||||
|
||||
buildings = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Occupied', 'Vacant', 'Under Construction']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(10, 50),
|
||||
'width': random.randint(5, 20),
|
||||
'depth': random.randint(5, 20),
|
||||
'color': random.choice(['#8a2be2', '#5f9ea0', '#ff6347', '#4682b4']),
|
||||
'file': ''
|
||||
} for i, pos in enumerate(building_positions)
|
||||
]
|
||||
|
||||
lamps = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Functional', 'Non-functional']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(3, 10),
|
||||
'color': random.choice(['#ffff00', '#ff0000', '#00ff00']),
|
||||
} for i, pos in enumerate(lamp_positions)
|
||||
]
|
||||
|
||||
trees = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Healthy', 'Diseased', 'Wilting']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(5, 30),
|
||||
'radius_bottom': random.uniform(0.1, 0.5),
|
||||
'radius_top': random.uniform(0.5, 2.0),
|
||||
'color_trunk': '#8b4513',
|
||||
'color_leaves': random.choice(['#228b22', '#90ee90', '#8b4513']),
|
||||
} for i, pos in enumerate(tree_positions)
|
||||
]
|
||||
|
||||
return {
|
||||
'buildings': buildings,
|
||||
'lamps': lamps,
|
||||
'trees': trees,
|
||||
}
|
||||
|
38
templates/pxy_city_digital_twins/_status_gauge.html
Normal file
38
templates/pxy_city_digital_twins/_status_gauge.html
Normal file
@ -0,0 +1,38 @@
|
||||
<a-entity
|
||||
class="status-gauge"
|
||||
gauge-click-toggle
|
||||
position="0 {{ offset_y|default:'3' }} 0"
|
||||
scale="0.3 0.3 0.3">
|
||||
|
||||
<!-- Glass core -->
|
||||
<a-circle
|
||||
radius="0.6"
|
||||
class="glass-core"
|
||||
color="{{ ring_color|default:'#00FFFF' }}"
|
||||
material="shader: standard; transparent: true; opacity: 0.3; metalness: 0.8; roughness: 0.1; side: double"
|
||||
rotation="0 90 0">
|
||||
</a-circle>
|
||||
|
||||
<!-- Animated outer ring -->
|
||||
<a-ring
|
||||
class="gauge-ring"
|
||||
radius-inner="0.7"
|
||||
radius-outer="0.9"
|
||||
color="{{ ring_color|default:'#00FFFF' }}"
|
||||
material="shader: flat; opacity: 0.8; side: double"
|
||||
rotation="0 90 0"
|
||||
animation="property: rotation; to: 0 90 0; loop: true; dur: 2000; easing: linear">
|
||||
</a-ring>
|
||||
|
||||
<!-- Dynamic Text -->
|
||||
<a-text
|
||||
class="gauge-label"
|
||||
value="ID: {{ id }}\n{{ status }}"
|
||||
align="center"
|
||||
color="#FFFFFF"
|
||||
width="2"
|
||||
side="double"
|
||||
position="0 0 0.02"
|
||||
rotation="0 90 0">
|
||||
</a-text>
|
||||
</a-entity>
|
@ -3,7 +3,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LDS City</title>
|
||||
<title>Digital Twin City</title>
|
||||
<!-- A-Frame 1.7.0 & environment component -->
|
||||
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
|
||||
<script src="https://unpkg.com/aframe-environment-component/dist/aframe-environment-component.min.js"></script>
|
||||
@ -19,22 +19,33 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<!-- Register a simple component to toggle the chat panel -->
|
||||
|
||||
<script>
|
||||
AFRAME.registerComponent('toggle-chat', {
|
||||
AFRAME.registerComponent('gauge-click-toggle', {
|
||||
init: function () {
|
||||
this.el.addEventListener('click', function () {
|
||||
var panel = document.querySelector('#chatPanel');
|
||||
var isVisible = panel.getAttribute('visible');
|
||||
panel.setAttribute('visible', !isVisible);
|
||||
const colors = ["#00FF00", "#FFFF00", "#FF0000", "#00FFFF"];
|
||||
const statuses = ["OK", "Warning", "Critical", "Offline"];
|
||||
let i = 0;
|
||||
|
||||
const ring = this.el.querySelector('.gauge-ring');
|
||||
const label = this.el.querySelector('.gauge-label');
|
||||
|
||||
this.el.addEventListener('click', () => {
|
||||
i = (i + 1) % colors.length;
|
||||
if (ring) ring.setAttribute('color', colors[i]);
|
||||
if (label) {
|
||||
const current = label.getAttribute('value');
|
||||
const idLine = current.split("\n")[0]; // Preserve ID line
|
||||
label.setAttribute('value', `${idLine}\n${statuses[i]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<a-scene shadow="type: pcfsoft"
|
||||
environment="preset: forest; dressing: trees; groundColor: #777; skyType: gradient; dressingAmount: 20;">
|
||||
<a-scene environment="preset: forest; groundTexture: walk; dressing: trees; fog: 0.7">
|
||||
|
||||
<!-- Camera & Controls (give it an id for look-at) -->
|
||||
<a-entity id="mainCamera" camera look-controls wasd-controls position="0 2 5">
|
||||
@ -56,19 +67,13 @@
|
||||
color="{{ building.color }}">
|
||||
</a-box>
|
||||
|
||||
<!-- Label entity: plane + text, billboarded to face camera -->
|
||||
<a-entity position="{{ building.position_x }} 3 {{ building.position_z }}"
|
||||
billboard="#mainCamera">
|
||||
<!-- Semi-transparent background plane -->
|
||||
<a-plane width="4" height="1.2" color="#000" opacity="0.5"></a-plane>
|
||||
<!-- Text in front of plane -->
|
||||
<a-text value="Status: {{ building.status }}"
|
||||
width="4"
|
||||
align="center"
|
||||
color="#FFF"
|
||||
position="0 0 0.01">
|
||||
</a-text>
|
||||
<a-entity
|
||||
position="{{ building.position_x }} {{ building.height }} {{ building.position_z }}">
|
||||
{% include "pxy_city_digital_twins/_status_gauge.html" with ring_color="#00FFFF" offset_y="1.50" status=building.status id=building.id %}
|
||||
</a-entity>
|
||||
|
||||
|
||||
|
||||
</a-entity>
|
||||
{% endfor %}
|
||||
|
||||
@ -204,29 +209,6 @@
|
||||
</a-entity>
|
||||
{% endfor %}
|
||||
|
||||
<!-- VR AI Agent -->
|
||||
<a-entity id="aiAgent" position="0 2 -3" toggle-chat class="clickable">
|
||||
<!-- Agent appears as a rotating sphere -->
|
||||
<a-sphere radius="0.5" color="#FF69B4"
|
||||
animation="property: rotation; to: 0 360 0; loop: true; dur: 5000">
|
||||
</a-sphere>
|
||||
<!-- Prompt text above the agent -->
|
||||
<a-text value="Ask me something!" position="0 1 0" align="center" width="2" color="#FFF"></a-text>
|
||||
</a-entity>
|
||||
|
||||
<!-- Chat Panel (initially hidden) -->
|
||||
<a-entity id="chatPanel" visible="false" position="1 2 -3">
|
||||
<!-- Background panel for the chat -->
|
||||
<a-plane width="3" height="2" color="#000" opacity="0.7" shadow="cast: false; receive: false"></a-plane>
|
||||
<!-- Chat text; later this can be dynamic -->
|
||||
<a-text value="Network Summary:
|
||||
Towers: {{ city_data.network_summary.num_towers }}
|
||||
Fiber: {{ city_data.network_summary.total_fiber_length|floatformat:2 }} m
|
||||
Wi-Fi: {{ city_data.network_summary.num_wifi }}"
|
||||
align="center" width="4" color="#FFF"
|
||||
position="0 0 0.01">
|
||||
</a-text>
|
||||
</a-entity>
|
||||
|
||||
</a-scene>
|
||||
|
||||
|
286
views.py
286
views.py
@ -2,28 +2,29 @@ from django.shortcuts import render, get_object_or_404
|
||||
from django.http import Http404
|
||||
import random
|
||||
import math
|
||||
from .services.presets import get_environment_preset
|
||||
import networkx as nx
|
||||
from .services.layouts import (
|
||||
rectangular_layout,
|
||||
circular_layout,
|
||||
diagonal_layout,
|
||||
triangular_layout,
|
||||
)
|
||||
from .services.random_city import generate_random_city_data
|
||||
from .services.com_con_city import generate_com_con_city_data
|
||||
from .services.osm_city import generate_osm_city_data
|
||||
|
||||
|
||||
def get_environment_preset(lat, long):
|
||||
"""
|
||||
Determines the A-Frame environment preset based on latitude and longitude.
|
||||
You can adjust the logic to suit your needs.
|
||||
"""
|
||||
# Example logic: adjust these thresholds as needed
|
||||
if lat >= 60 or lat <= -60:
|
||||
return 'snow' # Polar regions: snow environment
|
||||
elif lat >= 30 or lat <= -30:
|
||||
return 'forest' # Mid-latitudes: forest environment
|
||||
elif long >= 100:
|
||||
return 'goldmine' # Arbitrary example: for far east longitudes, a 'goldmine' preset
|
||||
else:
|
||||
return 'desert' # Default to desert for lower latitudes and moderate longitudes
|
||||
|
||||
def city_digital_twin(request, city_id, innovation_pct=None, technology_pct=None, science_pct=None):
|
||||
try:
|
||||
lat = float(request.GET.get('lat', 0))
|
||||
long = float(request.GET.get('long', 0))
|
||||
scale = float(request.GET.get('scale', 1.0)) # default to 1.0 (normal scale)
|
||||
|
||||
if city_id == "com_con":
|
||||
if city_id == "osm_city":
|
||||
city_data = generate_osm_city_data(lat, long,scale=scale)
|
||||
elif city_id == "com_con":
|
||||
city_data = generate_com_con_city_data(lat, long)
|
||||
elif city_id == "random_city":
|
||||
city_data = generate_random_city_data()
|
||||
@ -134,258 +135,3 @@ def get_example_data():
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
def rectangular_layout(num_elements, max_dimension):
|
||||
grid_size = int(math.sqrt(num_elements))
|
||||
spacing = max_dimension // grid_size
|
||||
return [
|
||||
{
|
||||
'position_x': (i % grid_size) * spacing,
|
||||
'position_z': (i // grid_size) * spacing
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def circular_layout(num_elements, radius):
|
||||
return [
|
||||
{
|
||||
'position_x': radius * math.cos(2 * math.pi * i / num_elements),
|
||||
'position_z': radius * math.sin(2 * math.pi * i / num_elements)
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def diagonal_layout(num_elements, max_position):
|
||||
return [
|
||||
{
|
||||
'position_x': i * max_position // num_elements,
|
||||
'position_z': i * max_position // num_elements
|
||||
}
|
||||
for i in range(num_elements)
|
||||
]
|
||||
|
||||
def triangular_layout(num_elements):
|
||||
positions = []
|
||||
row_length = 1
|
||||
while num_elements > 0:
|
||||
for i in range(row_length):
|
||||
if num_elements <= 0:
|
||||
break
|
||||
positions.append({
|
||||
'position_x': i * 10 - (row_length - 1) * 5, # Spread out each row symmetrically
|
||||
'position_z': row_length * 10
|
||||
})
|
||||
num_elements -= 1
|
||||
row_length += 1
|
||||
return positions
|
||||
|
||||
|
||||
def generate_random_city_data(innovation_pct=100, technology_pct=100, science_pct=100, max_position=100, radius=50):
|
||||
num_buildings = random.randint(5, 35)
|
||||
num_lamps = random.randint(5, 100)
|
||||
num_trees = random.randint(5, 55)
|
||||
|
||||
# Buildings layout distribution
|
||||
num_rectangular_buildings = int(num_buildings * innovation_pct / 100)
|
||||
num_circular_buildings = (num_buildings - num_rectangular_buildings) // 2
|
||||
num_triangular_buildings = num_buildings - num_rectangular_buildings - num_circular_buildings
|
||||
|
||||
building_positions = rectangular_layout(num_rectangular_buildings, max_position) + \
|
||||
circular_layout(num_circular_buildings, radius) + \
|
||||
triangular_layout(num_triangular_buildings)
|
||||
|
||||
# Lamps layout distribution
|
||||
num_triangular_lamps = int(num_lamps * technology_pct / 100)
|
||||
num_circular_lamps = (num_lamps - num_triangular_lamps) // 2
|
||||
num_diagonal_lamps = num_lamps - num_triangular_lamps - num_circular_lamps
|
||||
|
||||
lamp_positions = triangular_layout(num_triangular_lamps) + \
|
||||
circular_layout(num_circular_lamps, radius) + \
|
||||
diagonal_layout(num_diagonal_lamps, max_position)
|
||||
|
||||
# Trees layout distribution
|
||||
num_circular_trees = int(num_trees * science_pct / 100)
|
||||
num_triangular_trees = (num_trees - num_circular_trees) // 2
|
||||
num_diagonal_trees = num_trees - num_circular_trees - num_triangular_trees
|
||||
|
||||
tree_positions = circular_layout(num_circular_trees, radius) + \
|
||||
triangular_layout(num_triangular_trees) + \
|
||||
diagonal_layout(num_diagonal_trees, max_position)
|
||||
|
||||
buildings = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Occupied', 'Vacant', 'Under Construction']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(10, 50),
|
||||
'width': random.randint(5, 20),
|
||||
'depth': random.randint(5, 20),
|
||||
'color': random.choice(['#8a2be2', '#5f9ea0', '#ff6347', '#4682b4']),
|
||||
'file': ''
|
||||
} for i, pos in enumerate(building_positions)
|
||||
]
|
||||
|
||||
lamps = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Functional', 'Non-functional']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(3, 10),
|
||||
'color': random.choice(['#ffff00', '#ff0000', '#00ff00']),
|
||||
} for i, pos in enumerate(lamp_positions)
|
||||
]
|
||||
|
||||
trees = [
|
||||
{
|
||||
'id': i + 1,
|
||||
'status': random.choice(['Healthy', 'Diseased', 'Wilting']),
|
||||
'position_x': pos['position_x'],
|
||||
'position_z': pos['position_z'],
|
||||
'height': random.randint(5, 30),
|
||||
'radius_bottom': random.uniform(0.1, 0.5),
|
||||
'radius_top': random.uniform(0.5, 2.0),
|
||||
'color_trunk': '#8b4513',
|
||||
'color_leaves': random.choice(['#228b22', '#90ee90', '#8b4513']),
|
||||
} for i, pos in enumerate(tree_positions)
|
||||
]
|
||||
|
||||
return {
|
||||
'buildings': buildings,
|
||||
'lamps': lamps,
|
||||
'trees': trees,
|
||||
}
|
||||
|
||||
|
||||
def get_environment_by_lat(lat):
|
||||
if lat > 60 or lat < -60:
|
||||
return 'yeti'
|
||||
elif 30 < lat < 60 or -30 > lat > -60:
|
||||
return 'forest'
|
||||
else:
|
||||
return 'desert'
|
||||
|
||||
|
||||
import random
|
||||
|
||||
def generate_com_con_city_data(lat, long):
|
||||
random.seed(f"{lat},{long}")
|
||||
|
||||
center_x = lat % 100
|
||||
center_z = long % 100
|
||||
|
||||
grid_size = 5
|
||||
spacing = 15 # Distance between objects
|
||||
|
||||
# Towers in a grid
|
||||
towers = []
|
||||
for i in range(grid_size):
|
||||
for j in range(grid_size):
|
||||
x = center_x + (i - grid_size // 2) * spacing
|
||||
z = center_z + (j - grid_size // 2) * spacing
|
||||
towers.append({
|
||||
'id': len(towers) + 1,
|
||||
'status': 'Active' if random.random() > 0.2 else 'Inactive',
|
||||
'position_x': x,
|
||||
'position_y': 0,
|
||||
'position_z': z,
|
||||
'height': random.randint(40, 60),
|
||||
'range': random.randint(500, 1000),
|
||||
'color': '#ff4500'
|
||||
})
|
||||
|
||||
# Fiber paths connect neighboring towers
|
||||
# Compute optimized fiber paths using MST
|
||||
fiber_paths = compute_mst_fiber_paths(towers)
|
||||
|
||||
# Wi-Fi Hotspots scattered nearby but within grid bounds
|
||||
wifi_hotspots = []
|
||||
for i in range(10):
|
||||
x = center_x + random.uniform(-spacing * grid_size / 2, spacing * grid_size / 2)
|
||||
z = center_z + random.uniform(-spacing * grid_size / 2, spacing * grid_size / 2)
|
||||
wifi_hotspots.append({
|
||||
'id': i + 1,
|
||||
'position_x': x,
|
||||
'position_y': 1.5,
|
||||
'position_z': z,
|
||||
'status': 'Online' if random.random() > 0.2 else 'Offline',
|
||||
'radius': random.randint(1, 3),
|
||||
'color': '#32cd32'
|
||||
})
|
||||
|
||||
network_summary = compute_network_summary(towers, fiber_paths, wifi_hotspots)
|
||||
|
||||
return {
|
||||
'towers': towers,
|
||||
'fiber_paths': fiber_paths,
|
||||
'wifi_hotspots': wifi_hotspots,
|
||||
'network_summary': network_summary,
|
||||
}
|
||||
|
||||
|
||||
import networkx as nx
|
||||
import math
|
||||
|
||||
def compute_distance(t1, t2):
|
||||
"""
|
||||
Compute Euclidean distance between two towers in the horizontal plane.
|
||||
"""
|
||||
dx = t1['position_x'] - t2['position_x']
|
||||
dz = t1['position_z'] - t2['position_z']
|
||||
return math.sqrt(dx**2 + dz**2)
|
||||
|
||||
def compute_mst_fiber_paths(towers):
|
||||
"""
|
||||
Given a list of tower dictionaries, compute a Minimum Spanning Tree (MST)
|
||||
and return a list of fiber paths connecting the towers.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
# Add towers as nodes
|
||||
for tower in towers:
|
||||
G.add_node(tower['id'], **tower)
|
||||
|
||||
# Add edges: compute pairwise distances
|
||||
n = len(towers)
|
||||
for i in range(n):
|
||||
for j in range(i+1, n):
|
||||
d = compute_distance(towers[i], towers[j])
|
||||
G.add_edge(towers[i]['id'], towers[j]['id'], weight=d)
|
||||
|
||||
# Compute MST
|
||||
mst = nx.minimum_spanning_tree(G)
|
||||
|
||||
fiber_paths = []
|
||||
for edge in mst.edges(data=True):
|
||||
id1, id2, data = edge
|
||||
# Find towers corresponding to these IDs
|
||||
tower1 = next(t for t in towers if t['id'] == id1)
|
||||
tower2 = next(t for t in towers if t['id'] == id2)
|
||||
|
||||
fiber_paths.append({
|
||||
'id': len(fiber_paths) + 1,
|
||||
'start_x': tower1['position_x'],
|
||||
'start_z': tower1['position_z'],
|
||||
'end_x': tower2['position_x'],
|
||||
'end_z': tower2['position_z'],
|
||||
'mid_x': (tower1['position_x'] + tower2['position_x']) / 2,
|
||||
'mid_y': 0.1, # Slightly above the ground
|
||||
'mid_z': (tower1['position_z'] + tower2['position_z']) / 2,
|
||||
'length': data['weight'],
|
||||
# Optionally, compute the angle in degrees if needed:
|
||||
'angle': math.degrees(math.atan2(tower2['position_x'] - tower1['position_x'],
|
||||
tower2['position_z'] - tower1['position_z'])),
|
||||
'status': 'Connected',
|
||||
'color': '#4682b4'
|
||||
})
|
||||
return fiber_paths
|
||||
|
||||
def compute_network_summary(towers, fiber_paths, wifi_hotspots):
|
||||
total_fiber = sum(fiber['length'] for fiber in fiber_paths)
|
||||
return {
|
||||
'num_towers': len(towers),
|
||||
'total_fiber_length': total_fiber,
|
||||
'num_wifi': len(wifi_hotspots),
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user