Add OSM Digital Twin

This commit is contained in:
Ekaropolus 2025-05-11 23:21:12 -06:00
parent eaf0a6273a
commit e4ca27541e
20 changed files with 526 additions and 320 deletions

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
Subproject commit eaf0a6273ab5924dd1de9460e739d68559b92c5d

0
services/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

155
services/com_con_city.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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,
}

View 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>

View File

@ -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', {
init: function () {
this.el.addEventListener('click', function () {
var panel = document.querySelector('#chatPanel');
var isVisible = panel.getAttribute('visible');
panel.setAttribute('visible', !isVisible);
});
}
});
</script>
<script>
AFRAME.registerComponent('gauge-click-toggle', {
init: function () {
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
View File

@ -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),
}