From dec841fe9b2629a0288a0843d36da9cdce24840f Mon Sep 17 00:00:00 2001 From: Sanjanaa-55 Date: Sun, 27 Jul 2025 12:40:53 +0530 Subject: [PATCH] fixed the issue #23 --- GSSoC_2k25.code-workspace | 10 + __pycache__/config.cpython-312.pyc | Bin 0 -> 2749 bytes app.py | 159 +- utils/__pycache__/air_quality.cpython-312.pyc | Bin 0 -> 1354 bytes utils/__pycache__/chatbot.cpython-312.pyc | Bin 0 -> 1372 bytes .../city_pulse_animation.cpython-312.pyc | Bin 0 -> 59513 bytes utils/__pycache__/crime.cpython-312.pyc | Bin 0 -> 1413 bytes utils/__pycache__/tourist.cpython-312.pyc | Bin 0 -> 2503 bytes utils/__pycache__/weather.cpython-312.pyc | Bin 0 -> 2267 bytes utils/city_pulse_animation.py | 1337 +++++++++++++++++ 10 files changed, 1484 insertions(+), 22 deletions(-) create mode 100644 GSSoC_2k25.code-workspace create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 utils/__pycache__/air_quality.cpython-312.pyc create mode 100644 utils/__pycache__/chatbot.cpython-312.pyc create mode 100644 utils/__pycache__/city_pulse_animation.cpython-312.pyc create mode 100644 utils/__pycache__/crime.cpython-312.pyc create mode 100644 utils/__pycache__/tourist.cpython-312.pyc create mode 100644 utils/__pycache__/weather.cpython-312.pyc create mode 100644 utils/city_pulse_animation.py diff --git a/GSSoC_2k25.code-workspace b/GSSoC_2k25.code-workspace new file mode 100644 index 00000000..6bffa336 --- /dev/null +++ b/GSSoC_2k25.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": { + "git.ignoreLimitWarning": true + } +} \ No newline at end of file diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3aac20577c91d3292952cff89253a3a8cb0c3016 GIT binary patch literal 2749 zcmZvddr(x@9mnqiyTI}Wc?c>Gkv9l}Eb`F1fIQ?WkHsiyJn&o3y|8;1@9v72#0Sxu zn3Q0n292h~84|QJjZ+(qP0bi>(`H&zHI{lr?#q1xwGw}P~ z&+nYy`JLbIoV$Mx3DI--{IGFYcs`KhZnBs8CqfnX;(a*oWlrRP0}c3q?{S}(G~fqX z(5c!V0w7S;dI*AGRfj+*gsD0lA|O)LQLq4_RUHGd5U1*e5Dy8eUIdF_iK-JJ36fQv z0!txP)yp6a(p8-SnUJOGY{-FJRp-HS$X9g%tboEfjk_s=l~AmD3{V2AR9y;XP_F6< zSPg4b&4UptRb2(uuvXPIPz!acu7`Edpz20wf@W2>Kr5_QbsMxphpIcl1YN4$0FS{& zRd0fBcwE&#fhSFir*nY4m)(VnhZ7`tNtspDb4i3dS zVVhzHVY_07V25I#gr^kyH0)IDF4(QuJus};XO4$XXf|>~do%aBwbf>eAks6wU%ilV zqMWCVhUbi%o7(wIAGfcCIPjlMa_gF$)*d3#2Rr+I=ZGrg>7{R8Ezc=w8RXp9rhSYW9lr|#uVrvx72AuNQTYI0n);{M^_H(m1{?aBM6PWG& z%i|3KrWRBwID3d>wht1S4sDoRd9|U4ryVyZUirNPCx)6tyFmH{GQiff(id;tP5kIh z>`L|Xmao3QiUIT{v5yRqK>~ypmtSlVCL%# zHm^e#w-E_sr`B|r;8qpI_Z5Cv#qeK&yf0n9SkMWMab7VAZ7=3<5 zDif+LiYPA6E#oO}`mMdY+j)wOENfnmr{fBo-rVtS2a4<2=7o1Fcv`gkJU@H@$DCY? zeM-fB=i)mi^_CvOSxO(iL-IC1{Et7@A=&)pwUoclAgMpGJMFRw*TQT6<+U^< zo;TiaFa#oD3116G^6#(l`u!G9pdY*1xn0=4sqMGZTv*=J3lH~y`y`TvKlxX@5{S4ghM7hiAq1&`8k>I%*CXz|sf z;*y?2<5avqqXC;uHYO`7z5#6 zF(Md<H12JABVi}0@0t*?4_W}tFEb;=28Cb$;gXi~}$Uu_Uh-3y* zegv~i8A#={kzP8&T&C#fr@*ptpc2B(b}OP@@4MXvBr#X@pCg$xvNTK%YD+~5kz_N-)} z*sDMW21>lZDh5h9ZP=Lqq`xaX-&4jwxmO+)46OFbV+{km#)}v=3{@g}6sAf+)rjW% zIlERtH5zVVlCsovT;l(?2`Neyr(>5WM`b!bS*h7{%wpxVJ+kxjqvDm_K8iO#K6;UI QSnm?H@SY&K7?1S-0NlmqUjP6A literal 0 HcmV?d00001 diff --git a/app.py b/app.py index 82ed4e24..f8d2cffd 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,7 @@ import streamlit as st import pandas as pd import datetime +import asyncio # Import your existing utility functions from utils.weather import get_current_weather, get_monthly_weather @@ -9,10 +10,39 @@ from utils.air_quality import get_air_quality from utils.crime import get_crime_news from utils.chatbot import search_google - +# Import the CityPulseAnimations class +from utils.city_pulse_animation import CityPulseAnimations +import random st.set_page_config(page_title="City Pulse", layout="wide") +@st.cache_data(ttl=300) # Cache for 5 minutes +def get_city_pulse_data(city, lat, lon): + """Get city pulse data synchronously for Streamlit""" + try: + # Set up API keys (replace with your actual keys) + api_keys = { + 'openweather': 'YOUR_OPENWEATHER_API_KEY_HERE', # Get from https://openweathermap.org/api + # 'weatherapi': 'YOUR_WEATHERAPI_KEY_HERE', # Optional + # 'aqicn': 'YOUR_AQICN_API_KEY_HERE' # Optional + } + + # Run the async function + async def fetch_data(): + async with CityPulseAnimations(api_keys) as city_pulse: + return await city_pulse.update_city_data(city, lat, lon) + + # Execute the async function + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + city_data = loop.run_until_complete(fetch_data()) + loop.close() + + return city_data + except Exception as e: + st.error(f"Error fetching city pulse data: {e}") + return None + st.title("City Pulse 🌆") city = st.selectbox("Select a City", list(CITY_COORDS.keys())) @@ -23,11 +53,95 @@ # --- Initialize chat history in session state --- if "messages" not in st.session_state: st.session_state.messages = [] + + # --- City Pulse Section --- + st.header(f"đŸŽ¯ City Pulse for {city}") + + city_pulse_data = get_city_pulse_data(city, lat, lon) + + if city_pulse_data: + pulse_params = city_pulse_data.get('pulse_params', {}) + + col1, col2, col3, col4 = st.columns(4) + + with col1: + st.metric("Pulse Color", pulse_params.get('color', '#00ff7f')) + + with col2: + st.metric("Pulse Speed", f"{pulse_params.get('speed', 1.0):.1f}x") + + with col3: + st.metric("Pulse Pattern", pulse_params.get('pattern', 'normal').title()) + + with col4: + st.metric("Pulse Intensity", f"{pulse_params.get('intensity', 0.7):.1f}") + + # Show pulse visualization + pulse_color = pulse_params.get('color', '#00ff7f') + pulse_speed = pulse_params.get('speed', 1.0) + pulse_intensity = pulse_params.get('intensity', 0.7) + pulse_pattern = pulse_params.get('pattern', 'normal') + + st.markdown(f""" +
+
+ â¤ī¸ +
+

+ Pattern: {pulse_pattern.title()} | Reflecting real-time city conditions +

+
+ + + """, unsafe_allow_html=True) + + # Show detailed pulse information + with st.expander("🔍 Pulse Details"): + weather_data = city_pulse_data.get('weather', {}) + current_weather = weather_data.get('current', {}) + air_quality = weather_data.get('air_quality', {}) + crime_data = city_pulse_data.get('crime', {}) + tourism_data = city_pulse_data.get('tourism', {}) + + st.write("**Pulse Factors:**") + st.write(f"đŸŒĄī¸ Temperature: {current_weather.get('temperature', 'N/A')}°C") + st.write(f"🏭 Air Quality Index: {air_quality.get('aqi', 'N/A')}") + st.write(f"🚨 Crime Risk Level: {crime_data.get('risk_level', 0.3):.1f}") + st.write(f"đŸŽ¯ Tourist Activity: {tourism_data.get('activity_level', 0.6):.1f}") + + st.info("💡 **How it works:** Color reflects air quality, speed varies with weather severity, intensity is influenced by crime data, and size changes with tourist activity levels.") + + else: + st.warning("âš ī¸ City Pulse data unavailable. Please check your API keys or try again later.") # Create tabs tabs = st.tabs(["Weather", "Air Quality", "Tourist Info", "Crime News", "Trends", "Find with City Pulse"]) - # --- Existing Tabs (No changes needed for these sections) --- + # --- Weather Tab --- with tabs[0]: st.header(f"Current Weather in {city}") weather = get_current_weather(city, lat, lon) @@ -53,6 +167,7 @@ }) st.dataframe(df_monthly) + # --- Air Quality Tab --- with tabs[1]: st.header(f"Air Quality in {city}") air_quality = get_air_quality(city) @@ -66,6 +181,7 @@ comp_df["Pollutant"] = comp_df["Pollutant"].str.upper() st.table(comp_df) + # --- Tourist Info Tab --- with tabs[2]: st.header(f"Tourist Recommendations in {city}") tourist_data = get_recommendations(city) @@ -84,6 +200,7 @@ else: st.info("No tourist places data available or could not be fetched.") + # --- Crime News Tab --- with tabs[3]: st.header(f"Recent Crime News in {city}") crime_news = get_crime_news(city) @@ -100,6 +217,7 @@ else: st.info("No crime news found.") + # --- Trends Tab --- with tabs[4]: st.header("How Popular is Your City? 📈") trends = tourist_data.get("trends", []) @@ -115,29 +233,26 @@ else: st.info("No trends data available.") - # --- Chatbot Tab --- - -with tabs[5]: - st.header("🤖 Search CityBot") - - if "search_history" not in st.session_state: - st.session_state.search_history = [] + with tabs[5]: + st.header("🤖 Search CityBot") - user_input = st.chat_input("Ask a question (e.g., top cafes, weekend events)") + if "search_history" not in st.session_state: + st.session_state.search_history = [] + + user_input = st.chat_input("Ask a question (e.g., top cafes, weekend events)") - if user_input: - st.session_state.search_history.append({"role": "user", "text": user_input}) + if user_input: + st.session_state.search_history.append({"role": "user", "text": user_input}) - with st.spinner("Searching Google..."): - answer = search_google(user_input) + with st.spinner("Searching Google..."): + answer = search_google(user_input) - st.session_state.search_history.append({"role": "bot", "text": answer}) - - # Display chat messages - for msg in st.session_state.search_history: - if msg["role"] == "user": - st.chat_message("user").markdown(msg["text"]) - else: - st.chat_message("assistant").markdown(msg["text"]) + st.session_state.search_history.append({"role": "bot", "text": answer}) + # Display chat messages + for msg in st.session_state.search_history: + if msg["role"] == "user": + st.chat_message("user").markdown(msg["text"]) + else: + st.chat_message("assistant").markdown(msg["text"]) \ No newline at end of file diff --git a/utils/__pycache__/air_quality.cpython-312.pyc b/utils/__pycache__/air_quality.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3638345fc6d287784a5bb86e53c81db860033ff8 GIT binary patch literal 1354 zcmah|&u<$=6rT02*PFGSjUD8u6>UtFi%YR7B7sJwQgN-O{756x3SwEZCZ1`$VZFPV z83!d=BMWf}92!o|p$CqjaNz&oVv8QEDV1Dt>Md0`RYGEB;)WDeWu$%c-Z%T+_r15X zzopYQ5bN*iH_h)10REK0^QDiR!z;qM2M|D-2dYQAR@GtvYwIHCzYE3owIi&mBmG-Y zHMX>Z@kBz4eDdZ}W%2XM)msad&+1o~Zqz@ntTaZit0Y-a5+BYBRFG;NEdnUU)S)y; zzpr;ZCmAKtxQZR6 zDix&!GNF*n{n#=nWEZzDG#Lx$OC=b(#URA~R~WJ;CM_5i1F}|H!$Ij{@$C|F32v|_ z@*@;zIf z!Br1)qXk``>u|&6b|YwofscJg3o+uzIupVa3?gp|w+P;7W6CJk*D&M8Iu)d8ZZt$y zo))8M1jzN*>TTk2TZHvd&k5-hOt~c;N&l}9|_Y1!`D9SemYEN2AS+1nVF{v zBWrd)`pX8+)Ln1K+xy_x(+|ckcdrke+}&s=`u^+g!sF!RU}CCws?c-J_pI|nJG*;v z=gnQ*w_op;2QNeiCU7cR_xjMu4xE?PfA6RH2jg=u)E_3&yI=GZXB5f%J?B!-x-@jA z_RPLB{iD`*&UF_g%Us_+y*Jyp-{_VF&BWv&cXlu8=Po>r>nZa9=yCHw(EbV@5Mojeh{&ieaw+ literal 0 HcmV?d00001 diff --git a/utils/__pycache__/chatbot.cpython-312.pyc b/utils/__pycache__/chatbot.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1a5f2f30ac0d70f2a3c147d090ae44f4ee145af GIT binary patch literal 1372 zcmZuwOH3O_7@mFl&BI_~Od7ULTCgi11UDrFr~q5kDgr7OMYu+2+1=O%d)Lg2ZEV>M zih4+;N+s$6P#QHyN953BdTTgU>cv57W34zuYA?CbIER+1QfFg3T>4A%&A0z!{{P$2 z{Niv}0kMB0x8onM0Q_!zG>vYMnum~GAOo37f(Vf52qiNST4uil5hhQ0xfe#_e4%eD zR1m}YsnDm=^o@bx8>uaSWHg(}<`ZjKm`Poe!n2|EH+T;yF|ujh(ab*Ei%bA~!%B1vNc2h< z03}v77g^b2RNe!!m2g{w+X;8n`F;K_{k5AVH6`|-p}E8rxiHAL$gTfB|2=S@4wDM% zbyW^r1h}O+$0Y#WC0=gB?M+XSZ=P$^N%S(&QR2Vg!;L*h<-iuR404pWeZJs}W1L1~ zLqoY-ZZM{4v7{oV6KGJl{wtq*9<)`gJy$P&W#!*XOh9CZs0PX$2QN^O^vaq>>d? zObE&(F%oWS8C4!EIA>o^BTOxZgdl_(91{xN!7WKiUm{6Z?Oz>KCGgLM+ z3c^TdVhYwCvZAx=NK*-F301eeIb3HD zhF+7-k@H~QI2ffh6_IpV#G-C$OscbnW}VkEIGw>dr63bgFVyXHX=W_CgcOLPNL1HF zRTNTUoaj_gJi4GN0hq`t(SU+Bu$GQWnu=j!En`SS(V5U^%L9u`bKzi=c#8y-Q!+s(LQ$<@N{4bh*9ad4I=L z?e6`)XTPVqBLCK@RF;-^(4PN+X@6pGsO&5Ue&~8QSDt+AdW?Sx{dlV~xp3rKJnB?x z4DE65@FzTQU)XUxKR5E+)mJt@>mPsGKVIpZsB}zL?2~`LbO6^)>aSOA!0vhpD9&GP z_dOaZZysDN_g7j+el?H$bHWh!DY{Ao-ky4E+6|uc_Dpw}pNyFaKV(}B?BEP^Tc)ou zhi^Nk{mkJYWpF=7(96PBQVfkJ0-Xj31RjDKK>&LRoSMf|nw&{0lkg(>G~OlZAwXJF bRE-Bz8zTHx_oZseaY&YeH?~wR^G#h9Q literal 0 HcmV?d00001 diff --git a/utils/__pycache__/city_pulse_animation.cpython-312.pyc b/utils/__pycache__/city_pulse_animation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f1474e431cf751998b91ac0f54648925b5a39d53 GIT binary patch literal 59513 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 literal 0 HcmV?d00001 diff --git a/utils/__pycache__/crime.cpython-312.pyc b/utils/__pycache__/crime.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92a410cbc46b70fc59407675ee2cb81aa79b96e9 GIT binary patch literal 1413 zcmah}&1)M+6rb5I?dsbfR5mWQY_)DhaSSbmG;ss|N=aRufRY9q6|vDyrB&sXGBdIx zd$$#(2b&g*55)>`iH|PsKhR4L%`q1jrF5D25XdPvC#Fzb3Z0cD5z>^r!*AaEy*KZR zVeW@Qe!%6Q#jo=}ivT?KjJ9yQDaRN1u?-Ahz#>>YJ>eo00D=Xn74zI-2>bBVToeuQ z8?Y!)(c9-B~z1YwefAZ}MrlP(jee|ua6g`?RR8bF32x9 zxoN05(5a#1fa9~38SvRv036l!6$1agVXH6AqqR5pKYsr!{DD{arMBM)?sRkrzWY=H zwl4(|fCk&Wz5#HbqZuI@X)|`GI}_$$R#@OK!O_0dQmu0Zu7F$O0$72`@cimtBxFii$NCass*b6dEh55XYSJQN0Rs_U zs7%e0#l#9OCME36grgpGd7yA$GQzlolPdNe6jL#}SwVynCgu=jQLLK;Wj-(A452!$ z5GEA}&&PW!BgMr)OL|_nC?fgvWed$PeLTWzdYd4RIn;pbAFEBf;l6rr zT{#F2)n*@sx(~Yt-QGU8d&up5?P)*>E47P9;fS0#>>1de*qYcC_bxu@d2d~H)#xAU z@Neqy12y4BVw=u}^X*sblG_v8KD%|c{_&pB=y`{SMY=c{8+ry5EwcWR8;owwZOpm- z@w)@J2X?PF`p4X%(YtH6*Y-Ya41M6n2DjI?)^^`-#FEeRPc^we{3nn@kt31kISPYl z?|R^`!$9vd0Of?+)wh$V>vu*EqQeJ~k>AykzmG)DpO8U5=l4@@PMrt8p4X;d7w<`- z>6m!03wnA?=9rXm4-aBOi7KsMZG?k+*BXW2qk|@F)-Z7ZR~i6951J literal 0 HcmV?d00001 diff --git a/utils/__pycache__/tourist.cpython-312.pyc b/utils/__pycache__/tourist.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3a668877eb87117673b0462eeb2e3019f1325fd GIT binary patch literal 2503 zcmai0O>7&-6`uX!?otx}O-m*fd21(zh+3poDNZCC0c47jQ|rf#q&OB>5Nqy;T$<#r zXIGX*kgiemptK1RpJXF0te_~WLO$fEHaQh_Q6Lu@GGJok00PoWZt_on!a;$)p}1D- zy6qfx-n@D5oAr+o;C#IAM-!Y|5ilk8@g}?*Akvgfv}2Hq%uWRV&)l!%50IH zXPYxO&#?$|1<7&Tn^kUu*2=80ayiT?h9I6wgE zjIy8*t>uz!Mcj;==0tgT5`^z6vYzj$jLNE<%3EyH4n7*AF?U=WE!Jv`Dpb)Y=+|tG z&7m6SMpdTZa%}hJDw|_eQI%A;>b}#q(XwEuHPqw9vC&f0_$vQ0zRhXSx4-CxkmEOs zetHt2G{jtU)r2Zn@cgH(<#TjZS0UiYd$WV(RMrYPdsU&z9)N|jqqg5-9hpHNyu~A6 zLUJS3TXU)y`Yb?+NfU#v|w! zt}3YqZm~1yFtYYJvA>E)v?i(@s^?#-Bsw^>+cYM})VT9-Lu=wy@q<6?&P&V^x&qoI z=A~#|{}WAmj&|lnv{+=S;=<*=xfv}FSHl+bSWz)CD{XToCVeA0T()AI&E|_XYgKFy z!Iu`XX_*;?AloiXh)ycq1%8AWtvA|K_?{uz-mTORWLAZfhd+% z&c!&=N$tdzp3pl{iL_EWyGs^4kIxf=a|MgOqX4nXWz0wDLu3b%b2GEklbN}h)H|7x z@zm6;?YXe171fNPEEjb}wSx|I8T~RQ8Ok$|STd|-U=>Iu@Y>pfVi*8#bEZWIeIINN z+-!I1N){V5eZo_v5`UB;tC zyNuJL*t`fOOlS47MYIcx&Eln#Z_l3ZJ3BWqlS+f8Xr@~gjvLE%2y)2~482hT4xmC1 z3>k#uX)(<|Lf>CQfAe)V+JlXD?{-JH;qSiRli1~$w&wc4!BNTlQir)*x zw?fBOq-|g4M^}G&wGlo1AbRv(^ypSJ{uuF|&~@=+_Tyy3zkk(p-Se>f;2Qfo@l)|P z?oXy3%7@pI>*A*qTXO#+7k4oD9pVBZ2+I@N2_mnb#^862G7ayKSGeuYAAL6X*^zp9 z=wYZAtb=VU!cz(D>s~#1{p1?AK6XDeup%`eh}!$LU;S)_-wq#Geee2vH?;NOR`}@p z%=(3Txc@P7xqU0CMn~|Y$@eE4U9a4B-*m4X+3JclqVnybn?v>2-rR}~H6jOZ$8N^f zPH#o}9*KO^`yFKBd&Ge=Fcmt`2zEgT9l{RcT*BX%q}_AoZc}{SHU>zkk}>vW2v$?VDeOHh=Q@{!OXgJyH*zst1xS+fnA3*$*|m zeTp4Df&TRBsX*!w`tu2Q>L7QQ52Pa8-H40IhumWVclU>ZF^2mKoFH&9$@V+l9B=h} z^S+TI0B5s0+!$yYT)y1A7li&sH9O|_=n06348Z4^bh4zYi$(kv8HOeOXJN+S16eT4 s*XXscknbzx-tPLr4VE%kKQy$}HC#vG9RY>M*g6V4;RhIj41vo(0sW0~D*ylh literal 0 HcmV?d00001 diff --git a/utils/__pycache__/weather.cpython-312.pyc b/utils/__pycache__/weather.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3626c40331b013fe18e7017fb7c7582f15acb157 GIT binary patch literal 2267 zcmb7FO>7fK6rRm`*MBk2A8~#PxS>RtKwOm;s(=YJKM)lN!U=)Mt+nh<;&t|q*>$Q| zj%pzeiCQVm30HuWV-c4gd+329QZIH>33q9wNIh{2q`e?j>YKGUK#KY^hi~@Hy!Yn4 z+4sHqAruNA7=KRR%-rS?`i%`%{Zoe{?oIG=W=<;|nId<`^yDqvpQBROBqVkppqJsRG z0&@p)Dl`DH7S(XZ9dwUhWUXz>tjFDXITyAwTl5q>S?YPmkUVq85h5OmlX#g)BGB^h zPyx}s!$q#ZN#0Iak$e=lCI4$upe_X+%|-`96th8q9mDkvzFyVeG%3u=BnX)z;9XGHxRa?0r82$P8GnQqT1M#?E$o)EO*40(m)X8jI z5-cJ9UjGO2`udtLR!H-`1PcWXnvb*dnsnD;S0>C7wQlpluElqLci-Q9)L) z6t(o|%$uUJJ*psUw#UM%VFzX~R?M^_&tcn_$*Hm=TMM?gellsGeuEM=MIsxPtb>^> z>RODmgVY=o!O9U#=(UkX3cDzT0b;!E9iO^DE6DZ{d?SZV%e2{9Y}rB60y8r$>Jqj+ zSrfc+mTV~)Z2TgepVkBw+bpCF0K06CDAb8*=$Z*8^ny%UEKz5?m>e2NO`4dPDO1q0 zf+j>&AtPuOHZ!Sl4G$A}9;b$}IcMoc3c`n*yPl)jn5m($iEBqLO`cDVq+mrcQ}xXI zjRm_Ef=$Kk8K=VF85p6V{e%ShudpP;%=%WMWaq%Z)~ke+=e&&*xOz4c9+Z_y*<^YNV#*M96nL@o!AJrt|XQdUknz9HzM7o(bdA* zXeH8DZSN`_TUFLhR@&dKwsn-+SI@6?RoV_$L1%3B^R*k5NbH*5l-$8SNfNUV1e`i*FxaabI1S&S%)U*)gF-s2XT ze_=4=_MJuO0RM@>4gbhsw!mgNC&Jf2^3I|Mv>IV7dS($Y-7}0nc?TrEz`cpL1-{+` zc&WyIUaIk)cX@nwyJs@!%U%z33*d4nf3(JB73w6TENpYx%&DqC7NRpmSEE;CGbbp~ zA)=d<;{Evvm(y zE%lXm4}5>@yMyJAlAA2!pJX;!#QA>rp{9=iOT=oZdFATz)vq#Z4eO!(#nEagTpZnK zjywl41(&$0Z`Vp_IaJ-~+Zn9vs z*@PNfiWkT}2s-As8+6mTGMleG$Aogx=Aq-j+f154AO|SWc0*{!_mN}j+{+8!NmZ9} z3O+@Sz!s&oIS2q*Gt3j@e}cllqP;((;H}U`pmnKdCAJ(Zx5d^2hl<{+zh$XqrE|Hn X+}gM9KV0OWd%_IA)e~V_oCE#>=AJ{N literal 0 HcmV?d00001 diff --git a/utils/city_pulse_animation.py b/utils/city_pulse_animation.py new file mode 100644 index 00000000..b2c2f7bc --- /dev/null +++ b/utils/city_pulse_animation.py @@ -0,0 +1,1337 @@ +import asyncio +import aiohttp +import json +import time +import math +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Tuple +import colorsys +import aiohttp_cors + +class CityPulseAnimations: + """ + Real-time city pulse visualization system that creates dynamic animations + based on weather, air quality, crime data, and tourist activity. + """ + + def __init__(self, api_keys: Dict[str, str]): + """ + Initialize the City Pulse system with API keys. + + Args: + api_keys: Dictionary containing API keys for different services + - 'openweather': OpenWeatherMap API key + - 'aqicn': Air Quality API key (optional, has free tier) + - 'weatherapi': WeatherAPI.com key (optional) + """ + self.api_keys = api_keys + self.session = None + self.city_data = {} + self.pulse_state = { + 'color': '#00ff7f', # Default green + 'speed': 1.0, # Pulse speed multiplier + 'intensity': 0.7, # Pulse intensity (0-1) + 'pattern': 'normal', # normal, erratic, calm, intense + 'size': 100, # Base size in pixels + 'glow': 0.5 # Glow effect intensity + } + + # Color mappings for different conditions + self.aqi_colors = { + 1: '#00e400', # Good - Green + 2: '#ffff00', # Fair - Yellow + 3: '#ff7e00', # Moderate - Orange + 4: '#ff0000', # Poor - Red + 5: '#8f3f97', # Very Poor - Purple + 6: '#7e0023' # Hazardous - Maroon + } + + self.weather_patterns = { + 'clear': {'speed': 0.8, 'pattern': 'calm'}, + 'clouds': {'speed': 1.0, 'pattern': 'normal'}, + 'rain': {'speed': 1.5, 'pattern': 'erratic'}, + 'thunderstorm': {'speed': 2.0, 'pattern': 'intense'}, + 'snow': {'speed': 0.6, 'pattern': 'calm'}, + 'mist': {'speed': 0.9, 'pattern': 'normal'}, + 'fog': {'speed': 0.7, 'pattern': 'calm'} + } + + async def __aenter__(self): + """Async context manager entry.""" + self.session = aiohttp.ClientSession() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self.session: + await self.session.close() + + async def get_weather_data(self, city: str, lat: float = None, lon: float = None) -> Dict: + """ + Fetch comprehensive weather data using multiple APIs for redundancy. + + Args: + city: City name + lat: Latitude (optional) + lon: Longitude (optional) + + Returns: + Dictionary containing weather data + """ + weather_data = {} + + try: + # Try OpenWeatherMap first (most comprehensive) + if 'openweather' in self.api_keys: + weather_data = await self._get_openweather_data(city, lat, lon) + + # Fallback to Open-Meteo (free, no API key required) + if not weather_data: + weather_data = await self._get_open_meteo_data(lat or 0, lon or 0) + + # Add WeatherAPI.com data if available + if 'weatherapi' in self.api_keys: + additional_data = await self._get_weatherapi_data(city) + weather_data.update(additional_data) + + except Exception as e: + print(f"Error fetching weather data: {e}") + # Return default data to keep the system running + weather_data = self._get_default_weather_data() + + return weather_data + + async def _get_openweather_data(self, city: str, lat: float = None, lon: float = None) -> Dict: + """Fetch data from OpenWeatherMap API.""" + api_key = self.api_keys['openweather'] + base_url = "https://api.openweathermap.org/data/2.5" + + # Get coordinates if not provided + if not lat or not lon: + geo_url = f"{base_url}/weather?q={city}&appid={api_key}&units=metric" + else: + geo_url = f"{base_url}/weather?lat={lat}&lon={lon}&appid={api_key}&units=metric" + + async with self.session.get(geo_url) as response: + if response.status == 200: + current_data = await response.json() + lat, lon = current_data['coord']['lat'], current_data['coord']['lon'] + + # Get detailed forecast + forecast_url = f"{base_url}/forecast?lat={lat}&lon={lon}&appid={api_key}&units=metric" + async with self.session.get(forecast_url) as forecast_response: + forecast_data = await forecast_response.json() if forecast_response.status == 200 else {} + + # Get air pollution data + air_url = f"{base_url}/air_pollution?lat={lat}&lon={lon}&appid={api_key}" + async with self.session.get(air_url) as air_response: + air_data = await air_response.json() if air_response.status == 200 else {} + + return self._process_openweather_data(current_data, forecast_data, air_data) + + return {} + + async def _get_open_meteo_data(self, lat: float, lon: float) -> Dict: + """Fetch data from Open-Meteo API (free, no key required).""" + base_url = "https://api.open-meteo.com/v1" + + # Current weather and forecast + weather_url = f"{base_url}/forecast?latitude={lat}&longitude={lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m&hourly=temperature_2m,relative_humidity_2m,precipitation_probability,weather_code&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto" + + # Air quality + air_url = f"https://air-quality-api.open-meteo.com/v1/air-quality?latitude={lat}&longitude={lon}¤t=us_aqi,pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone" + + weather_data = {} + air_quality_data = {} + + try: + async with self.session.get(weather_url) as response: + if response.status == 200: + weather_data = await response.json() + + async with self.session.get(air_url) as response: + if response.status == 200: + air_quality_data = await response.json() + + except Exception as e: + print(f"Error fetching Open-Meteo data: {e}") + + return self._process_open_meteo_data(weather_data, air_quality_data) + + async def _get_weatherapi_data(self, city: str) -> Dict: + """Fetch data from WeatherAPI.com.""" + api_key = self.api_keys['weatherapi'] + base_url = "https://api.weatherapi.com/v1" + + current_url = f"{base_url}/current.json?key={api_key}&q={city}&aqi=yes" + forecast_url = f"{base_url}/forecast.json?key={api_key}&q={city}&days=7&aqi=yes&alerts=yes" + + try: + async with self.session.get(forecast_url) as response: + if response.status == 200: + data = await response.json() + return self._process_weatherapi_data(data) + except Exception as e: + print(f"Error fetching WeatherAPI data: {e}") + + return {} + + def _process_openweather_data(self, current: Dict, forecast: Dict, air: Dict) -> Dict: + """Process OpenWeatherMap data into standardized format.""" + processed = { + 'current': { + 'temperature': current.get('main', {}).get('temp', 0), + 'feels_like': current.get('main', {}).get('feels_like', 0), + 'humidity': current.get('main', {}).get('humidity', 0), + 'pressure': current.get('main', {}).get('pressure', 1013), + 'description': current.get('weather', [{}])[0].get('description', ''), + 'main': current.get('weather', [{}])[0].get('main', ''), + 'icon': current.get('weather', [{}])[0].get('icon', ''), + 'wind_speed': current.get('wind', {}).get('speed', 0), + 'wind_direction': current.get('wind', {}).get('deg', 0), + 'clouds': current.get('clouds', {}).get('all', 0), + 'visibility': current.get('visibility', 10000) / 1000, # Convert to km + 'uv_index': 0 # Not available in current weather + }, + 'location': { + 'name': current.get('name', ''), + 'country': current.get('sys', {}).get('country', ''), + 'lat': current.get('coord', {}).get('lat', 0), + 'lon': current.get('coord', {}).get('lon', 0), + 'timezone': current.get('timezone', 0) + }, + 'forecast': [], + 'air_quality': {} + } + + # Process forecast data + if forecast and 'list' in forecast: + for item in forecast['list'][:40]: # 5 days * 8 (3-hour intervals) + processed['forecast'].append({ + 'datetime': datetime.fromtimestamp(item['dt']), + 'temperature': item['main']['temp'], + 'description': item['weather'][0]['description'], + 'main': item['weather'][0]['main'], + 'icon': item['weather'][0]['icon'], + 'humidity': item['main']['humidity'], + 'wind_speed': item['wind']['speed'], + 'precipitation': item.get('rain', {}).get('3h', 0) + item.get('snow', {}).get('3h', 0) + }) + + # Process air quality data + if air and 'list' in air: + air_info = air['list'][0] + processed['air_quality'] = { + 'aqi': air_info['main']['aqi'], + 'co': air_info['components']['co'], + 'no2': air_info['components']['no2'], + 'o3': air_info['components']['o3'], + 'so2': air_info['components']['so2'], + 'pm2_5': air_info['components']['pm2_5'], + 'pm10': air_info['components']['pm10'], + 'nh3': air_info['components']['nh3'] + } + + return processed + + def _process_open_meteo_data(self, weather: Dict, air: Dict) -> Dict: + """Process Open-Meteo data into standardized format.""" + if not weather or 'current' not in weather: + return {} + + current = weather['current'] + processed = { + 'current': { + 'temperature': current.get('temperature_2m', 0), + 'feels_like': current.get('apparent_temperature', 0), + 'humidity': current.get('relative_humidity_2m', 0), + 'pressure': current.get('pressure_msl', 1013), + 'description': self._weather_code_to_description(current.get('weather_code', 0)), + 'main': self._weather_code_to_main(current.get('weather_code', 0)), + 'wind_speed': current.get('wind_speed_10m', 0), + 'wind_direction': current.get('wind_direction_10m', 0), + 'clouds': current.get('cloud_cover', 0), + 'visibility': 10, # Default value + 'precipitation': current.get('precipitation', 0) + }, + 'forecast': [], + 'air_quality': {} + } + + # Process air quality if available + if air and 'current' in air: + air_current = air['current'] + processed['air_quality'] = { + 'aqi': air_current.get('us_aqi', 1), + 'pm2_5': air_current.get('pm2_5', 0), + 'pm10': air_current.get('pm10', 0), + 'co': air_current.get('carbon_monoxide', 0), + 'no2': air_current.get('nitrogen_dioxide', 0), + 'so2': air_current.get('sulphur_dioxide', 0), + 'o3': air_current.get('ozone', 0) + } + + return processed + + def _process_weatherapi_data(self, data: Dict) -> Dict: + """Process WeatherAPI.com data into standardized format.""" + current = data.get('current', {}) + location = data.get('location', {}) + + processed = { + 'current': { + 'temperature': current.get('temp_c', 0), + 'feels_like': current.get('feelslike_c', 0), + 'humidity': current.get('humidity', 0), + 'pressure': current.get('pressure_mb', 1013), + 'description': current.get('condition', {}).get('text', ''), + 'wind_speed': current.get('wind_kph', 0) / 3.6, # Convert to m/s + 'wind_direction': current.get('wind_degree', 0), + 'clouds': current.get('cloud', 0), + 'visibility': current.get('vis_km', 10), + 'uv_index': current.get('uv', 0) + }, + 'location': { + 'name': location.get('name', ''), + 'country': location.get('country', ''), + 'lat': location.get('lat', 0), + 'lon': location.get('lon', 0) + }, + 'air_quality': {} + } + + # Air quality data + if 'air_quality' in current: + aq = current['air_quality'] + processed['air_quality'] = { + 'co': aq.get('co', 0), + 'no2': aq.get('no2', 0), + 'o3': aq.get('o3', 0), + 'so2': aq.get('so2', 0), + 'pm2_5': aq.get('pm2_5', 0), + 'pm10': aq.get('pm10', 0), + 'us_epa_index': aq.get('us-epa-index', 1), + 'gb_defra_index': aq.get('gb-defra-index', 1) + } + + return processed + + def _weather_code_to_description(self, code: int) -> str: + """Convert WMO weather codes to descriptions.""" + descriptions = { + 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", + 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", + 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", + 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", + 66: "Light freezing rain", 67: "Heavy freezing rain", + 71: "Slight snow fall", 73: "Moderate snow fall", 75: "Heavy snow fall", + 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", + 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", + 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail" + } + return descriptions.get(code, "Unknown") + + def _weather_code_to_main(self, code: int) -> str: + """Convert WMO weather codes to main categories.""" + if code == 0 or code == 1: + return "Clear" + elif code <= 3: + return "Clouds" + elif code <= 48: + return "Mist" + elif code <= 67: + return "Rain" + elif code <= 86: + return "Snow" + elif code >= 95: + return "Thunderstorm" + return "Unknown" + + def _get_default_weather_data(self) -> Dict: + """Return default weather data when APIs fail.""" + return { + 'current': { + 'temperature': 25, + 'feels_like': 25, + 'humidity': 60, + 'pressure': 1013, + 'description': 'Clear sky', + 'main': 'Clear', + 'wind_speed': 3, + 'wind_direction': 180, + 'clouds': 20, + 'visibility': 10 + }, + 'air_quality': {'aqi': 2, 'pm2_5': 15, 'pm10': 25}, + 'location': {'name': 'Unknown', 'country': 'Unknown'} + } + + async def get_crime_data(self, lat: float, lon: float) -> Dict: + """ + Fetch crime data for the location. + Note: This is a placeholder implementation. Real crime APIs often require + specific municipal or government API access. + """ + try: + # Simulated crime data - replace with actual API calls + # You would integrate with local police APIs, CrimeoMeter, or similar services + base_risk = 0.3 # Base crime risk level (0-1) + + # Simulate time-based crime variations + current_hour = datetime.now().hour + time_multiplier = 1.2 if 20 <= current_hour or current_hour <= 6 else 0.8 + + crime_data = { + 'risk_level': min(base_risk * time_multiplier, 1.0), + 'incidents_24h': int(base_risk * 10), + 'trend': 'stable', # increasing, decreasing, stable + 'categories': { + 'theft': 0.4, + 'assault': 0.2, + 'vandalism': 0.3, + 'other': 0.1 + } + } + + return crime_data + + except Exception as e: + print(f"Error fetching crime data: {e}") + return {'risk_level': 0.3, 'incidents_24h': 3, 'trend': 'stable'} + + async def get_tourist_activity(self, city: str) -> Dict: + """ + Estimate tourist activity based on various factors. + This is a simplified implementation - real systems might use + Google Places API, social media APIs, or tourism board data. + """ + try: + # Simulate tourist activity based on time and season + now = datetime.now() + + # Seasonal multiplier + seasonal_multiplier = { + 12: 0.8, 1: 0.6, 2: 0.7, # Winter + 3: 0.9, 4: 1.1, 5: 1.2, # Spring + 6: 1.4, 7: 1.5, 8: 1.3, # Summer + 9: 1.1, 10: 1.0, 11: 0.9 # Fall + }.get(now.month, 1.0) + + # Daily multiplier + daily_multiplier = 1.3 if now.weekday() >= 5 else 1.0 # Weekend boost + + # Hourly pattern + hourly_pattern = { + range(6, 9): 0.7, # Early morning + range(9, 12): 1.1, # Morning + range(12, 17): 1.4, # Afternoon + range(17, 21): 1.2, # Evening + range(21, 24): 0.9, # Night + range(0, 6): 0.3 # Late night + } + + hour_multiplier = 1.0 + for time_range, multiplier in hourly_pattern.items(): + if now.hour in time_range: + hour_multiplier = multiplier + break + + base_activity = 0.6 + activity_level = base_activity * seasonal_multiplier * daily_multiplier * hour_multiplier + activity_level = min(activity_level, 1.0) + + tourist_data = { + 'activity_level': activity_level, + 'hotspots_active': int(activity_level * 15), + 'peak_hours': [10, 11, 14, 15, 16], + 'seasonal_trend': 'high' if seasonal_multiplier > 1.2 else 'medium' if seasonal_multiplier > 0.8 else 'low' + } + + return tourist_data + + except Exception as e: + print(f"Error calculating tourist activity: {e}") + return {'activity_level': 0.6, 'hotspots_active': 8, 'seasonal_trend': 'medium'} + + def calculate_pulse_parameters(self, weather_data: Dict, crime_data: Dict, tourist_data: Dict) -> Dict: + """ + Calculate pulse visualization parameters based on all data sources. + + Returns: + Dictionary with pulse parameters: color, speed, intensity, pattern, size, glow + """ + pulse_params = self.pulse_state.copy() + + # 1. Determine color based on air quality (primary factor) + air_quality = weather_data.get('air_quality', {}) + aqi = air_quality.get('aqi', air_quality.get('us_epa_index', 2)) + + if aqi <= 2: + base_color = '#00ff7f' # Good - Green + elif aqi == 3: + base_color = '#ffff00' # Moderate - Yellow + elif aqi == 4: + base_color = '#ff7e00' # Poor - Orange + else: + base_color = '#ff0000' # Very Poor/Hazardous - Red + + pulse_params['color'] = base_color + + # 2. Adjust speed based on weather severity + weather_main = weather_data.get('current', {}).get('main', '').lower() + weather_config = self.weather_patterns.get(weather_main, {'speed': 1.0, 'pattern': 'normal'}) + + speed = weather_config['speed'] + + # Wind speed affects pulse speed + wind_speed = weather_data.get('current', {}).get('wind_speed', 0) + speed *= (1 + wind_speed / 50) # Normalize wind effect + + # Temperature extremes affect speed + temp = weather_data.get('current', {}).get('temperature', 20) + if temp > 35 or temp < -10: # Extreme temperatures + speed *= 1.3 + + pulse_params['speed'] = min(speed, 3.0) # Cap at 3x speed + + # 3. Adjust intensity based on crime data + crime_risk = crime_data.get('risk_level', 0.3) + base_intensity = 0.7 + + # Higher crime increases intensity (more urgent pulse) + intensity_multiplier = 1 + (crime_risk * 0.5) + pulse_params['intensity'] = min(base_intensity * intensity_multiplier, 1.0) + + # 4. Adjust pattern based on weather and crime + if weather_main in ['thunderstorm', 'tornado']: + pulse_params['pattern'] = 'intense' + elif crime_risk > 0.7: + pulse_params['pattern'] = 'erratic' + elif weather_main in ['clear', 'snow'] and crime_risk < 0.3: + pulse_params['pattern'] = 'calm' + else: + pulse_params['pattern'] = weather_config.get('pattern', 'normal') + + # 5. Adjust size based on tourist activity + tourist_activity = tourist_data.get('activity_level', 0.6) + base_size = 100 + size_multiplier = 0.8 + (tourist_activity * 0.4) # 0.8x to 1.2x size + pulse_params['size'] = int(base_size * size_multiplier) + + # 6. Adjust glow based on weather conditions + clouds = weather_data.get('current', {}).get('clouds', 50) + visibility = weather_data.get('current', {}).get('visibility', 10) + + glow_base = 0.5 + if visibility < 5: # Poor visibility increases glow + glow_base += 0.3 + if clouds > 80: # Heavy clouds reduce glow + glow_base -= 0.2 + + pulse_params['glow'] = max(min(glow_base, 1.0), 0.1) + + return pulse_params + + def generate_css_animation(self, pulse_params: Dict) -> str: + """ + Generate CSS animations based on pulse parameters. + + Returns: + CSS string with keyframe animations + """ + color = pulse_params['color'] + speed = pulse_params['speed'] + intensity = pulse_params['intensity'] + pattern = pulse_params['pattern'] + size = pulse_params['size'] + glow = pulse_params['glow'] + + # Convert hex color to RGB for glow effects + rgb = tuple(int(color[i:i+2], 16) for i in (1, 3, 5)) + glow_color = f"rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, {glow})" + + # Base animation duration (inverse of speed) + duration = max(2.0 / speed, 0.5) + + # Pattern-specific keyframes + if pattern == 'calm': + keyframes = f""" + @keyframes cityPulse {{ + 0%, 100% {{ + transform: scale(1); + box-shadow: 0 0 {int(20 * glow)}px {glow_color}; + }} + 50% {{ + transform: scale({1 + intensity * 0.1}); + box-shadow: 0 0 {int(40 * glow)}px {glow_color}; + }} + }} + """ + elif pattern == 'erratic': + keyframes = f""" + @keyframes cityPulse {{ + 0% {{ + transform: scale(1); + box-shadow: 0 0 {int(20 * glow)}px {glow_color}; + }} + 25% {{ + transform: scale({1 + intensity * 0.2}); + box-shadow: 0 0 {int(60 * glow)}px {glow_color}; + }} + 50% {{ + transform: scale({1 + intensity * 0.05}); + box-shadow: 0 0 {int(30 * glow)}px {glow_color}; + }} + 75% {{ + transform: scale({1 + intensity * 0.15}); + box-shadow: 0 0 {int(50 * glow)}px {glow_color}; + }} + 100% {{ + transform: scale(1); + box-shadow: 0 0 {int(20 * glow)}px {glow_color}; + }} + }} + """ + elif pattern == 'intense': + keyframes = f""" + @keyframes cityPulse {{ + 0%, 100% {{ + transform: scale(1); + box-shadow: 0 0 {int(30 * glow)}px {glow_color}; + }} + 50% {{ + transform: scale({1 + intensity * 0.3}); + box-shadow: 0 0 {int(80 * glow)}px {glow_color}; + }} + }} + @keyframes cityPulseGlow {{ + 0%, 50%, 100% {{ opacity: 1; }} + 25%, 75% {{ opacity: 0.7; }} + }} + """ + else: # normal + keyframes = f""" + @keyframes cityPulse {{ + 0%, 100% {{ + transform: scale(1); + box-shadow: 0 0 {int(25 * glow)}px {glow_color}; + }} + 50% {{ + transform: scale({1 + intensity * 0.15}); + box-shadow: 0 0 {int(50 * glow)}px {glow_color}; + }} + }} + """ + + # Main pulse element styles + pulse_styles = f""" + .city-pulse {{ + width: {size}px; + height: {size}px; + background: radial-gradient(circle, {color} 0%, rgba({rgb[0]}, {rgb[1]}, {rgb[2]}, 0.8) 70%, transparent 100%); + border-radius: 50%; + animation: cityPulse {duration:.1f}s ease-in-out infinite; + position: relative; + margin: 20px auto; + }} + + .city-pulse::before {{ + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: {int(size * 0.6)}px; + height: {int(size * 0.6)}px; + background: {color}; + border-radius: 50%; + opacity: 0.9; + }} + + .city-pulse::after {{ + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: {int(size * 1.5)}px; + height: {int(size * 1.5)}px; + border: 2px solid {color}; + border-radius: 50%; + opacity: 0.3; + animation: cityPulse {duration * 1.5:.1f}s ease-in-out infinite reverse; + }} + """ + + return keyframes + pulse_styles + + def generate_javascript_controller(self, pulse_params: Dict) -> str: + """ + Generate JavaScript code to control real-time pulse updates. + + Returns: + JavaScript string for real-time control + """ + return f""" + class CityPulseController {{ + constructor() {{ + this.isActive = true; + this.updateInterval = 60000; // Update every minute + this.pulseElement = null; + this.heartIcon = null; + this.lastUpdate = 0; + this.currentParams = {json.dumps(pulse_params)}; + }} + + init() {{ + this.pulseElement = document.querySelector('.city-pulse'); + this.heartIcon = document.querySelector('.pulse-heart'); + this.startRealTimeUpdates(); + this.addEventListeners(); + }} + + startRealTimeUpdates() {{ + setInterval(() => {{ + if (this.isActive) {{ + this.fetchAndUpdatePulse(); + }} + }}, this.updateInterval); + }} + + async fetchAndUpdatePulse() {{ + try {{ + // This would call your Python backend API + const response = await fetch('/api/city-pulse-update'); + const newParams = await response.json(); + this.updatePulseAnimation(newParams); + }} catch (error) {{ + console.log('Failed to update pulse data:', error); + }} + }} + + updatePulseAnimation(params) {{ + if (!this.pulseElement) return; + + // Update CSS custom properties for smooth transitions + document.documentElement.style.setProperty('--pulse-color', params.color); + document.documentElement.style.setProperty('--pulse-size', params.size + 'px'); + document.documentElement.style.setProperty('--pulse-speed', params.speed + 's'); + document.documentElement.style.setProperty('--pulse-intensity', params.intensity); + document.documentElement.style.setProperty('--pulse-glow', params.glow); + + // Apply new animation class based on pattern + this.pulseElement.className = `city-pulse pulse-${{params.pattern}}`; + + this.currentParams = params; + this.lastUpdate = Date.now(); + }} + + addEventListeners() {{ + // Pause/resume on click + if (this.pulseElement) {{ + this.pulseElement.addEventListener('click', () => {{ + this.togglePulse(); + }}); + }} + + // Handle visibility changes + document.addEventListener('visibilitychange', () => {{ + this.isActive = !document.hidden; + }}); + }} + + togglePulse() {{ + this.isActive = !this.isActive; + if (this.pulseElement) {{ + this.pulseElement.style.animationPlayState = this.isActive ? 'running' : 'paused'; + }} + }} + + // Simulate real-time data changes for demo + simulateDataChanges() {{ + const patterns = ['normal', 'calm', 'erratic', 'intense']; + const colors = ['#00ff7f', '#ffff00', '#ff7e00', '#ff0000']; + + setInterval(() => {{ + const randomPattern = patterns[Math.floor(Math.random() * patterns.length)]; + const randomColor = colors[Math.floor(Math.random() * colors.length)]; + const randomSpeed = 0.5 + Math.random() * 2; + const randomIntensity = 0.5 + Math.random() * 0.5; + + this.updatePulseAnimation({{ + color: randomColor, + speed: randomSpeed, + intensity: randomIntensity, + pattern: randomPattern, + size: this.currentParams.size, + glow: this.currentParams.glow + }}); + }}, 10000); // Change every 10 seconds for demo + }} + }} + + // Initialize when DOM is loaded + document.addEventListener('DOMContentLoaded', () => {{ + const pulseController = new CityPulseController(); + pulseController.init(); + + // Enable simulation mode for demo (remove in production) + // pulseController.simulateDataChanges(); + }}); + """ + + async def update_city_data(self, city: str, lat: float = None, lon: float = None) -> Dict: + """ + Main method to fetch all city data and update pulse parameters. + + Args: + city: City name + lat: Latitude (optional) + lon: Longitude (optional) + + Returns: + Complete city data with pulse parameters + """ + # Fetch all data concurrently for better performance + tasks = [ + self.get_weather_data(city, lat, lon), + self.get_crime_data(lat or 0, lon or 0), + self.get_tourist_activity(city) + ] + + try: + weather_data, crime_data, tourist_data = await asyncio.gather(*tasks) + except Exception as e: + print(f"Error fetching city data: {e}") + # Use fallback data + weather_data = self._get_default_weather_data() + crime_data = {'risk_level': 0.3, 'incidents_24h': 3, 'trend': 'stable'} + tourist_data = {'activity_level': 0.6, 'hotspots_active': 8, 'seasonal_trend': 'medium'} + + # Calculate pulse parameters + pulse_params = self.calculate_pulse_parameters(weather_data, crime_data, tourist_data) + + # Store updated data + self.city_data = { + 'city': city, + 'timestamp': datetime.now().isoformat(), + 'weather': weather_data, + 'crime': crime_data, + 'tourism': tourist_data, + 'pulse_params': pulse_params + } + + return self.city_data + + def generate_complete_html(self, city_data: Dict) -> str: + """ + Generate complete HTML page with real-time city pulse visualization. + + Args: + city_data: Complete city data dictionary + + Returns: + Complete HTML string + """ + pulse_params = city_data.get('pulse_params', self.pulse_state) + weather = city_data.get('weather', {}).get('current', {}) + location = city_data.get('weather', {}).get('location', {}) + air_quality = city_data.get('weather', {}).get('air_quality', {}) + + css_animations = self.generate_css_animation(pulse_params) + js_controller = self.generate_javascript_controller(pulse_params) + + # Get AQI level description + aqi = air_quality.get('aqi', air_quality.get('us_epa_index', 2)) + aqi_descriptions = { + 1: "Good", 2: "Fair", 3: "Moderate", + 4: "Poor", 5: "Very Poor", 6: "Hazardous" + } + aqi_desc = aqi_descriptions.get(aqi, "Unknown") + + # Status text based on multiple factors + temp = weather.get('temperature', 0) + humidity = weather.get('humidity', 0) + description = weather.get('description', 'Unknown').title() + + status_parts = [] + if aqi <= 2: + status_parts.append("Healthy") + elif aqi >= 4: + status_parts.append("Unhealthy") + + if temp > 30: + status_parts.append("Hot") + elif temp < 5: + status_parts.append("Cold") + + if humidity > 80: + status_parts.append("Humid") + elif humidity < 30: + status_parts.append("Dry") + + pulse_status = f"({aqi_desc} AQI: {aqi}), {description}, Intensity: {pulse_params['pattern'].title()}, Pattern: {pulse_params['pattern'].title()}" + + html_template = f""" + + + + + + City Pulse - {location.get('name', city_data.get('city', 'Unknown'))} + + + +
+

{location.get('name', city_data.get('city', 'Unknown')).upper()}

+ +
+ +
+

{location.get('name', city_data.get('city', 'Unknown'))}

+

{pulse_status}

+ +
+
â¤ī¸
+
+ +
+ + + +
+
+ +
+
+
đŸŒĄī¸
+

Temperature

+
{weather.get('temperature', 0):.1f}
+
°C
+
+ +
+
💧
+

Feels Like

+
{weather.get('feels_like', 0):.1f}
+
°C
+
+ +
+
💨
+

Humidity

+
{weather.get('humidity', 0)}
+
%
+
+ +
+
đŸŒŦī¸
+

Wind Speed

+
{weather.get('wind_speed', 0):.1f}
+
m/s
+
+ +
+
🏭
+

Air Quality

+
{aqi_desc}
+
AQI: {aqi}
+
+ +
+
đŸ‘ī¸
+

Visibility

+
{weather.get('visibility', 10):.1f}
+
km
+
+
+ +
+ City Pulse Legend:
+ đŸŸĸ Green: Good air quality, calm conditions
+ 🟡 Yellow: Moderate air quality, normal activity
+ 🟠 Orange: Poor air quality, increased activity
+ 🔴 Red: Unhealthy conditions, high activity

+ + Pulse Patterns:
+ Calm: Gentle, slow pulse â€ĸ Normal: Steady rhythm
+ Erratic: Irregular beats â€ĸ Intense: Fast, strong pulse

+ + Speed varies with weather severity â€ĸ Size reflects tourist activity
+ Intensity influenced by crime data â€ĸ Updates every minute +
+ + + + + """ + + return html_template + + async def run_city_pulse_server(self, city: str = "Itanagar", port: int = 8501): + """ + Run a simple web server to serve the city pulse visualization. + This method can be called to start the server. + """ + from aiohttp import web + import aiohttp_cors + + async def handle_city_pulse(request): + city_param = request.query.get('city', city) + + # Get city coordinates (you might want to add a geocoding service) + city_coords = { + 'itanagar': (27.0844, 93.6053), + 'mumbai': (19.0760, 72.8777), + 'delhi': (28.6139, 77.2090), + 'bangalore': (12.9716, 77.5946), + 'kolkata': (22.5726, 88.3639), + 'chennai': (13.0827, 80.2707), + 'hyderabad': (17.3850, 78.4867), + 'pune': (18.5204, 73.8567) + } + + lat, lon = city_coords.get(city_param.lower(), (27.0844, 93.6053)) + + city_data = await self.update_city_data(city_param, lat, lon) + html_content = self.generate_complete_html(city_data) + + return web.Response(text=html_content, content_type='text/html') + + async def handle_api_update(request): + city_param = request.query.get('city', city) + city_coords = { + 'itanagar': (27.0844, 93.6053), + 'mumbai': (19.0760, 72.8777), + 'delhi': (28.6139, 77.2090), + 'bangalore': (12.9716, 77.5946), + 'kolkata': (22.5726, 88.3639), + 'chennai': (13.0827, 80.2707), + 'hyderabad': (17.3850, 78.4867), + 'pune': (18.5204, 73.8567) + } + + lat, lon = city_coords.get(city_param.lower(), (27.0844, 93.6053)) + city_data = await self.update_city_data(city_param, lat, lon) + + return web.json_response(city_data['pulse_params']) + + app = web.Application() + + # Setup CORS + cors = aiohttp_cors.setup(app, defaults={ + "*": aiohttp_cors.ResourceOptions( + allow_credentials=True, + expose_headers="*", + allow_headers="*", + allow_methods="*" + ) + }) + + # Routes + app.router.add_get('/', handle_city_pulse) + app.router.add_get('/api/city-pulse-update', handle_api_update) + + # Add CORS to all routes + for route in list(app.router.routes()): + cors.add(route) + + print(f"đŸŽ¯ City Pulse Server starting on http://localhost:{port}") + print(f"🌍 Default city: {city}") + print(f"🔄 Real-time updates enabled") + print(f"📊 Multi-source data integration active") + + return app + + +# Example usage and API key setup +def get_api_keys(): + """ + Set up your API keys here. Get free keys from: + + 1. OpenWeatherMap: https://openweathermap.org/api + - Free tier: 1000 calls/day + - Provides weather + air quality data + + 2. WeatherAPI.com: https://www.weatherapi.com/ + - Free tier: 1M calls/month + - Comprehensive weather data + + 3. Air Quality APIs: + - AQICN: https://aqicn.org/api/ (free with registration) + - Open-Meteo: https://open-meteo.com/ (completely free, no key needed) + + Note: The system works with fallbacks, so you can start with just one API key + or even none (using Open-Meteo free service). + """ + return { + 'openweather': 'YOUR_OPENWEATHER_API_KEY_HERE', + 'weatherapi': 'YOUR_WEATHERAPI_KEY_HERE', # Optional + 'aqicn': 'YOUR_AQICN_API_KEY_HERE' # Optional + } + + +async def main(): + """ + Main function to run the City Pulse system. + """ + api_keys = get_api_keys() + + async with CityPulseAnimations(api_keys) as city_pulse: + # Test the system with Itanagar + print("🚀 Testing City Pulse system...") + + city_data = await city_pulse.update_city_data("Itanagar", 27.0844, 93.6053) + + print(f"✅ Data fetched for {city_data['city']}") + print(f"đŸŒĄī¸ Current temperature: {city_data['weather']['current']['temperature']}°C") + print(f"🎨 Pulse color: {city_data['pulse_params']['color']}") + print(f"⚡ Pulse speed: {city_data['pulse_params']['speed']:.1f}x") + print(f"🔮 Pulse pattern: {city_data['pulse_params']['pattern']}") + + # Generate HTML file + html_content = city_pulse.generate_complete_html(city_data) + + with open('city_pulse_demo.html', 'w', encoding='utf-8') as f: + f.write(html_content) + + print("📄 Generated city_pulse_demo.html - Open this file in your browser!") + + # Optionally start the server + # app = await city_pulse.run_city_pulse_server("Itanagar", 8501) + # web.run_app(app, host='localhost', port=8501) + + +if __name__ == "__main__": + # Run the main function + asyncio.run(main()) \ No newline at end of file