{"id":11,"date":"2026-08-17T16:51:38","date_gmt":"2026-08-17T16:51:38","guid":{"rendered":"https:\/\/android.dk\/?page_id=11"},"modified":"2026-08-17T16:51:38","modified_gmt":"2026-08-17T16:51:38","slug":"target-locator","status":"publish","type":"page","link":"https:\/\/android.dk\/index.php\/target-locator\/","title":{"rendered":"TARGET LOCATOR"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\">\n<title>Target Locator \u2014 Drone Camera Geolocation<\/title>\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Roboto+Mono:wght@400;500;700&#038;family=Inter:wght@400;500;600;700&#038;display=swap\" rel=\"stylesheet\">\n<style>\n  :root{\n    --bg:#0a0f0c;\n    --panel:#11170f;\n    --panel-2:#161d13;\n    --line:#26332a;\n    --line-bright:#3a4a3e;\n    --green:#6ee7a0;\n    --green-dim:#3f7a5c;\n    --amber:#f2b84b;\n    --red:#e2504a;\n    --text:#e8ece6;\n    --text-dim:#8fa392;\n    --text-faint:#5c6b60;\n  }\n  *{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}\n  html,body{margin:0;padding:0;background:var(--bg);color:var(--text);}\n  body{\n    font-family:'Inter',system-ui,sans-serif;\n    padding:16px;\n    padding-bottom:48px;\n    max-width:900px;\n    margin:0 auto;\n    min-height:100vh;\n  }\n  .mono{font-family:'Roboto Mono',ui-monospace,monospace;}\n\n  header{\n    display:flex; align-items:baseline; justify-content:space-between;\n    border-bottom:1px solid var(--line-bright);\n    padding-bottom:12px; margin-bottom:18px;\n  }\n  header h1{\n    font-size:18px; font-weight:700; letter-spacing:0.02em; margin:0;\n    color:var(--green);\n  }\n  header .sub{font-size:11px; color:var(--text-faint); letter-spacing:0.08em; text-transform:uppercase;}\n\n  .card{\n    background:var(--panel);\n    border:1px solid var(--line);\n    border-radius:6px;\n    padding:16px;\n    margin-bottom:14px;\n  }\n  .card-title{\n    font-size:11px; font-weight:700; letter-spacing:0.1em; text-transform:uppercase;\n    color:var(--text-dim); margin:0 0 14px 0;\n    display:flex; align-items:center; gap:8px;\n  }\n  .card-title::before{content:''; width:6px; height:6px; background:var(--green-dim); border-radius:1px; display:inline-block;}\n\n  .row{display:grid; grid-template-columns:1fr 1fr; gap:12px;}\n  @media (max-width:520px){ .row{grid-template-columns:1fr;} }\n\n  .field{margin-bottom:14px;}\n  .field label{\n    display:flex; justify-content:space-between; align-items:baseline;\n    font-size:12px; color:var(--text-dim); margin-bottom:6px;\n  }\n  .field label .val{font-family:'Roboto Mono',monospace; color:var(--green); font-size:13px; font-weight:500;}\n  .field input[type=\"text\"], .field input[type=\"number\"]{\n    width:100%;\n    background:var(--panel-2);\n    border:1px solid var(--line);\n    color:var(--text);\n    font-family:'Roboto Mono',monospace;\n    font-size:16px;\n    padding:10px 12px;\n    border-radius:4px;\n    outline:none;\n  }\n  .field input[type=\"text\"]:focus, .field input[type=\"number\"]:focus{border-color:var(--green-dim);}\n  .field input[type=\"range\"]{width:100%; accent-color:var(--green);}\n  .hint{font-size:11px; color:var(--text-faint); margin-top:4px; line-height:1.4;}\n\n  .readout{\n    background:var(--panel-2);\n    border:1px solid var(--line-bright);\n    border-radius:6px;\n    padding:18px;\n    text-align:center;\n  }\n  .readout .mgrs{\n    font-family:'Roboto Mono',monospace;\n    font-size:28px; font-weight:700; color:var(--green);\n    letter-spacing:0.03em;\n    word-break:break-all;\n  }\n  .readout .latlon{\n    font-family:'Roboto Mono',monospace;\n    font-size:13px; color:var(--text-dim); margin-top:6px;\n  }\n  .readout.invalid .mgrs{color:var(--text-faint); font-size:16px; font-weight:500;}\n\n  .stat-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:10px; margin-top:14px;}\n  @media (max-width:520px){ .stat-grid{grid-template-columns:1fr 1fr;} }\n  .stat{background:var(--panel-2); border:1px solid var(--line); border-radius:4px; padding:10px 12px;}\n  .stat .k{font-size:10px; color:var(--text-faint); text-transform:uppercase; letter-spacing:0.06em;}\n  .stat .v{font-family:'Roboto Mono',monospace; font-size:16px; color:var(--text); margin-top:2px;}\n  .stat .v.warn{color:var(--amber);}\n  .stat .v.bad{color:var(--red);}\n\n  .banner{\n    display:flex; gap:10px; align-items:flex-start;\n    padding:10px 12px; border-radius:4px; font-size:12.5px; line-height:1.5;\n    margin-top:12px;\n  }\n  .banner.amber{background:rgba(242,184,75,0.08); border:1px solid rgba(242,184,75,0.35); color:var(--amber);}\n  .banner.red{background:rgba(226,80,74,0.08); border:1px solid rgba(226,80,74,0.35); color:var(--red);}\n  .banner .dot{flex-shrink:0; margin-top:4px; width:6px; height:6px; border-radius:50%; background:currentColor;}\n\n  #compass{display:block; margin:0 auto;}\n\n  details{margin-top:2px;}\n  summary{\n    cursor:pointer; font-size:12px; color:var(--text-dim); padding:8px 0;\n    list-style:none; display:flex; align-items:center; gap:6px;\n  }\n  summary::-webkit-details-marker{display:none;}\n  summary::before{content:'\u25b8'; font-size:10px; color:var(--green-dim);}\n  details[open] summary::before{content:'\u25be';}\n\n  .btn{\n    background:var(--panel-2); border:1px solid var(--line-bright); color:var(--text);\n    font-family:'Inter',sans-serif; font-size:13px; font-weight:500;\n    padding:9px 14px; border-radius:4px; cursor:pointer;\n  }\n  .btn:active{background:var(--line);}\n  .btn.copy{color:var(--green); border-color:var(--green-dim);}\n\n  footer{text-align:center; font-size:10.5px; color:var(--text-faint); margin-top:22px; line-height:1.6;}\n<\/style>\n<\/head>\n<body>\n\n<header>\n  <h1>TARGET LOCATOR<\/h1>\n  <div class=\"sub\">Camera geolocation<\/div>\n<\/header>\n\n<div class=\"card\">\n  <div class=\"card-title\">Drone telemetry (read from controller app)<\/div>\n\n  <div class=\"field\">\n    <label>Drone position (MGRS)<\/label>\n    <input type=\"text\" id=\"mgrs\" class=\"mono\" placeholder=\"e.g. 32VNH1234567890\" autocomplete=\"off\" autocapitalize=\"characters\" spellcheck=\"false\">\n    <div class=\"hint\">Paste or type exactly as shown on the drone app. Any precision (10m\u20131m) works.<\/div>\n  <\/div>\n\n  <div class=\"row\">\n    <div class=\"field\">\n      <label>Altitude above launch point <span class=\"val\" id=\"altVal\">60 m<\/span><\/label>\n      <input type=\"range\" id=\"alt\" min=\"1\" max=\"500\" step=\"1\" value=\"60\">\n    <\/div>\n    <div class=\"field\">\n      <label>Camera heading <span class=\"val\" id=\"hdgVal\">090\u00b0<\/span><\/label>\n      <input type=\"range\" id=\"hdg\" min=\"0\" max=\"359\" step=\"1\" value=\"90\">\n      <div class=\"hint\">Gimbal\/camera pointing direction, true north bearing.<\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"field\">\n    <label>Camera angle below horizon <span class=\"val\" id=\"angVal\">35\u00b0<\/span><\/label>\n    <input type=\"range\" id=\"ang\" min=\"1\" max=\"89\" step=\"1\" value=\"35\">\n    <div class=\"hint\">0\u00b0 = level with horizon, 90\u00b0 = straight down. If your gimbal readout shows negative pitch (e.g. \u221235\u00b0), enter 35.<\/div>\n  <\/div>\n\n  <div class=\"field\">\n    <label>Target elevation vs. launch point <span class=\"val\" id=\"elevVal\">0 m<\/span><\/label>\n    <input type=\"range\" id=\"elev\" min=\"-100\" max=\"100\" step=\"1\" value=\"0\">\n    <div class=\"hint\">Guesstimate. Positive = target is higher than launch point (e.g. building roof, hillside). Leave at 0 if unknown \/ flat ground.<\/div>\n  <\/div>\n<\/div>\n\n<div class=\"card\">\n  <div class=\"card-title\">Estimated target location<\/div>\n  <div id=\"readout\" class=\"readout\">\n    <div class=\"mgrs mono\" id=\"outMgrs\">\u2014<\/div>\n    <div class=\"latlon\" id=\"outLatLon\">\u2014<\/div>\n  <\/div>\n\n  <svg id=\"compass\" width=\"140\" height=\"140\" viewBox=\"0 0 140 140\" style=\"margin-top:16px;\"><\/svg>\n\n  <div class=\"stat-grid\">\n    <div class=\"stat\"><div class=\"k\">Horiz. distance<\/div><div class=\"v\" id=\"statDist\">\u2014<\/div><\/div>\n    <div class=\"stat\"><div class=\"k\">Slant range<\/div><div class=\"v\" id=\"statSlant\">\u2014<\/div><\/div>\n    <div class=\"stat\"><div class=\"k\">Est. error radius<\/div><div class=\"v\" id=\"statErr\">\u2014<\/div><\/div>\n  <\/div>\n\n  <div id=\"warnings\"><\/div>\n\n  <details>\n    <summary>Uncertainty inputs<\/summary>\n    <div class=\"row\" style=\"margin-top:10px;\">\n      <div class=\"field\">\n        <label>Altitude uncertainty <span class=\"val\" id=\"eAltVal\">\u00b15 m<\/span><\/label>\n        <input type=\"range\" id=\"eAlt\" min=\"0\" max=\"30\" step=\"1\" value=\"5\">\n      <\/div>\n      <div class=\"field\">\n        <label>Camera angle uncertainty <span class=\"val\" id=\"eAngVal\">\u00b11\u00b0<\/span><\/label>\n        <input type=\"range\" id=\"eAng\" min=\"0\" max=\"10\" step=\"0.5\" value=\"1\">\n      <\/div>\n    <\/div>\n    <div class=\"field\">\n      <label>Heading uncertainty <span class=\"val\" id=\"eHdgVal\">\u00b12\u00b0<\/span><\/label>\n      <input type=\"range\" id=\"eHdg\" min=\"0\" max=\"15\" step=\"0.5\" value=\"2\">\n    <\/div>\n    <div class=\"hint\">Error radius combines range error (from altitude + angle uncertainty) and cross-track error (from heading uncertainty) as a rough circular estimate \u2014 treat it as a search radius around the point, not a precise bound.<\/div>\n  <\/details>\n\n  <div style=\"margin-top:14px; display:flex; gap:8px;\">\n    <button class=\"btn copy\" id=\"copyMgrs\">Copy target MGRS<\/button>\n    <button class=\"btn\" id=\"copyLatLon\">Copy lat\/lon<\/button>\n  <\/div>\n<\/div>\n\n<footer>\n  Assumes flat terrain at the entered target elevation and a straight optical line of sight \u2014 not corrected for obstructions.<br>\n  Coordinates give the point at the center of the camera frame. Off-center targets need manual field-of-view correction.\n<\/footer>\n\n<script type=\"module\">\nimport { toPoint, forward } from 'https:\/\/esm.sh\/mgrs@2.0.0';\n\nconst $ = id => document.getElementById(id);\nconst altS=$('alt'), hdgS=$('hdg'), angS=$('ang'), elevS=$('elev');\nconst eAltS=$('eAlt'), eAngS=$('eAng'), eHdgS=$('eHdg');\nconst mgrsIn=$('mgrs');\n\nfunction fmt(n, unit, decimals=0){ return `${n.toFixed(decimals)}${unit}`; }\n\nfunction updateLabels(){\n  $('altVal').textContent = fmt(+altS.value,' m');\n  $('hdgVal').textContent = (+hdgS.value).toString().padStart(3,'0')+'\u00b0';\n  $('angVal').textContent = fmt(+angS.value,'\u00b0');\n  $('elevVal').textContent = (+elevS.value>=0?'+':'') + elevS.value + ' m';\n  $('eAltVal').textContent = '\u00b1'+eAltS.value+' m';\n  $('eAngVal').textContent = '\u00b1'+eAngS.value+'\u00b0';\n  $('eHdgVal').textContent = '\u00b1'+eHdgS.value+'\u00b0';\n}\n\nfunction drawCompass(headingDeg, valid){\n  const svg = $('compass');\n  const cx=70, cy=70, r=58;\n  const rad = headingDeg*Math.PI\/180;\n  const tx = cx + r*Math.sin(rad);\n  const ty = cy - r*Math.cos(rad);\n  const col = valid ? 'var(--green)' : 'var(--text-faint)';\n  svg.innerHTML = `\n    <circle cx=\"${cx}\" cy=\"${cy}\" r=\"${r}\" fill=\"none\" stroke=\"#26332a\" stroke-width=\"1\"\/>\n    <circle cx=\"${cx}\" cy=\"${cy}\" r=\"3\" fill=\"#5c6b60\"\/>\n    <text x=\"${cx}\" y=\"16\" text-anchor=\"middle\" fill=\"#5c6b60\" font-size=\"11\" font-family=\"Roboto Mono, monospace\">N<\/text>\n    <text x=\"${cx}\" y=\"${cy+r+16}\" text-anchor=\"middle\" fill=\"#5c6b60\" font-size=\"11\" font-family=\"Roboto Mono, monospace\">S<\/text>\n    <text x=\"8\" y=\"${cy+4}\" fill=\"#5c6b60\" font-size=\"11\" font-family=\"Roboto Mono, monospace\">W<\/text>\n    <text x=\"${cx+r+6}\" y=\"${cy+4}\" fill=\"#5c6b60\" font-size=\"11\" font-family=\"Roboto Mono, monospace\">E<\/text>\n    <line x1=\"${cx}\" y1=\"${cy}\" x2=\"${tx}\" y2=\"${ty}\" stroke=\"${col}\" stroke-width=\"2.5\" stroke-linecap=\"round\"\/>\n    <circle cx=\"${tx}\" cy=\"${ty}\" r=\"4\" fill=\"${col}\"\/>\n  `;\n}\n\nfunction compute(){\n  updateLabels();\n\n  const alt = +altS.value;\n  const hdg = +hdgS.value;\n  const ang = +angS.value;\n  const elev = +elevS.value;\n  const eAlt = +eAltS.value;\n  const eAng = +eAngS.value;\n  const eHdg = +eHdgS.value;\n\n  drawCompass(hdg, true);\n\n  const readout = $('readout');\n  const warningsEl = $('warnings');\n  warningsEl.innerHTML = '';\n\n  let lonlat;\n  try{\n    lonlat = toPoint(mgrsIn.value.trim().toUpperCase().replace(\/\\s+\/g,''));\n    if(!lonlat || isNaN(lonlat[0]) || isNaN(lonlat[1])) throw new Error();\n  }catch(e){\n    readout.classList.add('invalid');\n    $('outMgrs').textContent = mgrsIn.value.trim() ? 'Invalid MGRS format' : 'Enter drone MGRS above';\n    $('outLatLon').textContent = '\u2014';\n    $('statDist').textContent='\u2014'; $('statSlant').textContent='\u2014'; $('statErr').textContent='\u2014';\n    return;\n  }\n  readout.classList.remove('invalid');\n\n  const [droneLon, droneLat] = lonlat;\n\n  const h = alt - elev; \/\/ height above target\n  const thetaRad = ang * Math.PI\/180;\n\n  if(h <= 0){\n    $('outMgrs').textContent = 'Target elevation \u2265 drone altitude';\n    $('outLatLon').textContent = 'Lower the target elevation guess or the drone can\\'t see it';\n    $('statDist').textContent='\u2014'; $('statSlant').textContent='\u2014'; $('statErr').textContent='\u2014';\n    warningsEl.innerHTML = `<div class=\"banner red\"><div class=\"dot\"><\/div><div>Height above target is zero or negative \u2014 the entered target elevation is at or above the drone. Check the altitude and target elevation values.<\/div><\/div>`;\n    return;\n  }\n\n  const d = h \/ Math.tan(thetaRad);\n  const slant = h \/ Math.sin(thetaRad);\n\n  const hdgRad = hdg * Math.PI\/180;\n  const dN = d * Math.cos(hdgRad);\n  const dE = d * Math.sin(hdgRad);\n\n  const R = 6378137; \/\/ meters, WGS84 equatorial radius, fine for local offsets\n  const dLat = (dN \/ R) * (180\/Math.PI);\n  const dLon = (dE \/ (R * Math.cos(droneLat*Math.PI\/180))) * (180\/Math.PI);\n\n  const targetLat = droneLat + dLat;\n  const targetLon = droneLon + dLon;\n\n  let targetMgrs;\n  try{\n    targetMgrs = forward([targetLon, targetLat], 5);\n  }catch(e){\n    targetMgrs = 'Conversion error';\n  }\n\n  $('outMgrs').textContent = targetMgrs;\n  $('outLatLon').textContent = `${targetLat.toFixed(6)}\u00b0, ${targetLon.toFixed(6)}\u00b0`;\n\n  $('statDist').textContent = d < 1000 ? Math.round(d)+' m' : (d\/1000).toFixed(2)+' km';\n  $('statSlant').textContent = slant < 1000 ? Math.round(slant)+' m' : (slant\/1000).toFixed(2)+' km';\n\n  \/\/ error propagation\n  const eAngRad = eAng * Math.PI\/180;\n  const dd_dh = 1\/Math.tan(thetaRad);\n  const dd_dtheta = -h \/ (Math.sin(thetaRad)**2);\n  const rangeErr = Math.sqrt((dd_dh*eAlt)**2 + (dd_dtheta*eAngRad)**2);\n  const crossErr = d * (eHdg*Math.PI\/180);\n  const errRadius = Math.sqrt(rangeErr**2 + crossErr**2);\n\n  const errStat = $('statErr');\n  errStat.textContent = errRadius < 1000 ? Math.round(errRadius)+' m' : (errRadius\/1000).toFixed(2)+' km';\n  errStat.className = 'v' + (errRadius > d*0.5 ? ' bad' : errRadius > d*0.2 ? ' warn' : '');\n\n  if(ang < 15){\n    warningsEl.innerHTML += `<div class=\"banner amber\"><div class=\"dot\"><\/div><div>Shallow camera angle (${ang}\u00b0) \u2014 small angle errors translate into large distance errors at this grazing angle. Steepen the shot if possible before relying on this fix.<\/div><\/div>`;\n  }\n  if(errRadius > d*0.5 && d>0){\n    warningsEl.innerHTML += `<div class=\"banner red\"><div class=\"dot\"><\/div><div>Error radius is more than half the target distance. Treat this as a rough search area, not a precise pin.<\/div><\/div>`;\n  }\n}\n\n[altS,hdgS,angS,elevS,eAltS,eAngS,eHdgS].forEach(el=>el.addEventListener('input', compute));\nmgrsIn.addEventListener('input', compute);\n\n$('copyMgrs').addEventListener('click', ()=>{\n  const t = $('outMgrs').textContent;\n  if(t && t!=='\u2014') navigator.clipboard?.writeText(t);\n});\n$('copyLatLon').addEventListener('click', ()=>{\n  const t = $('outLatLon').textContent;\n  if(t && t!=='\u2014') navigator.clipboard?.writeText(t);\n});\n\ncompute();\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Target Locator \u2014 Drone Camera Geolocation TARGET LOCATOR Camera geolocation Drone telemetry (read from controller app) Drone position (MGRS) Paste or type exactly as shown on the drone app. Any precision (10m\u20131m) works. Altitude above launch point 60 m Camera heading 090\u00b0 Gimbal\/camera pointing direction, true north bearing. Camera angle below horizon 35\u00b0 0\u00b0 = [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"inline_featured_image":false,"footnotes":""},"class_list":["post-11","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/pages\/11","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/comments?post=11"}],"version-history":[{"count":3,"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/pages\/11\/revisions"}],"predecessor-version":[{"id":15,"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/pages\/11\/revisions\/15"}],"wp:attachment":[{"href":"https:\/\/android.dk\/index.php\/wp-json\/wp\/v2\/media?parent=11"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}