From e4ca27541e264e4916481d96f0e0fb0d68b0672b Mon Sep 17 00:00:00 2001 From: Ekaropolus Date: Sun, 11 May 2025 23:21:12 -0600 Subject: [PATCH] Add OSM Digital Twin --- __pycache__/urls.cpython-310.pyc | Bin 397 -> 397 bytes __pycache__/views.cpython-310.pyc | Bin 8840 -> 2541 bytes pxy_city_digital_twins | 1 + services/__init__.py | 0 services/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 141 bytes .../__pycache__/com_con_city.cpython-310.pyc | Bin 0 -> 4705 bytes services/__pycache__/layouts.cpython-310.pyc | Bin 0 -> 1547 bytes services/__pycache__/network.cpython-310.pyc | Bin 0 -> 2158 bytes services/__pycache__/osm_city.cpython-310.pyc | Bin 0 -> 1892 bytes services/__pycache__/presets.cpython-310.pyc | Bin 0 -> 758 bytes .../__pycache__/random_city.cpython-310.pyc | Bin 0 -> 2453 bytes services/com_con_city.py | 155 ++++++++++ services/layouts.py | 45 +++ services/network.py | 63 ++++ services/osm_city.py | 70 +++++ services/presets.py | 25 ++ services/random_city.py | 81 +++++ .../pxy_city_digital_twins/_status_gauge.html | 38 +++ .../city_digital_twin.html | 82 ++--- views.py | 286 +----------------- 20 files changed, 526 insertions(+), 320 deletions(-) create mode 160000 pxy_city_digital_twins create mode 100644 services/__init__.py create mode 100644 services/__pycache__/__init__.cpython-310.pyc create mode 100644 services/__pycache__/com_con_city.cpython-310.pyc create mode 100644 services/__pycache__/layouts.cpython-310.pyc create mode 100644 services/__pycache__/network.cpython-310.pyc create mode 100644 services/__pycache__/osm_city.cpython-310.pyc create mode 100644 services/__pycache__/presets.cpython-310.pyc create mode 100644 services/__pycache__/random_city.cpython-310.pyc create mode 100644 services/com_con_city.py create mode 100644 services/layouts.py create mode 100644 services/network.py create mode 100644 services/osm_city.py create mode 100644 services/presets.py create mode 100644 services/random_city.py create mode 100644 templates/pxy_city_digital_twins/_status_gauge.html diff --git a/__pycache__/urls.cpython-310.pyc b/__pycache__/urls.cpython-310.pyc index 4227ce172b88834abe3d20ceb3ed0a33753556c0..eadc07cd53a982cd325afd810bc185ff39870af9 100644 GIT binary patch delta 21 acmeBW?q%l5=jG*M0D=WhiWwVu8W{mC%>-%y delta 21 bcmeBW?q%l5=jG*M0D|;ihtoImG%^AJG}Hx( diff --git a/__pycache__/views.cpython-310.pyc b/__pycache__/views.cpython-310.pyc index 8d7cdb4caefdf8ecbfc879d056c08a9333554d4c..2c7d80d435cc3263c63cfe01e0458e911447de14 100644 GIT binary patch delta 1414 zcmZuxy>A>v6yMqJ``WvUzb-g3vG_>Vh;X?ikXVr;fgD^JP$Gz?vDQ1|_2T`w%+AF= zYfmS*NFX5!YbhY2ARW-q@DET>P$5xn1?3e;bQBcuX3svIB4#ze_vSZm-@JMAW!A`vAh;WAN)9#q0Au7)*S3oUGgbzFye zl^8)IY~p62o58to4$l>ODVPrz@B-)!QYMwh2rrTGB)d5l>)Ud! zGa3=f@}@^m{#`c4NYE&ID7R!=&3-D+-_Pr=&)i<%u${o^$333ciSKxEx&bQRZ;E!<>l3;g0L`{tj`t1OC5MFRX*j;?yQiW(-_SM0eUMAWP8Sf}X5F ze8-<;Kg%x;xs*yDqHko#hW1n*p@ESiq94fYGFMV9)l-8QpDP11+5+ALzQa|)k7VFB zfGYvF&b5&QuzShn}dqbY-M|U+diLtzj|c|x4*cWxf(irA2$-p9{Da!u1wA=VIpK~MwB1M?BFPu zqoWz4$!m}>kwYf-%)~Wi;$_40Q&%&Wzo;y{FrDhV;#GhW5>-&t9&S!2&uVs3m=}d- samq~W;n}^48pzGtVN7}f-4Hh+c@CfmOOU(3 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 KF%ytrVE_Pta~`e$ literal 0 HcmV?d00001 diff --git a/services/__pycache__/com_con_city.cpython-310.pyc b/services/__pycache__/com_con_city.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..147fc1ef24869ea8604eebc0fd720a2be08b9682 GIT binary patch literal 4705 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 Y+RN4^8CBM{k)O85P+y<7ONFU_1Nf1)L;wH) literal 0 HcmV?d00001 diff --git a/services/__pycache__/layouts.cpython-310.pyc b/services/__pycache__/layouts.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1eb96dee39ff579d806c38aecfa13e8e94d80231 GIT binary patch literal 1547 zcmbVM&2JMs5TEgGwwrw^6%e4xN?!qSbmy&D-pzwgYXI zoLY%;e0L7zn15-noO0%X1ma`HOGH~fo_LmKJho@X_HRCl#l;9{tgfu>tknSC@Z$6k zw75rPZV{6pF@r8hA-yB$qV#2rW+4N)K(i-9S*O{T_z2q3j4WU~VGr}cvQOj@nGIqV zES`ulyv9R(31d8l7QDn4@B*I*iBcSe)*HhiJ|H5W-gbgyH6QGb9u1d?%sR0?jN$Rp z1liG5IL78zmn=zmCH@t=;t@=7D#~bB7-dzF_x4W72UDOkv2nXZMMAx=mFyXHkd_N( z7$++4>wxZg-|57|VQ09v-%FI;?@85HHqLtXxynt)r23gkQq#%e{bFRz#&ExkHZx^x zQVfP$?BH#KSOB6HeT>lm6ns8d*J)zoyg$lf-Kz{YHnSp$GqXj)+_>$Rp2}_M4`RDh z`es+#vZl!{ZHuyzj|ROoO9yFgO?Pn+@6pR>Vd$Ig-I{}~7m1aZzQ4~!g*7f72rr$p=bPTL&R1yvBJsDNr5gDkb-*yTTpd)>w>r=c zIQQxbqq4y%GcOE>*g2#);F{q&xG7cKFLFxypC_xnPMYlsdcDe~?y?&!RS=zXa*a<` zn5_{zQ|sI|*!n(^c}lDg6O=fi<{`yDzP@8&EEu52D-&uNw1>KhkY5|)364GKQEE|3 zSqAAhVVPyxk+oOiH6nEliGRZcb{nj77v^3~IOet!Y|psuXl64mkQsNKD&>SrfHI(d zkmiyykn^ltpMz++kuIM1vNZ489bIQLhk;`bgNs(Hvvt2pT85$X(RY;?((L0U3^4o@ y`5}hy{7A2pLc8V?-c_mQd~&$Cqf^&>yDQur6mpcMTl^ElKM5fe;U8hWQU3x{fI`#& literal 0 HcmV?d00001 diff --git a/services/__pycache__/network.cpython-310.pyc b/services/__pycache__/network.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42c54038b6cd0095301770b562e1382863bc6c05 GIT binary patch literal 2158 zcmZ`)&5s*36t_JenS5>2E!__l&=e>lsO+{&sQ^`_(t>(wLE06EKozF3H<>n>Nj*+C z8%-ooDnSp0GpL8I#HD`%{{?4XIrU0!NPPV4$!?db_2@mn_iR5uzxT5zt=B6A?W@zL zd%p*S{9%-%jX}8r%Fcpu!fBthsO?#camJl{q~&mzd+1&6^AdWG(=E~ro?r#J-Z35y znhU`LZGy6kU^zUllAId8LZ%ElvpP6*N0CP~$1W#G?ijvI_Kk0M@SA=zTU1}G2V<%>v@Ijvwg}J_%Ysi zSlc&w`#cW-xZ{(bEK07%cY&!W?4w*^x>Fzvpz9dmL^2lH*>GM`%(@mQ@nAFvw}w%Y z#7Q^YmO_N@T-(}iT5nOpLn+iq9(B1B-xM+&0&N*wC5eDQjSU_U-Fr^6mkq2bpTy)3ohr)-Lta zPSnpXuN^LYX`r$JZ}PZd=3>c3up)wuXA#IHSkIW@Wy8!tr8%iIp>%)w*|gt2kqgjT z<;^0Mf=^0sUby(-#?1@OnmuQ$gqtT-dwd{FTAoNablX82#8xxiUZG92-xo<24}upZ z-M-L1e5g^Ty-wOsWphEZWURfbG8*=D5OLm4QZBTEMcR{CBJ9O3h6L{leR5D>rJc>O z(pJEV_RL_WUF2D5ACC(nlja50B`&&{fh|$+WTRQvegO|nueLlZYMN*oYkEslt{qfE zSC5oKyT-RXX0lT(JwNY@6gQbXi7~lim@WA7OA2eyl3f7{ynwCHz^Tzid~ZciX=oFay$z=D!{F>5ei`%E z!%=woW;8cF!&~7zbj%Op`U|0z88_xY_70Q|?!Ytbf1eBI%vsU2FqS0i- zg70GpSpoc|Bfy_O@a=3g7({aK1;B4Qtx7T)w2P9p7FBBg6Ge3jWVHe_z8mkv`1v^S zBtvZ0bY;_Qehy66&1k!qs%!{qRgXA%E?9k7y5~as2!1ki925gZKzaU_A3$yTI+#x# p%hzDAptGR0MY5BQY;u?u9b6iueAE}0&D&&M2PB{Y3xe8v{{dfS450u3 literal 0 HcmV?d00001 diff --git a/services/__pycache__/osm_city.cpython-310.pyc b/services/__pycache__/osm_city.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edb0d6304392de44f56583e1a2ea43c7571a0f1f GIT binary patch literal 1892 zcmZ`)&2Jk;6rY*>@W$VX-6T%G+z&ohf}|=4Arys*R=q$Iq#~+XRMy5bUaz&;-Oi31 z+uD_iQjS%*7O1ef^*`Xqi9cel98gYF^n!YzyjjO-4`oLCdtWp2-n{ol8=ap+A)H`p`hQ0$;oiKoP}0YT=BM7RjjA(lV;IbZSza8V_*Gp!g=TbBC}8Z6AJ* z5&BcH2tE6>qtCbu>wrc0uEH1p4a)?0NYIank^}>4PmBYMQKF@~I@!aiA#k50D8XHl z7zv@~LoGEEn0>9UPYjx)mP@9%fhOh@DN8>`7ij(=&UmeFB_=IwA{Wu(FF4J)C>GzatP|WeWFvLejFRP8O5g6sMKMnk*&dq|mRyS5J!l z6ZAv_WpufLR%cxGNVAe@N^~`;(8f>J6!LU;E3x=N|rq64CQUk+i!0VEe!1J)DuT zH1<6zdeWdQ5!r~N{g6vD7LFLi9d*WhsFRid7{2GL0N#}mKUNpJCjK;Q7b4RfDe07^T!dc(+L&$5qwSK(sfA7Ih z!(lez>N?xAAkPB(DqMbEiK=5hP|QUI%L>#Dvu(VgxHSbQ6`TUFt!$CZM_~+A4g<*f zD}Mai7M1i@`{csB;IX|bjfi`JkR%++T9-jRIE&j|9`3cH5DdHud!Zpm5tF(Xdx6wD zA*9O`VY|a)UQlMs6O8k4KbA(<4;>*jmkFs42C5kpekipbgN_(P5revOC!pb;G&{Wz z>L5+16$_}uLy6(!-Ea`lSZeWLPij63>^cy2s2>JAr~DZ)q@KxQM%rDcBSOwqV}fJa zZIx$z)ea+-mAF+>{djz5i9M-7u(Qx}(d>C;joB*9SG7Hw<+x=y_uTeyM#lUd@WIb1 zIBvD9c5?a6U7})&djQK=Cze*nYov&a*djWv5(`_HkUBOo!K;8k@1G)hSV^!Ba|tHN zX8s?&xQ5r%8K5_D8D>74Y8P0``b+&^UDIFk>);GpEqkEq9oeQdpmBpCw?UG*xaUO7 oAMuYs;;PfL9j{#{yO`=nX>IO>bl|fs^*&?uOSO=S*}`S=AA+>ohyVZp literal 0 HcmV?d00001 diff --git a/services/__pycache__/presets.cpython-310.pyc b/services/__pycache__/presets.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e009ef730a81ca49ea74bb7a3acb85685455c1f4 GIT binary patch literal 758 zcmZWn&2G~`5Z<*nsT(yYf{Ft|Xc0mp2byaS6)FPtm`hINV!7TWHF0*M-F1smkL6)_ z51ztDIOPR6a3FkRHzE=v%{M1vdlseDy3y+ zlq{`Tw9!f1?t`slo*9{`Pn8dC%hE22Tm~zBRRmev%1NWO^4*2a8MfGb-)^^!@W$S3 zo>BaR;{HdAFNcjnW}Yl;sc6oouP}}a-`F^6V$9MAn!z2Ss)gT^Nw!{3)}QM%FG8KF z0t04c8g2^XC%AfD%b-hI24d73GSjz)h0ze}+= literal 0 HcmV?d00001 diff --git a/services/__pycache__/random_city.cpython-310.pyc b/services/__pycache__/random_city.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51e1dea856c2fac70a1c90e22b2c585f222bf369 GIT binary patch literal 2453 zcmbtWOK;mo5MD0HCH3?pjvq;1LE3}R##UtKg$8bb1Vzz9S|Df(2rX2aOHo8AQeCc+ z*q~0%ALyxQkwe{EenS7so(lA&-is6l+H__q$+82Vx&}ux!`*L(^Udr!t<}l|#*c4Z z>HS$HKjq+&7p939PiHJi5uIK%=72YDf$(h* z)3FAM1P1`)SWCFOWcl(0JQsOs;<+&KtmZ5E#e9{QpPRr~<>44pI=%`*IH=u1K|%P);&fnD)ZlppPxdW{ zkmTf>55A@&V?=X<6Nn75A40wSY8)9k<#dC9G&j&sxy9`xsx_#WGa9aPaTUZ`eqQ>4 zeorwnfk8A(<$mV-!$A~q#lG}>FOdh!UnD#b@7zz5Op2i|qclLjR?X~uT7AQBmcLJ{oO1FD+d*>D?J2!8)_I4DMv!W5~nn_O%GqHdm ziy##11yR_O%6<}Y*;6(T2KeKraVnJ6jpE>ishIE*p7s^<-tq>6t-;g%jvvYW4v)e}LQBXeQIc(Cfp`oM*;WCmr)&=PRrzilWztXk zgWdN4B)bHH5msR@s|JI=vi_?Y)UaN%R|nU^APIyggU%dIhkMf7yc?&!7iYVmOc7tB zzXIr*{!c;C^j|qkKQ>=THT^(5&~HuXw>i_KBh`+k+I`L@M3*$ts{HXV(NuczLH%Kx zTu)BtLrz~QVY?G2+Cx-86;9ee!&{74bNhHkN; z-phn86uOY`_7DsO9)hYAB*VV$kj9#-M@f=C_Vkf5@TIED!0#o{;^BS=?MmiHLE;D6 zsP?_5o#_dLt&cfr*8r|Fi>O^fQ%6;(Id3 HH|pkpA<}SI literal 0 HcmV?d00001 diff --git a/services/com_con_city.py b/services/com_con_city.py new file mode 100644 index 0000000..c3b7237 --- /dev/null +++ b/services/com_con_city.py @@ -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' + } + diff --git a/services/layouts.py b/services/layouts.py new file mode 100644 index 0000000..3b00306 --- /dev/null +++ b/services/layouts.py @@ -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 diff --git a/services/network.py b/services/network.py new file mode 100644 index 0000000..4419bec --- /dev/null +++ b/services/network.py @@ -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), + } \ No newline at end of file diff --git a/services/osm_city.py b/services/osm_city.py new file mode 100644 index 0000000..88ac0fb --- /dev/null +++ b/services/osm_city.py @@ -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, + } diff --git a/services/presets.py b/services/presets.py new file mode 100644 index 0000000..f5685a1 --- /dev/null +++ b/services/presets.py @@ -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' diff --git a/services/random_city.py b/services/random_city.py new file mode 100644 index 0000000..60d5655 --- /dev/null +++ b/services/random_city.py @@ -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, + } + diff --git a/templates/pxy_city_digital_twins/_status_gauge.html b/templates/pxy_city_digital_twins/_status_gauge.html new file mode 100644 index 0000000..97c4b6f --- /dev/null +++ b/templates/pxy_city_digital_twins/_status_gauge.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/templates/pxy_city_digital_twins/city_digital_twin.html b/templates/pxy_city_digital_twins/city_digital_twin.html index da426db..443c1ee 100644 --- a/templates/pxy_city_digital_twins/city_digital_twin.html +++ b/templates/pxy_city_digital_twins/city_digital_twin.html @@ -3,7 +3,7 @@ - LDS City + Digital Twin City @@ -19,22 +19,33 @@ } }); - - + + + - + @@ -56,19 +67,13 @@ color="{{ building.color }}"> - - - - - - - + + {% include "pxy_city_digital_twins/_status_gauge.html" with ring_color="#00FFFF" offset_y="1.50" status=building.status id=building.id %} + + + {% endfor %} @@ -204,29 +209,6 @@ {% endfor %} - - - - - - - - - - - - - - - - - diff --git a/views.py b/views.py index ed589a8..d7cf6b6 100644 --- a/views.py +++ b/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), - } \ No newline at end of file