Flate Rate $15 Shipping Australia Wide Dismiss
GDPR Cookie Consent with Real Cookie Banner
/* =============================================================
KJ — Birth Chart Generator (Universal, Clean + Stable)
- OSM place search (portal + mini fallback)
- Timezone-by-place-and-date → UT (fixes DST / wrong ASC)
- Swiss Ephemeris bootstrap (WASM) with safe wrappers
- Bright wheel, colored planet glyphs with anti-overlap stacking
- Aspect lines on wheel + responsive aspects table
- Export composite PNG (wheel + table) and email sender
- Elementor-safe wiring; single consent mover (no duplicates)
============================================================= */
/* ---------- Paths (update if you moved files) ---------- */
const JS_DIRECT = "/wp-content/kjahli/swisseph-kjahli.js";
const WASM_DIRECT = "/wp-content/kjahli/swisseph-kjahli.wasm";
const DATA_DIRECT = "/wp-content/kjahli/swisseph-kjahli.data";
const OSM_PROXY = "/wp-content/kjahli/osm-proxy.php";
/* ---------- UI helpers ---------- */
const $ = sel => document.querySelector(sel);
const logBox = $("#kj-log");
const log = (m)=>{ if(logBox){ logBox.textContent += m+"\n"; logBox.scrollTop = logBox.scrollHeight; } console.log(m); };
/* ---------- Colors / glyphs ---------- */
const PLANET_COLORS = {
Sun:"#f6c200", Moon:"#cccccc", Mercury:"#00a2ff", Venus:"#e75480",
Mars:"#d62828", Jupiter:"#ff7f0e", Saturn:"#8d6e63",
Uranus:"#29b6f6", Neptune:"#4b70dd", Pluto:"#9c27b0", "True Node":"#2e7d32", Chiron:"#607d8b"
};
window.PLANET_GLYPH = {
Sun:"☉", Moon:"☾", Mercury:"☿", Venus:"♀", Mars:"♂",
Jupiter:"♃", Saturn:"♄", Uranus:"♅", Neptune:"♆", Pluto:"♇",
"True Node":"☊", Chiron:"⚷"
};
const SIGNS = ["\u2648\uFE0E","\u2649\uFE0E","\u264A\uFE0E","\u264B\uFE0E","\u264C\uFE0E","\u264D\uFE0E","\u264E\uFE0E","\u264F\uFE0E","\u2650\uFE0E","\u2651\uFE0E","\u2652\uFE0E","\u2653\uFE0E"];
/* ---------- Aspect matcher (shared by wheel + exporter) ---------- */
(function exposeAspectMatcher(){
const _norm = d => ((d%360)+360)%360;
const _sep = (a,b)=>{ let d=Math.abs(_norm(a)-_norm(b)); return d>180?360-d:d; };
const ASPECTS = [
{key:"conj", angle: 0, orb:8, color:"#999999", width:1.8, sym:"☌"},
{key:"opp", angle:180, orb:8, color:"#e53935", width:2.4, sym:"☍"},
{key:"tri", angle:120, orb:6, color:"#1e88e5", width:2.0, sym:"△"},
{key:"sqr", angle: 90, orb:6, color:"#e53935", width:2.0, sym:"□"},
{key:"sex", angle: 60, orb:4, color:"#1e88e5", width:1.8, sym:"✶"},
];
window.matchAspect = (a1,a2)=>{
const s=_sep(a1,a2);
for(const a of ASPECTS){ if(Math.abs(s-a.angle)<=a.orb) return a; }
return null;
};
})();
/* =====================================================
TIMEZONE: Convert local birth time (place+date) to UTC
===================================================== */
async function getOffsetMinutesByLatLonAtLocalTime(lat, lon, localISO){
// TimeAPI.io (free, no key)
try{
const u = `https://timeapi.io/api/TimeZone/coordinate?latitude=${lat}&longitude=${lon}`;
const r = await fetch(u); if(r.ok){
const j = await r.json();
if(j?.timeZone){
const tz = j.timeZone;
const off = offsetMinutesForZoneAtLocal(tz, localISO);
if(Number.isFinite(off)) return off;
}
}
}catch(_){/* ignore */}
// worldtimeapi fallback — pick a plausible zone for AU/region
try{
const zones = await (await fetch("https://worldtimeapi.org/api/timezone")).json();
const guess = zones.find(z=>/Australia\//.test(z)) || zones.find(z=>/Etc\/GMT/.test(z));
if(guess){
const off = offsetMinutesForZoneAtLocal(guess, localISO);
if(Number.isFinite(off)) return off;
}
}catch(_){/* ignore */}
// Browser zone as last meaningful fallback
try{
const z = Intl.DateTimeFormat().resolvedOptions().timeZone;
const off = offsetMinutesForZoneAtLocal(z, localISO);
if(Number.isFinite(off)) return off;
}catch(_){/* ignore */}
log("⚠️ Timezone lookup failed — using UTC offset 0");
return 0;
}
function offsetMinutesForZoneAtLocal(timeZone, localISO){
try{
const dtf = new Intl.DateTimeFormat('en-US', {
timeZone, hour12:false,
year:'numeric', month:'2-digit', day:'2-digit',
hour:'2-digit', minute:'2-digit', second:'2-digit'
});
const [dPart,tPart] = localISO.split("T");
const [y,m,d] = dPart.split("-").map(Number);
const [H,Mi] = (tPart||"00:00").split(":").map(Number);
const utcGuess = Date.UTC(y, m-1, d, H, Mi, 0);
const parts = dtf.formatToParts(new Date(utcGuess));
const get = (type)=>+(parts.find(p=>p.type===type)?.value||"0");
const y2=get('year'), M2=get('month'), d2=get('day'), H2=get('hour'), m2=get('minute'), s2=get('second');
const backUTC = Date.UTC(y2, M2-1, d2, H2, m2, s2);
return (utcGuess - backUTC)/60000; // minutes
}catch(e){ log("TZ calc error: "+(e.message||e)); return NaN; }
}
function localToUTC_JD(y,m,d,H,M, lat, lon){
const iso = `${String(y).padStart(4,"0")}-${String(m).padStart(2,"0")}-${String(d).padStart(2,"0")}T${String(H).padStart(2,"0")}:${String(M).padStart(2,"0")}`;
return getOffsetMinutesByLatLonAtLocalTime(lat, lon, iso).then(offMin=> (H + M/60) - (offMin/60));
}
/* =====================================================
Swiss Ephemeris bootstrap
===================================================== */
let SwissephFactory=null, __se=null;
(async()=>{ try{ const mod=await import(JS_DIRECT); SwissephFactory=mod?.default||mod; log("🟣 Module attached"); }catch(e){ log("❌ ESM import failed: "+(e.message||e)); } })();
async function ensureSwiss(){
if(__se) return __se;
if(typeof SwissephFactory!=="function") throw new Error("Swiss factory not found");
const wasmURL=WASM_DIRECT+"?v="+Date.now();
const dataURL=DATA_DIRECT+"?v="+Date.now();
async function fetchInstantiateWasm(imports){
const r=await fetch(wasmURL,{cache:"no-store"});
if(!r.ok) throw new Error("WASM HTTP "+r.status);
const buf=await r.arrayBuffer();
const {instance}=await WebAssembly.instantiate(buf,imports);
return instance;
}
const modOpts={
locateFile:(p)=> p.endsWith(".wasm")?wasmURL : p.endsWith(".data")?dataURL : p,
instantiateWasm:(imports,cb)=>{fetchInstantiateWasm(imports).then(i=>cb(i));return{};},
print:(m)=>log(String(m)),
printErr:(m)=>log("ERR:"+String(m)),
monitorRunDependencies:(n)=>log("⏳ runDeps: "+n)
};
let mod = SwissephFactory(modOpts);
if(mod && typeof mod.then==="function") mod = await mod;
// Wrap low-level exports if needed (Emscripten variations)
if(typeof mod.swe_julday!=="function" && typeof mod._swe_julday==="function"){
const readCString=(ptr,max=512)=>{const heap=mod.HEAPU8;let end=ptr,stop=ptr+max;while(endmod._swe_julday(y|0,m|0,d|0,+h,g|0);
mod.swe_calc_ut=(jd,ipl,iflag)=>{const px=mod._malloc(24),serr=mod._malloc(256);const rc=mod._swe_calc_ut(+jd,ipl|0,iflag|0,px,serr);const out={rc,x:[NaN,NaN,NaN],serr:""};if(rc>=0){out.x=[mod.HEAPF64[px/8],mod.HEAPF64[px/8+1],mod.HEAPF64[px/8+2]];}else{out.serr=readCString(serr,256);}mod._free(px);mod._free(serr);return out;};
const _housesFn = mod._swe_houses_ex || mod._swe_houses_ex2;
if(typeof _housesFn==="function"){
mod.swe_houses_ex=(jd,lat,lon,hsys)=>{const cusp=mod._malloc(13*8),ascm=mod._malloc(10*8);_housesFn(+jd,0,+lat,+lon,hsys.charCodeAt(0),cusp,ascm);const out={cusp:[],ascmc:[]};for(let i=0;i<13;i++) out.cusp[i]=mod.HEAPF64[cusp/8+i];for(let i=0;i<10;i++) out.ascmc[i]=mod.HEAPF64[ascm/8+i];mod._free(cusp);mod._free(ascm);return out;};
}
if(typeof mod.SE_GREG_CAL==="undefined") mod.SE_GREG_CAL=1;
log("ℹ️ Using wrapped C exports");
}
if(typeof mod.swe_julday!=="function") throw new Error("Swiss Ephemeris did not initialise.");
__se=mod; log("✅ Swiss Ephemeris ready");
return mod;
}
/* =====================================================
Wheel drawing (bright) + aspects on top
===================================================== */
function drawWheel(container,{asc,houses,planets}){
container.innerHTML="";
const size=640,cx=size/2,cy=size/2;
const rOuter=270, rBandO=260, rBandI=228, rInner=180;
const rChord=rInner-14, rGlyph=(rBandI+rInner)/2, rSignGlyph=(rBandO+rBandI)/2;
const SVGNS="http://www.w3.org/2000/svg";
const mk=(n)=>document.createElementNS(SVGNS,n);
const polar=(r,deg)=>{const a=(deg-90)*Math.PI/180; return {x:cx+r*Math.cos(a), y:cy+r*Math.sin(a)};};
const text=(x,y,val,fs,fill)=>{const t=mk("text");t.setAttribute("x",x);t.setAttribute("y",y);t.setAttribute("fill",fill);
t.setAttribute("font-size",fs);t.setAttribute("font-family","Noto Sans Symbols 2, DejaVu Sans, Arial Unicode MS, sans-serif");
t.setAttribute("font-weight","600");t.setAttribute("text-anchor","middle");t.setAttribute("dominant-baseline","middle");t.textContent=val;return t;};
const line=(x1,y1,x2,y2,stroke,sw,op=1)=>{const l=mk("line");l.setAttribute("x1",x1);l.setAttribute("y1",y1);l.setAttribute("x2",x2);l.setAttribute("y2",y2);
l.setAttribute("stroke",stroke);l.setAttribute("stroke-width",sw);l.setAttribute("stroke-linecap","round");l.setAttribute("opacity",op);return l;};
const svg=mk("svg"); svg.classList.add("kj-wheel"); svg.setAttribute("viewBox",`0 0 ${size} ${size}`);
// Outer rim + sign band slices
const rimOuter=mk("circle"); rimOuter.setAttribute("cx",cx);rimOuter.setAttribute("cy",cy);rimOuter.setAttribute("r",rOuter);
rimOuter.setAttribute("fill","#fff"); rimOuter.setAttribute("stroke","#fff"); rimOuter.setAttribute("stroke-width","26"); svg.appendChild(rimOuter);
const bandRing=mk("circle"); bandRing.setAttribute("cx",cx);bandRing.setAttribute("cy",cy);bandRing.setAttribute("r",rBandO+1);
bandRing.setAttribute("fill","none"); bandRing.setAttribute("stroke","#222"); bandRing.setAttribute("stroke-width","2"); svg.appendChild(bandRing);
const signBandFill="#4a4a4f", signSep="#2c2c30";
const toXY=(r,deg)=>{const a=(deg-90)*Math.PI/180;return[cx+r*Math.cos(a),cy+r*Math.sin(a)];};
for(let i=0;i<12;i++){
const start=i*30, end=start+30;
const [x1,y1]=toXY(rBandO,start), [x2,y2]=toXY(rBandO,end), [x3,y3]=toXY(rBandI,end), [x4,y4]=toXY(rBandI,start);
const p=mk("path");
p.setAttribute("d",`M ${x1} ${y1} A ${rBandO} ${rBandO} 0 0 1 ${x2} ${y2} L ${x3} ${y3} A ${rBandI} ${rBandI} 0 0 0 ${x4} ${y4} Z`);
p.setAttribute("fill",signBandFill); svg.appendChild(p);
const s1=polar(rBandO,start), s2=polar(rBandI,start);
svg.appendChild(line(s1.x,s1.y,s2.x,s2.y,signSep,2,0.9));
}
const bandInner=mk("circle"); bandInner.setAttribute("cx",cx);bandInner.setAttribute("cy",cy);bandInner.setAttribute("r",rBandI);
bandInner.setAttribute("fill","none"); bandInner.setAttribute("stroke","#1f1f22"); bandInner.setAttribute("stroke-width","2"); svg.appendChild(bandInner);
// Inner white wheel
const inner=mk("circle"); inner.setAttribute("cx",cx);inner.setAttribute("cy",cy);inner.setAttribute("r",rInner);
inner.setAttribute("fill","#ffffff"); inner.setAttribute("stroke","#d9dbe2"); inner.setAttribute("stroke-width","1.5"); svg.appendChild(inner);
// House cusps (Asc highlighted)
const cusps=(houses?.cusp?.length>=13)?houses.cusp:Array.from({length:13},(_,i)=>i*30);
for(let i=1;i<=12;i++){
const a=cusps[i] ?? (i*30);
const p1=polar(rInner,a), p2=polar(70,a);
const isAsc=(i===1);
svg.appendChild(line(p1.x,p1.y,p2.x,p2.y, isAsc? "#7c3aed":"#c8cbd5", isAsc?2.2:1.2, isAsc?1:.95));
}
// House numbers
for(let i=0;i<12;i++){ const mid=i*30+15; const pos=polar(100,mid); svg.appendChild(text(pos.x,pos.y,String(i+1),12,"#8a8f99")); }
// Sign glyphs (not purple)
for(let i=0;i<12;i++){ const pos=polar(rSignGlyph,i*30+15); svg.appendChild(text(pos.x,pos.y,SIGNS[i],19,"#f2f3f6")); }
// Aspect lines
if(Array.isArray(planets) && planets.length>1 && typeof window.matchAspect==="function"){
for(let i=0;ia.lon-b.lon);
let cur = [sorted[0]];
for(let i=1;i{
const k = group.length;
const start = -(k-1)/2;
for(let i=0;i{
const th=document.createElement("th");
th.innerHTML=(window.PLANET_GLYPH[p.name]||"•")+" "+p.name;
th.style.padding="8px"; th.style.borderBottom="1px solid #e6e6e6";
trh.appendChild(th);
});
thead.appendChild(trh);
const tbody=document.createElement("tbody");
for(let r=0;r=r){ td.textContent="—"; td.style.color="#bbb"; }
else{
const asp=window.matchAspect(planets[r].lon, planets[c].lon);
if(asp){
td.textContent=asp.sym;
td.style.color = (asp.key==="conj") ? "#666" : ((asp.key==="opp"||asp.key==="sqr") ? "#e53935" : "#1e88e5");
td.style.fontWeight="700";
}else{ td.textContent="·"; td.style.color="#bbb"; }
}
tr.appendChild(td);
}
tbody.appendChild(tr);
}
tbl.appendChild(thead); tbl.appendChild(tbody);
}
/* =====================================================
Generate & Clear
===================================================== */
const _norm = d => ((d%360)+360)%360;
async function generate(){
log("=== Generating chart ===");
try{
const se = await ensureSwiss();
const date = $("#kj-date")?.value;
const time = $("#kj-time")?.value;
const c = $("#kj-coords")?.value;
const hsys = $("#kj-house")?.value || "P";
if(!date||!time||!c){ log("❌ Fill date, time, and place"); alert("Please fill date, time, and pick a place."); return; }
const [y,m,d] = date.split("-").map(n=>parseInt(n,10));
const [H,Mi] = (time||"0:0").split(":").map(n=>parseInt(n,10));
const [lat,lon]= c.split(",").map(v=>parseFloat(v.trim()));
// Convert local time (at place) to UT hour
const utHour = await localToUTC_JD(y,m,d,H||0,Mi||0, lat, lon);
const jd = se.swe_julday(y,m,d, utHour, se.SE_GREG_CAL);
log("JD (UT): "+jd.toFixed(6));
// Planets
const PID={Sun:0,Moon:1,Mercury:2,Venus:3,Mars:4,Jupiter:5,Saturn:6,Uranus:7,Neptune:8,Pluto:9,"True Node":11,Chiron:15};
const planets=[];
for(const [name,id] of Object.entries(PID)){
const r=se.swe_calc_ut(jd,id,0);
if(r?.x){ planets.push({name,lon:_norm(r.x[0])}); }
}
// Houses & ASC (Swiss expects geo lon East+)
const hs = se.swe_houses_ex(jd, lat, lon, hsys);
const asc = hs.ascmc?.[0];
// Publish
window.LAST_PLANETS = planets;
window.LAST_ASC = asc;
window.LAST_HOUSES = hs;
drawWheel($("#kj-chart"), {asc, houses:hs, planets});
renderAspectsTable(planets);
}catch(e){ log("❌ Chart error: "+(e.message||e)); alert("Chart error: "+(e.message||e)); }
}
function clearAll(){
["#kj-date","#kj-time","#kj-place","#kj-coords"].forEach(sel=>{ const el=$(sel); if(el) el.value=""; });
if(logBox) logBox.textContent="";
const chart=$("#kj-chart"); if(chart) chart.innerHTML="The wheel will render here.";
const tbl=$("#kj-aspects-table"); if(tbl) tbl.innerHTML="";
}
/* Wire buttons (survive Elementor re-render) */
function wireButtons(){
$("#kj-go")?.addEventListener("click", generate);
$("#kj-clear")?.addEventListener("click", clearAll);
}
document.addEventListener("DOMContentLoaded", wireButtons);
new MutationObserver(wireButtons).observe(document.documentElement,{childList:true,subtree:true});
/* =====================================================
Exporter (wheel + full aspects table) & Email sender
===================================================== */
window.ensureCanvg = window.ensureCanvg || (async function ensureCanvgSafe(){
if (window.Canvg && typeof window.Canvg.from === "function") return window.Canvg;
const sources = [
"/wp-content/kjahli/canvg.min.js",
"https://cdn.jsdelivr.net/npm/canvg@3/dist/browser/canvg.min.js",
"https://unpkg.com/canvg@3/dist/browser/canvg.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/canvg/3.0.10/umd/canvg.min.js"
];
for(const src of sources){
try{
await new Promise((res,rej)=>{ const s=document.createElement("script"); s.src=src; s.async=true; s.onload=res; s.onerror=()=>rej(); document.head.appendChild(s); });
if(window.Canvg && typeof window.Canvg.from==="function") return window.Canvg;
}catch(_){/* try next */}
}
return null; // no throw
});
window.exportCompositePngDataUrl = async function exportCompositePngDataUrl(){
const wheelSVG = document.querySelector('#kj-chart svg.kj-wheel');
if(!wheelSVG) throw new Error("No chart found. Generate a chart first.");
const svgString = new XMLSerializer().serializeToString(wheelSVG);
const wheelSize = 1100;
// Rasterize wheel
const wheelCanvas = document.createElement('canvas');
wheelCanvas.width = wheelSize; wheelCanvas.height = wheelSize;
const wCtx = wheelCanvas.getContext('2d');
wCtx.fillStyle = '#ffffff'; wCtx.fillRect(0,0,wheelSize,wheelSize);
try{
const CanvgRef = await window.ensureCanvg();
if(CanvgRef){ const v = await CanvgRef.from(wCtx, svgString); await v.render(); }
else{ const svg64 = btoa(unescape(encodeURIComponent(svgString))); const img = new Image(); await new Promise((res,rej)=>{ img.onload=res; img.onerror=()=>rej(new Error("SVG rasterize failed")); img.src='data:image/svg+xml;base64,'+svg64; }); wCtx.drawImage(img, 0, 0, wheelSize, wheelSize); }
}catch(_){ const svg64 = btoa(unescape(encodeURIComponent(svgString))); const img = new Image(); await new Promise((res,rej)=>{ img.onload=res; img.onerror=()=>rej(new Error("SVG rasterize failed")); img.src='data:image/svg+xml;base64,'+svg64; }); wCtx.drawImage(img, 0, 0, wheelSize, wheelSize); }
// Table data
const P = Array.isArray(window.LAST_PLANETS) ? window.LAST_PLANETS : [];
const W = 1200, topPad=40, leftPad=40, rightPad=40, gap=28, bottomPad=36;
const headerH=36, rowH=28;
const cols = P.length ? P.length+1 : 0;
const colW = cols ? Math.floor((W-leftPad-rightPad)/cols) : 0;
const tableH = P.length ? (headerH + rowH*P.length + 16) : 0;
const H = topPad + wheelSize + (P.length ? gap + tableH : 0) + bottomPad;
const canvas = document.createElement('canvas');
canvas.width = W; canvas.height = H;
const ctx = canvas.getContext('2d');
// BG
ctx.fillStyle="#ffffff"; ctx.fillRect(0,0,W,H);
// Wheel centered
const wx = Math.round((W-wheelSize)/2), wy = topPad;
ctx.drawImage(wheelCanvas, wx, wy, wheelSize, wheelSize);
if(P.length){
const x0=leftPad, y0=wy+wheelSize+gap;
// header
ctx.fillStyle="#f6f7fb"; ctx.fillRect(x0,y0, colW*cols, headerH);
ctx.strokeStyle="#e6e6e6"; ctx.lineWidth=1;
ctx.fillStyle="#222"; ctx.font="600 14px system-ui, -apple-system, Segoe UI, Arial";
// corner
ctx.strokeRect(x0,y0,colW,headerH);
for(let c=0;c=r){ ctx.fillStyle="#bbb"; ctx.fillText('—', x+colW/2-4, y+19); }
else{
const asp = window.matchAspect(P[r].lon, P[c].lon);
if(asp){ ctx.fillStyle=(asp.key==="conj")?"#666":((asp.key==="opp"||asp.key==="sqr")?"#e53935":"#1e88e5"); ctx.fillText(asp.sym, x+colW/2-6, y+19); }
else { ctx.fillStyle="#bbb"; ctx.fillText('·', x+colW/2-2, y+19); }
}
}
}
}
return canvas.toDataURL('image/png');
};
(function wireEmailButton(){
document.addEventListener("click", async (e)=>{
const btn = e.target.closest && e.target.closest("#kj-send");
if(!btn) return;
e.preventDefault();
const email = ($("#kj-email")?.value||"").trim();
if(!email){ alert("Please enter your email."); return; }
if(!window.kjAjax?.url || !window.kjAjax?.nonce){ alert("Email service not booted. Add the [kj_ajax_boot] shortcode on this page."); return; }
if(!document.querySelector('#kj-chart svg.kj-wheel')){ alert("Please generate a chart first."); return; }
try{
const pngDataUrl = await window.exportCompositePngDataUrl();
const date = $("#kj-date")?.value || '';
const time = $("#kj-time")?.value || '';
const place= $("#kj-place")?.value || '';
const consent = !!$("#kj-consent")?.checked;
const meta = `DOB: ${date} ${time}
Place: ${place}`;
const fd = new FormData();
fd.append('action','kj_send_chart');
fd.append('nonce', window.kjAjax.nonce);
fd.append('email', email);
fd.append('consent', consent ? '1' : '0');
fd.append('meta', meta);
fd.append('png', pngDataUrl);
const res = await fetch(window.kjAjax.url, { method:'POST', body: fd, credentials:'same-origin' });
const j = await res.json();
if(!j?.success) throw new Error((j?.data?.message||j?.message)||('HTTP '+res.status));
alert("Chart sent! Check your inbox.");
}catch(err){ console.error(err); alert("Sorry — could not send image: "+(err.message||err)); }
});
})();
/* =====================================================
OSM place search (portal, bright, self-healing)
===================================================== */
(function OSMv3(){
const PROXY = OSM_PROXY || "/wp-content/kjahli/osm-proxy.php";
const LIMIT = 8;
// Badge (status)
let badge = document.getElementById("kj-badge");
if(!badge){
badge = document.createElement("div");
badge.id = "kj-badge";
Object.assign(badge.style,{
position:"fixed",right:"10px",bottom:"10px",zIndex:2147483647,
background:"#111",color:"#fff",font:"12px/1.3 system-ui",
padding:"8px 10px",borderRadius:"8px",boxShadow:"0 6px 16px rgba(0,0,0,.35)",
pointerEvents:'none' // never blocks other UI
});
badge.textContent = "OSM: booting…";
(document.readyState==="loading"
? document.addEventListener("DOMContentLoaded",()=>document.body.appendChild(badge))
: document.body.appendChild(badge));
}
const setBadge = t => { try{ badge.style.display="block"; badge.textContent=t; }catch(_){} };
// Styles for portal
if(!document.getElementById("kj-osm-portal-style")){
const st=document.createElement("style"); st.id="kj-osm-portal-style";
st.textContent = `
#kj-osm-portal{position:fixed;display:none;background:#fff;color:#111;border:1px solid #ccc;border-radius:10px;
max-height:260px;overflow:auto;padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.2);z-index:2147483600}
#kj-osm-portal .itm{padding:8px 10px;border-radius:6px;cursor:pointer;color:#111;font-size:14px}
#kj-osm-portal .itm:hover,#kj-osm-portal .itm.active{background:#e8f0fe;color:#000}
`;
document.head.appendChild(st);
}
// Portal
let portal = document.getElementById("kj-osm-portal");
if(!portal){ portal=document.createElement("div"); portal.id="kj-osm-portal"; document.body.appendChild(portal); }
function findInput(){
return document.getElementById("kj-place")
|| document.querySelector('input#kj-place')
|| document.querySelector('input[placeholder*="Type 3"][type="text"]')
|| document.querySelector('input[placeholder*="letters"][type="text"]')
|| document.querySelector('input[placeholder*="Place"][type="text"]')
|| null;
}
function findCoords(){
return document.getElementById("kj-coords")
|| document.querySelector('input#kj-coords')
|| document.querySelector('input[readonly]')
|| null;
}
let state={input:null,coords:null,items:[],sel:-1,lastId:0,debounce:null};
function ensurePos(){ if(!state.input) return; const r=state.input.getBoundingClientRect(); portal.style.left=r.left+"px"; portal.style.top=(r.bottom+6)+"px"; portal.style.width=r.width+"px"; }
function show(){ ensurePos(); portal.style.display="block"; }
function hide(){ portal.style.display="none"; portal.innerHTML=""; state.sel=-1; state.items=[]; }
function unbind(prev){ if(!prev) return; prev.onfocus=prev.oninput=prev.onkeydown=null; }
function bind(input, coords){
if(!input) return; if(state.input===input) return;
unbind(state.input);
state={...state,input,coords,items:[],sel:-1,lastId:0};
document.addEventListener("pointerdown",(e)=>{ if(e.target===state.input || portal.contains(e.target)) return; hide(); });
window.addEventListener("scroll",()=>{ if(portal.style.display==="block") ensurePos(); }, true);
window.addEventListener("resize",()=>{ if(portal.style.display==="block") ensurePos(); });
state.input.onfocus=()=>{ if(state.input.value.trim().length>=3 && state.items.length) show(); };
state.input.oninput=()=>{
const q=state.input.value.trim();
if(q.length<3){ hide(); return; }
clearTimeout(state.debounce);
const id=++state.lastId;
state.debounce=setTimeout(()=>search(q,id),240);
};
state.input.onkeydown=(e)=>{
if(portal.style.display!=="block"||!state.items.length) return;
if(e.key==="ArrowDown"){ e.preventDefault(); state.sel=(state.sel+1)%state.items.length; updateSel(); }
else if(e.key==="ArrowUp"){ e.preventDefault(); state.sel=(state.sel-1+state.items.length)%state.items.length; updateSel(); }
else if(e.key==="Enter"){ e.preventDefault(); pick(state.items[state.sel>=0?state.sel:0]); }
else if(e.key==="Escape"){ hide(); }
};
setBadge("OSM: ready — type a place");
}
function updateSel(){ [...portal.children].forEach((el,i)=>el.classList.toggle("active", i===state.sel)); }
function pick(p){
const name=p.display_name||p.name||"[no name]";
state.input.value=name;
if(state.coords) state.coords.value=`${p.lat},${p.lon}`;
setBadge("OSM: picked "+(state.coords?state.coords.value:"(no coords field)"));
hide(); state.input.blur();
}
async function search(q,id){
try{
setBadge('OSM: searching… "'+q+'"');
let data=null;
try{
const r=await fetch(`${PROXY}?q=${encodeURIComponent(q)}&limit=${LIMIT}&v=${Date.now()}`,{cache:"no-store"});
if(r.ok) data=await r.json(); else setBadge("OSM: proxy "+r.status+" (fallback)");
}catch(_){ setBadge("OSM: proxy error (fallback)"); }
if(!Array.isArray(data)){
const rr=await fetch(`https://nominatim.openstreetmap.org/search?format=jsonv2&q=${encodeURIComponent(q)}&limit=${LIMIT}`);
if(rr.ok) data=await rr.json(); else setBadge("OSM: public "+rr.status);
}
if(id!==state.lastId) return;
render(Array.isArray(data)?data:[]);
}catch(e){ setBadge("OSM: error "+(e.message||e)); hide(); }
}
function render(results){
portal.innerHTML=""; state.items=results;
if(!state.items.length){ hide(); setBadge("OSM: 0 results"); return; }
for(const p of state.items){
const row=document.createElement("div");
row.className="itm"; row.textContent=p.display_name||p.name||"[no name]";
row.addEventListener("pointerdown",(e)=>{ e.preventDefault(); pick(p); });
portal.appendChild(row);
}
state.sel=-1; updateSel(); ensurePos(); show();
setBadge(`OSM: rendered ${state.items.length} items`);
}
// Poll + observe + watchdog
function poll(ms=250,max=160){
let tries=0; const t=setInterval(()=>{
const inp=findInput(), crd=findCoords(); tries++;
if(inp){ clearInterval(t); bind(inp, crd); }
else if(tries%10===0){ setBadge("OSM: waiting… ("+tries+")"); }
if(tries>max){ clearInterval(t); setBadge("OSM: gave up waiting"); }
},ms);
}
const mo=new MutationObserver(()=>{ const inp=findInput(), crd=findCoords(); if(inp && state.input!==inp) bind(inp, crd); });
mo.observe(document.documentElement,{childList:true,subtree:true});
setInterval(()=>{ const inp=findInput(), crd=findCoords(); if(inp && state.input!==inp) bind(inp, crd); }, 2000);
poll();
})();
/* =====================================================
Consent checkbox: move directly under email (single source of truth)
===================================================== */
(function placeConsentUnderEmail(){
const MSG_TEXT = 'Send me this chart and add me to your newsletter (optional).';
function moveOnce(){
const email = document.getElementById('kj-email');
const cb = document.getElementById('kj-consent');
if(!email || !cb) return false;
// Prefer a wrapping label; else build one so tap target stays big
let label = cb.closest('label');
if(!label){
label = document.createElement('label');
label.style.display = 'flex';
label.style.alignItems = 'center';
label.style.gap = '8px';
label.appendChild(cb);
const span = document.createElement('span');
span.id = 'kj-consent-msg';
span.textContent = MSG_TEXT;
label.appendChild(span);
}else{
if(!label.querySelector('#kj-consent-msg')){
const span = document.createElement('span');
span.id = 'kj-consent-msg';
span.textContent = MSG_TEXT;
label.appendChild(span);
}
}
const emailBlock = email.closest('.elementor-field-group') || email.parentElement;
if(!emailBlock) return false;
let wrap = document.getElementById('kj-consent-wrap');
if(!wrap){ wrap = document.createElement('div'); wrap.id = 'kj-consent-wrap'; }
Object.assign(wrap.style,{ marginTop:'8px', display:'flex', alignItems:'center', gap:'10px', font:'14px/1.4 system-ui,-apple-system,Segoe UI,Arial', color:'#e9e9ee' });
cb.style.width = cb.style.height = '18px';
cb.style.flex = '0 0 auto';
wrap.innerHTML = '';
wrap.appendChild(label);
emailBlock.insertAdjacentElement('afterend', wrap);
return true;
}
const tryMove = ()=>{ try{ moveOnce(); }catch(_){ /* noop */ } };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', tryMove); else tryMove();
new MutationObserver(tryMove).observe(document.body,{childList:true,subtree:true});
setTimeout(tryMove, 400); setTimeout(tryMove, 900); setTimeout(tryMove, 1600);
})();
/* =====================================================
Mini OSM fallback — binds only if portal is not active
===================================================== */
(function miniOSMKeepAlive(){
const PROXY = "/wp-content/kjahli/osm-proxy.php";
let bound = false, tId = null;
function ensureStyles(){
if(document.getElementById('kj-mini-osm-style')) return;
const s=document.createElement('style'); s.id='kj-mini-osm-style';
s.textContent = `
#kj-mini-osm{position:absolute;left:0;right:0;top:100%;margin-top:6px;background:#fff;color:#111;
border:1px solid #ccc;border-radius:10px;max-height:260px;overflow:auto;padding:4px;box-shadow:0 10px 30px rgba(0,0,0,.2);z-index:2147483600;display:none}
#kj-mini-osm .itm{padding:8px 10px;border-radius:6px;cursor:pointer}
#kj-mini-osm .itm:hover{background:#e8f0fe}
.kj-mini-wrap{position:relative}
`;
document.head.appendChild(s);
}
function bind(){
if(bound) return;
const input = document.getElementById('kj-place');
const coords = document.getElementById('kj-coords');
if(!input) return;
// If the big portal is present and visible, do nothing
const portal = document.getElementById('kj-osm-portal');
if(portal && portal.style.display !== 'none') return;
ensureStyles();
if(!input.parentElement.classList.contains('kj-mini-wrap')){
input.parentElement.classList.add('kj-mini-wrap');
const dd = document.createElement('div'); dd.id='kj-mini-osm';
input.parentElement.appendChild(dd);
}
const list = document.getElementById('kj-mini-osm');
function show(){ list.style.display='block'; }
function hide(){ list.style.display='none'; list.innerHTML=''; }
function render(items){
list.innerHTML='';
if(!items.length){ hide(); return; }
items.forEach(p=>{
const row=document.createElement('div'); row.className='itm';
row.textContent = p.display_name || p.name || '[no name]';
row.onclick = ()=>{ input.value = row.textContent; if(coords) coords.value = `${p.lat},${p.lon}`; hide(); input.blur(); };
list.appendChild(row);
});
show();
}
let lastId=0, deb=null;
input.addEventListener('input', ()=>{
const q=input.value.trim();
if(q.length<3){ hide(); return; }
const id=++lastId;
clearTimeout(deb);
deb=setTimeout(async ()=>{
try{
const u = `${PROXY}?q=${encodeURIComponent(q)}&limit=8&v=${Date.now()}`;
let data=null; try{ const r=await fetch(u,{cache:'no-store'}); if(r.ok) data=await r.json(); }catch(_){/* ignore */}
if(!Array.isArray(data)){ const rr=await fetch(`https://nominatim.openstreetmap.org/search?format=jsonv2&q=${encodeURIComponent(q)}&limit=8`); if(rr.ok) data=await rr.json(); }
if(id!==lastId) return;
render(Array.isArray(data)?data:[]);
}catch(_){ hide(); }
}, 250);
});
input.addEventListener('keydown', (e)=>{ if(e.key==='Escape') hide(); });
document.addEventListener('pointerdown', (e)=>{ if(e.target!==input && !list.contains(e.target)) hide(); }, {capture:true});
bound = true;
}
function tick(){ try{ bind(); }catch(_){/* noop */} tId = setTimeout(tick, 1000); }
tick();
})();
/* =====================================================
Mobile/UI tweaks
===================================================== */
(function injectMobileCSS(){
if(document.getElementById('kj-mobile-fixes')) return;
const st = document.createElement('style'); st.id='kj-mobile-fixes';
st.textContent = `
.kj-bc{ max-width:680px; margin:0 auto; }
#kj-chart{ min-height: 40px; }
#kj-aspects-table{ overflow:auto; max-width:100%; }
/* Ensure overlays never block menu/toggles unless visible */
#kj-osm-portal,[id='kj-mini-osm']{ pointer-events:auto; }
#kj-badge{ pointer-events:none; }
/* Form label contrast on dark */
.kj-bc label, .kj-bc small, #kj-consent-wrap{ color:#e9e9ee; }
`;
document.head.appendChild(st);
})();