Spaces:
Running
Running
Download solar_engine.js from Asem75/Islamic_clock: direct link, hf CLI and curl.
- Browser
- Download file 7.13 kB
-
https://huggingface.co/spaces/Asem75/Islamic_clock/resolve/main/solar_engine.js
- Command line
-
hf download hf://spaces/Asem75/Islamic_clock/solar_engine.js
-
curl -L -o solar_engine.js https://huggingface.co/spaces/Asem75/Islamic_clock/resolve/main/solar_engine.js
7.13 kB
| // Astronomy Engine 2.1.19 API adapter. UTC is used internally. | |
| // All display/ruling thresholds are read from fiqh_time_registry.json. | |
| globalThis.SolarEngine = (() => { | |
| const H=3600000, D=24*H, rad=Math.PI/180; | |
| const ready=()=>Boolean(globalThis.Astronomy?.Equator&&globalThis.Astronomy?.Horizon); | |
| function position(ms,loc){ | |
| if(!ready())return null; | |
| const A=globalThis.Astronomy,date=new Date(ms),observer=new A.Observer(loc.lat,loc.lng,loc.height||0); | |
| const equator=A.Equator(A.Body.Sun,date,observer,true,true); | |
| // No extra refraction here: the registry's -0.833 degree horizon already | |
| // incorporates the standard apparent limb/refraction allowance. | |
| const horizon=A.Horizon(date,observer,equator.ra,equator.dec,null); | |
| return {decl:equator.dec,ra:equator.ra,altitude:horizon.altitude,azimuth:horizon.azimuth}; | |
| } | |
| function altitude(ms,loc){ | |
| return position(ms,loc)?.altitude??null; | |
| } | |
| function maximum(day,loc){ | |
| const guess=day+12*H-loc.lng/15*H;let lo=guess-6*H,hi=guess+6*H; | |
| for(let i=0;i<60;i++){const a=lo+(hi-lo)/3,b=hi-(hi-lo)/3;if(altitude(a,loc)<altitude(b,loc))lo=a;else hi=b;} | |
| return (lo+hi)/2; | |
| } | |
| // Resolve the sign change of the solar altitude rate. The bracket width is | |
| // numerical uncertainty, never a religious duration or an offset after noon. | |
| function transitTransition(day,loc,solver){ | |
| const tolerance=Number(solver?.time_tolerance_ms),sample=Number(solver?.derivative_sample_ms); | |
| if(!(tolerance>0&&sample>0))return null; | |
| const guess=day+12*H-loc.lng/15*H; | |
| const rate=t=>altitude(t+sample/2,loc)-altitude(t-sample/2,loc); | |
| let lo=guess-6*H,hi=guess+6*H; | |
| if(!(rate(lo)>0&&rate(hi)<0))return null; | |
| for(let i=0;i<80&&hi-lo>tolerance;i++){ | |
| const mid=(lo+hi)/2;if(rate(mid)<0)hi=mid;else lo=mid; | |
| } | |
| if(!(rate(hi)<0))return null; | |
| return {at:(lo+hi)/2,after:hi,before:lo,uncertainty_ms:hi-lo}; | |
| } | |
| function crossing(lo,hi,target,direction,loc){ | |
| const rising=direction==='ascending';let prev=lo,pv=altitude(lo,loc)-target; | |
| for(let t=lo+5*60000;t<=hi+1;t=Math.min(t+5*60000,hi)){ | |
| const val=altitude(t,loc)-target; | |
| if(rising?pv<=0&&val>=0:pv>=0&&val<=0){ | |
| let a=prev,b=t;for(let j=0;j<36;j++){const m=(a+b)/2,v=altitude(m,loc)-target;if(rising?v<0:v>0)a=m;else b=m;}return(a+b)/2; | |
| } | |
| if(t===hi)break;prev=t;pv=val; | |
| } | |
| return null; | |
| } | |
| function day(day,loc,profile,worship,madhhab){ | |
| if(!ready())return {day,boundaries:{},engineUnavailable:true}; | |
| const transition=transitTransition(day,loc,profile.solar_solver); | |
| const noon=transition?.at??maximum(day,loc), out={}, defs=profile.boundaries; | |
| const basic=(id,def)=>{ | |
| if(def.op==='maximum')return noon; | |
| if(def.op==='post_transit_transition')return transition?.after??null; | |
| if(def.op==='altitude')return crossing(def.direction==='ascending'?noon-12*H:noon,def.direction==='ascending'?noon:noon+12*H,def.degrees,def.direction,loc); | |
| if(def.op==='altitude_drop')return crossing(noon,noon+12*H,altitude(noon,loc)-def.degrees,'descending',loc); | |
| if(def.op==='shadow'){ | |
| const peak=altitude(noon,loc);if(peak<=0)return null; | |
| const target=Math.atan(1/(def.factor+1/Math.tan(peak*rad)))/rad; | |
| return crossing(noon,noon+12*H,target,'descending',loc); | |
| } | |
| return null; | |
| }; | |
| Object.entries(defs).forEach(([id,def])=>{out[id]=basic(id,def)}); | |
| const nextNoon=maximum(day+D,loc); | |
| for(let pass=0;pass<5;pass++)Object.entries(defs).forEach(([id,def])=>{ | |
| if(def.op==='alias')out[id]=out[def.boundary]??null; | |
| if(def.op==='next_day'){ | |
| const target=defs[def.boundary];out[id]=target?.op==='altitude'?crossing(nextNoon-12*H,nextNoon,target.degrees,target.direction,loc):null; | |
| } | |
| if(def.op==='rule_start'){ | |
| const rule=worship.rules.find(r=>r.id===def.rule_id),choice=rule?.madhhab?.[madhhab]; | |
| out[id]=out[choice?.start||rule?.common?.start]??null; | |
| } | |
| if(def.op==='fraction')out[id]=out[def.from]!=null&&out[def.to]!=null?out[def.from]+def.fraction*(out[def.to]-out[def.from]):null; | |
| }); | |
| return {day,boundaries:out,solar_transition:transition}; | |
| } | |
| function periods(days,profile,worship,madhhab,styles,scheduleBindings=null){ | |
| const out=[]; | |
| for(const d of days){const b=d.boundaries; | |
| for(const rule of worship.rules){ | |
| const state=styles.category_states[rule.category];if(!state)continue; | |
| const choice=rule.madhhab?.[madhhab];const common={...rule.common,...(typeof choice==='object'?choice:{}),...profile.rule_time_bindings?.[rule.id]}; | |
| if(scheduleBindings&&d.methodUnavailable&&state!==styles.category_states.solar_prayer_restriction)continue; | |
| const scheduled=scheduleBindings?.[rule.id];const windows=scheduled?[scheduled]:(typeof choice==='object'&&choice.windows)||rule.windows||[common]; | |
| for(const w of windows){ | |
| const weekday=['sunday','monday','tuesday','wednesday','thursday','friday','saturday'][new Date(d.day).getUTCDay()]; | |
| if((w.exclude_weekdays||common.exclude_weekdays||[]).includes(weekday))continue; | |
| const requires=w.requires||common.requires||rule.trigger?.requires; | |
| const activation=w.activation||common.activation||(requires?'after_prayer':'boundary'); | |
| const startId=w.display_start||common.display_start||w.start||common.start||rule.trigger?.start||profile.relation_bindings[w.start_relation||common.start_relation]; | |
| // Preserve the chosen school's outer limit, not a fabricated universal end. | |
| const endId=w.end||common.end||w.outer_end||common.outer_end||w.necessity_end||common.necessity_end||rule.trigger?.end; | |
| let start=b[startId],end=b[endId]; | |
| const estimate=worship.display_estimates?.[w.display_estimate_id]||rule.window_display_estimates?.[w.display_estimate_id]; | |
| if(estimate){const center=b[estimate.boundary];if(center!=null){start=center-Number(estimate.before_minutes||0)*60000;end=center+Number(estimate.after_minutes||0)*60000;}} | |
| const endEstimate=worship.display_estimates?.[w.display_end_estimate_id||common.display_end_estimate_id]; | |
| if(endEstimate){const center=b[endEstimate.boundary];if(center!=null)end=center-Number(endEstimate.before_minutes||0)*60000;} | |
| if(start!=null&&end!=null&&end>=start){ | |
| const row={id:rule.id,start,end,point:!estimate&&!endEstimate&&(end===start||Boolean(profile.boundaries[endId]?.transition_only)),startId,endId,state,rule,day:d.day,calculationMethod:scheduled?d.calculationMethod:null,conditional:rule.category==='prayer_restriction'||activation==='after_prayer'||Boolean(requires),requires,activation,displayEstimate:estimate||null,displayEndEstimate:endEstimate||null};out.push(row); | |
| for(const binding of styles.preference_bindings||[]){ | |
| const value=common[binding.field];if(!value)continue; | |
| const pair=binding.mode==='end'?[startId,value]:profile.relation_bindings[value]; | |
| if(!Array.isArray(pair))continue; | |
| const a=b[pair[0]],z=b[pair[1]]; | |
| if(a!=null&&z!=null&&z>a&&a>=start&&z<=end)out.push({...row,start:a,end:z,point:false,startId:pair[0],endId:pair[1],state:binding.state,preference:binding.field}); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return out; | |
| } | |
| return {ready,position,altitude,maximum,transitTransition,crossing,day,periods}; | |
| })(); | |