const {useState: useStudioState, useRef: useStudioRef, useEffect: useStudioEffect} = React; function BuildingSearch({onSelect}) { const [lat,setLat]=useStudioState('44.6488'), [lon,setLon]=useStudioState('-63.5752'); const [radius,setRadius]=useStudioState('150'), [rows,setRows]=useStudioState([]), [selected,setSelected]=useStudioState(null); const [height,setHeight]=useStudioState(9), [busy,setBusy]=useStudioState(false), [message,setMessage]=useStudioState('Halifax coordinates are preselected. Adjust them to your project site.'); const controller=useStudioRef(null), cache=useStudioRef(new Map()); useStudioEffect(() => () => controller.current?.abort(), []); async function search(event) { event.preventDefault(); setRows([]); setSelected(null); try { const box=BuildingData.searchBounds(lat,lon,radius), key=box.join(','); setBusy(true); setMessage('Searching only the submitted area…'); let results=cache.current.get(key); if (!results) { controller.current = new AbortController(); const timeout=setTimeout(()=>controller.current.abort(),25000); try { const response=await fetch('https://overpass-api.de/api/interpreter',{method:'POST',body:new URLSearchParams({data:`[out:json][timeout:20][maxsize:8388608];way["building"](${key})->.buildings;.buildings out geom 151;(node["addr:housenumber"](${key});node["addr:full"](${key});node(w.buildings)["addr:housenumber"];);out body 2001;`}), signal:controller.current.signal}); if (!response.ok) throw new Error(`Building service unavailable (${response.status}). Try again shortly or upload a floorplan.`); const data=await response.json(); if (data.remark) throw new Error('Building service could not complete the query. Reduce the area and retry.'); results=BuildingData.describeBuildings(data.elements || [],Number(lat),Number(lon)); cache.current.set(key,results); } finally {clearTimeout(timeout);} } setRows(results.slice(0,150)); setMessage(results.length ? `${Math.min(results.length,150)} buildings found.${results.length>150?' Result limit reached; narrow the area.':''} Select an address below; closest buildings are listed first.` : 'No supported building footprints in this area. Try nearby coordinates or upload a floorplan.'); } catch(error) {setMessage(error.name==='AbortError'?'Search cancelled or timed out. Reduce the area and retry.':error.message);} finally {setBusy(false);} } const ring=selected?.geometry || []; const xy=ring.map(p=>[(p.lon-Number(lon))*Math.cos(Number(lat)*Math.PI/180),-p.lat]); const xs=xy.map(p=>p[0]),ys=xy.map(p=>p[1]); const w=Math.max(...xs)-Math.min(...xs)||1,h=Math.max(...ys)-Math.min(...ys)||1; const span=Math.max(w,h), points=xy.map(p=>`${25+(p[0]-Math.min(...xs))/span*200},${25+(p[1]-Math.min(...ys))/span*200}`).join(' '); return
{busy&&}

Small bounding-box search around your coordinates. No region-wide model download. Addresses come from mapped buildings and address points inside their footprints. Missing addresses are marked explicitly. Supports closed building ways; complex multipolygon buildings are not included.

{message}

{rows.map(row=>)}
{selected&&
{selected.label}View building on map ↗N ↑{BuildingData.heightInfo(selected.tags).source}. Exterior only; confirm height and materials.
}
© OpenStreetMap contributors · ODbL
; } function SignalStudio() { const [project,setProject]=useStudioState(null), [tab,setTab]=useStudioState('setup'), [mode,setMode]=useStudioState('plan'); const [name,setName]=useStudioState('Untitled project'), [technology,setTechnology]=useStudioState('cellular'); const [model,setModel]=useStudioState(null), [file,setFile]=useStudioState(null), [revision,setRevision]=useStudioState(0); const [error,setError]=useStudioState(''), [setup,setSetup]=useStudioState(true), [initialRF,setInitialRF]=useStudioState({}); const rfApi=useStudioRef(null), planApi=useStudioRef(null), openInput=useStudioRef(null); const [planRevision,setPlanRevision]=useStudioState(0); useStudioEffect(()=>{requestAnimationFrame(()=>window.dispatchEvent(new Event('resize')));},[tab,setup]); function begin(nextFile=null,nextModel=null,source=null) { setProject({name:name.trim()||'Untitled project',technology,source:source||{type:mode},createdAt:new Date().toISOString()}); setFile(nextFile); setModel(nextModel); setInitialRF({simulation:{frequencyMHz:technology==='wifi'?5180:3500},antennas:[createAntenna({powerDbm:technology==='wifi'?20:30,x:0,y:0,z:2.2})]}); planApi.current=null; rfApi.current=null; setPlanRevision(v=>v+1);setRevision(v=>v+1);setSetup(false);setTab(nextFile?'plan':'rf');setError(''); } function acceptPlanModel(nextModel) { const state=rfApi.current?.(); setInitialRF({...state?.scenario,slice:undefined,transform:{...DEFAULT_TRANSFORM}});setModel(nextModel);setRevision(v=>v+1);setTab('rf'); } function save() { const rf=rfApi.current?.() || {}; const {floorplan:previousPlan,...metadata}=project; const payload={format:'engever-rf-simulator',version:1,project:metadata,model:rf.model||model,scenario:rf.scenario||initialRF,floorplan:planApi.current?.()||previousPlan||null}; downloadBlob(new Blob([JSON.stringify(payload)],{type:'application/json'}),`${project.name.replace(/[^a-z0-9_-]/gi,'-')}.rfproject.json`); } async function open(event) { try { const f=event.target.files[0];if(!f)return; if(f.size>60*1024*1024)throw new Error('Project must be smaller than 60 MB.'); const data=JSON.parse(await f.text()); if(!['engever-rf-simulator','signal-studio'].includes(data.format)||data.version!==1||!data.project||typeof data.project.name!=='string'||(data.model&&(typeof data.model.text!=='string'||typeof data.model.name!=='string')))throw new Error('Choose a valid RF Simulator project file.'); if(data.scenario?.antennas && (!Array.isArray(data.scenario.antennas)||data.scenario.antennas.some(a=>!a||typeof a.id!=='string'||!['x','y','z','powerDbm','azimuthDeg','tiltDeg'].every(k=>Number.isFinite(a[k])))))throw new Error('Project transmitters contain invalid coordinates or power settings.'); if(data.scenario?.slice) CoverageGeometry.validate(data.scenario.slice); if(data.scenario?.crowd) CrowdEngine.validate(data.scenario.crowd); CrowdEngine.validatePeople(data.scenario?.people); if(data.scenario?.viewMetric&&!['power','snr','sinr'].includes(data.scenario.viewMetric))throw new Error('Invalid saved heatmap metric.'); if(data.floorplan && (!Array.isArray(data.floorplan.walls)||!Array.isArray(data.floorplan.footprintPoints)||!Number.isFinite(data.floorplan.metersPerPixel)))throw new Error('The saved floorplan is incomplete.'); setProject({...data.project,floorplan:data.floorplan});setName(data.project.name);setTechnology(data.project.technology);setModel(data.model);setFile(null);setInitialRF(data.scenario||{});planApi.current=null;rfApi.current=null;setPlanRevision(v=>v+1);setRevision(v=>v+1);setTab(data.model?'rf':'plan');setSetup(false);setError(''); }catch(e){setError(e.message);}finally{event.target.value='';} } return
Projects / {project?.name||'New project'}
{project&&<>}
{error&&
{error}
} {setup&&
PLAN WITH CONFIDENCE

Every great network
starts with a solid plan.

Bring your building into focus. Build the environment, place your radios,
and explore coverage in one connected workspace.

Choose your starting point

01 / BUILDING SOURCE
{mode==='plan'?<>

Bring your site drawings

Images and PDFs open in the floorplan editor. Existing OBJ models go directly to RF design.

Your drawings stay in this browser. Save a project file to keep your work.:begin(null,m,s)}/>}
1Build the environmentScale, footprints and walls
2Design the networkMaterials and transmitters
3Evaluate coverageFloor coverage and exports
} {project&&<>}
; } ReactDOM.createRoot(document.getElementById('root')).render();