stepstead-web/assets/map-editor.html
jts dea01dd6a8 İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:59 +03:00

758 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex,nofollow">
<title>Stepstead — Harita Editörü</title>
<style>
:root{--bg:#0f1419;--panel:#1a2230;--line:#2c3a4f;--txt:#e6edf3;--muted:#8b99a8;--gold:#e8b923;--ok:#2ea043;--err:#e5534b}
*{box-sizing:border-box}
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--txt);line-height:1.5}
.wrap{max-width:1500px;margin:0 auto;padding:16px}
h1{font-size:20px;color:var(--gold);margin:0 0 4px}
.sub{color:var(--muted);font-size:13px;margin-bottom:14px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;margin-bottom:14px}
input,button,select,textarea{font-size:14px;border-radius:6px}
input,select,textarea{background:#222d3d;color:var(--txt);border:1px solid var(--line);padding:8px 10px}
input[type=email],input[type=password]{width:100%;margin-bottom:10px}
textarea{width:100%;min-height:54px;resize:vertical}
.btn{background:var(--gold);color:#1a1300;border:none;padding:8px 16px;font-weight:600;cursor:pointer}
.btn.sec{background:#2a3648;color:var(--txt)}
.btn.danger{background:var(--err);color:#fff}
.btn:disabled{opacity:.45;cursor:default}
.msg{font-size:13px;margin-top:8px}
.msg.ok{color:var(--ok)} .msg.err{color:var(--err)}
.hide{display:none}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
.toolbar .grp{display:flex;gap:6px;align-items:center;background:#141c28;border:1px solid var(--line);border-radius:8px;padding:6px 8px}
.modebtn{background:#2a3648;color:var(--txt);border:1px solid var(--line);padding:7px 12px;cursor:pointer;border-radius:6px}
.modebtn.on{background:var(--gold);color:#1a1300;border-color:var(--gold);font-weight:600}
.maparea{position:relative;width:100%;user-select:none;line-height:0;margin-bottom:14px}
#mapview{position:relative;width:100%}
#mapzoom{position:relative;width:100%;transform-origin:0 0}
#mapimg{width:100%;display:block;border-radius:8px;border:1px solid var(--line)}
#svg{position:absolute;inset:0;width:100%;height:100%;cursor:crosshair}
/* tam ekran düzenleme modu */
.maparea.fs{position:fixed;inset:0;z-index:2000;width:100vw;height:100vh;height:100dvh;margin:0;overflow:hidden;background:#0b0f14;display:flex;flex-direction:column}
.maparea.fs #mapview{flex:1;min-height:0;overflow:hidden;touch-action:none}
.maparea.fs #mapimg{border-radius:0;border:none}
.toolbar.fsfloat{flex:0 0 auto;margin:0;background:#141c28;border:none;border-bottom:1px solid var(--line);border-radius:0;padding:6px 8px;max-height:42vh;overflow:auto}
.maparea.fs #fsOpenBtn{display:none}
.zoomctl{display:none}
.maparea.fs .zoomctl{display:flex;position:absolute;right:10px;bottom:10px;z-index:5;flex-direction:column;gap:6px}
.zoomctl button{width:44px;height:44px;font-size:20px;font-weight:700;background:rgba(20,28,40,.92);color:var(--txt);border:1px solid var(--line);border-radius:8px;cursor:pointer;line-height:1;display:flex;align-items:center;justify-content:center}
.zoomctl button:hover{border-color:var(--gold)}
#fsClose{display:none}
.maparea.fs #fsClose{display:inline-flex}
#formCard{max-width:560px;margin-bottom:14px}
.taggroups{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;align-items:start}
@media(max-width:900px){.taggroups{grid-template-columns:1fr}}
.taggroups .card{margin-bottom:0}
.grouphead{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin-bottom:6px}
.grouphead .cnt{color:var(--muted);font-size:12px;font-weight:400}
.taglist{max-height:340px;overflow:auto}
.gempty{color:var(--muted);font-size:12px;padding:6px 2px}
.tagrow{display:flex;align-items:center;gap:8px;padding:6px 4px;border-top:1px solid var(--line);font-size:13px;cursor:pointer}
.tagrow:hover{background:#141c28}
.tagrow.sel{background:#243247}
.swatch{width:12px;height:12px;border-radius:3px;flex:0 0 12px}
.knd{color:var(--muted);font-size:11px;margin-left:auto}
.delx{flex:0 0 auto;background:none;border:none;color:var(--muted);cursor:pointer;font-size:15px;padding:0 4px;line-height:1}
.delx:hover{color:var(--err)}
.hint{color:var(--muted);font-size:12px;margin-top:8px}
label{font-size:12px;color:var(--muted);display:block;margin:8px 0 3px}
.row{display:flex;gap:8px}
.row>*{flex:1}
.regchecks{display:flex;flex-wrap:wrap;gap:6px;max-height:140px;overflow:auto;background:#141c28;border:1px solid var(--line);border-radius:6px;padding:8px}
.regchecks label{display:inline-flex;align-items:center;gap:5px;margin:0;font-size:12px;color:var(--txt);background:#222d3d;border:1px solid var(--line);border-radius:14px;padding:3px 9px;cursor:pointer}
.regchecks label:hover{border-color:var(--gold)}
.regchecks input{width:auto;margin:0}
.regchecks label.auto{border-color:var(--ok)}
.regchecks .gempty{color:var(--muted);font-size:12px}
polygon,polyline,circle,text{vector-effect:non-scaling-stroke}
#svg.draw *{pointer-events:none}
.veclabel{fill:#fff;paint-order:stroke;stroke:#000;stroke-width:3px;font-weight:600}
</style>
<script src="/nav.js?v=8" data-theme="dark" defer></script>
</head>
<body>
<div class="wrap">
<h1>🗺 Harita Editörü</h1>
<div class="sub">Haritada kıta/bölge/nokta işaretle, lokasyonlar arası yol çiz → DB'ye kaydet. Kıtalar: Eldoria · Kaldün · Meridia</div>
<div class="card" id="loginCard">
<input type="email" id="email" placeholder="E-posta" autocomplete="username">
<input type="password" id="pass" placeholder="Şifre" autocomplete="current-password">
<button class="btn" id="loginBtn">Giriş</button>
<div class="msg" id="loginMsg"></div>
</div>
<div id="editor" class="hide">
<div class="toolbar">
<div class="grp">
<span style="color:var(--muted);font-size:12px">Mod:</span>
<button class="modebtn on" data-mode="select">👆 Seç</button>
<button class="modebtn" data-mode="region">▱ Bölge çiz</button>
<button class="modebtn" data-mode="poi">📍 Nokta koy</button>
<button class="modebtn" data-mode="road">🛣 Yol çiz</button>
</div>
<div class="grp" id="drawCtl" style="display:none">
<span id="drawInfo" style="font-size:12px;color:var(--muted)">0 köşe</span>
<button class="modebtn" id="undoPt">↩︎ Geri al</button>
<button class="btn" id="finishBtn">✓ Bitir</button>
<button class="modebtn" id="cancelDraw">✕ İptal</button>
</div>
<div class="grp" id="roadCtl" style="display:none">
<span id="roadInfo" style="font-size:12px;color:var(--muted)">Başlangıç lokasyonuna tıkla</span>
<button class="modebtn" id="undoWp">↩︎ Geri al</button>
<button class="modebtn" id="cancelRoad">✕ İptal</button>
</div>
<div class="grp" style="gap:10px">
<span style="color:var(--muted);font-size:12px">Göster:</span>
<label style="margin:0"><input type="checkbox" id="showLabels" checked style="width:auto"> İsimler</label>
<label style="margin:0"><input type="checkbox" id="showRegions" checked style="width:auto"> Bölgeler</label>
<label style="margin:0"><input type="checkbox" id="showRoads" checked style="width:auto"> Yollar</label>
<label style="margin:0"><input type="checkbox" id="showPois" checked style="width:auto"> Noktalar</label>
</div>
<div class="grp">
<button class="btn" id="fsOpenBtn" title="Haritayı tam ekranda düzenle">🖊 Tam ekran düzenle</button>
</div>
</div>
<div class="maparea" id="maparea">
<div id="mapview">
<div id="mapzoom">
<img id="mapimg" src="/assets/world_map@4x.webp?v=2" alt="Dünya haritası">
<svg id="svg" viewBox="0 0 1 1" preserveAspectRatio="none"></svg>
</div>
</div>
<div class="zoomctl">
<button type="button" id="fsClose" title="Tam ekranı kapat" style="background:var(--gold);color:#1a1300"></button>
<button type="button" id="zoomIn" title="Yakınlaştır">+</button>
<button type="button" id="zoomOut" title="Uzaklaştır"></button>
<button type="button" id="zoomReset" title="Sığdır" style="font-size:15px"></button>
</div>
</div>
<div class="card hide" id="formCard">
<b id="formTitle" style="font-size:14px">Yeni etiket</b>
<label>İsim</label>
<input type="text" id="fName" placeholder="örn. Fısıltı Ormanı">
<div class="row">
<div>
<label>Tür</label>
<select id="fKind">
<option value="region">Bölge</option>
<option value="continent">Kıta</option>
<option value="poi">Nokta (POI)</option>
</select>
</div>
<div>
<label>Kıta</label>
<select id="fCont">
<option value=""></option>
<option>Eldoria</option>
<option>Kaldün</option>
<option>Meridia</option>
</select>
</div>
</div>
<div id="catWrap">
<label>Kategori <span style="color:var(--muted)">(tip — ikon/renk otomatik gelir)</span></label>
<select id="fCategory"><option value="">— (belirsiz) —</option></select>
</div>
<div id="regionsWrap" class="hide">
<label>Bölge(ler) — nokta hangi bölgede? <span style="color:var(--muted)">(içinde olduğu bölgeler otomatik işaretlendi)</span></label>
<div id="fRegions" class="regchecks"></div>
</div>
<label>Not (opsiyonel)</label>
<textarea id="fNote" placeholder="lore / açıklama"></textarea>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="btn" id="saveBtn">Kaydet</button>
<button class="btn sec" id="closeForm">Vazgeç</button>
<button class="btn danger hide" id="delBtn" style="margin-left:auto">Sil</button>
</div>
<div class="msg" id="formMsg"></div>
</div>
<div class="card hide" id="roadForm" style="max-width:560px">
<b id="roadTitle" style="font-size:14px">Yeni yol</b>
<div style="font-size:13px;margin:8px 0" id="roadEnds"></div>
<div class="row">
<div>
<label>Adım maliyeti</label>
<input type="number" id="rCost" min="1" max="100000" value="300">
</div>
<div>
<label>Olay şansı (0-1)</label>
<input type="number" id="rEvent" min="0" max="1" step="0.05" value="0.3">
</div>
</div>
<div class="hint" id="rWpInfo">0 ara nokta</div>
<div style="display:flex;gap:8px;margin-top:12px">
<button class="btn" id="saveRoad">Kaydet</button>
<button class="btn sec" id="closeRoad">Vazgeç</button>
<button class="btn danger hide" id="delRoadBtn" style="margin-left:auto">Sil</button>
</div>
<div class="msg" id="roadMsg"></div>
</div>
<div class="taggroups">
<div class="card">
<div class="grouphead">🌍 Kıtalar <span class="cnt" id="cntContinent">0</span></div>
<div class="taglist" id="listContinent"></div>
</div>
<div class="card">
<div class="grouphead">▱ Bölgeler <span class="cnt" id="cntRegion">0</span></div>
<div class="taglist" id="listRegion"></div>
</div>
<div class="card">
<div class="grouphead">📍 Noktalar <span class="cnt" id="cntPoi">0</span></div>
<div class="taglist" id="listPoi"></div>
</div>
<div class="card">
<div class="grouphead">🛣 Yollar <span class="cnt" id="cntRoad">0</span></div>
<div class="taglist" id="listRoad"></div>
</div>
</div>
<div class="hint" id="listHint">Yükleniyor…</div>
</div>
</div>
<script>
const API="https://api.stepstead.com";
const SVGNS="http://www.w3.org/2000/svg";
let TOKEN=localStorage.getItem("ss_upload_token")||"";
let CAN_EDIT=false; // map_edit izni — yoksa salt okunur (map_view)
let mode="select";
let tags=[]; // sunucudan gelen etiketler
let draft=[]; // çizilmekte olan köşeler [[x,y],...]
let editingId=null; // seçili/düzenlenen etiket id (null = yeni)
let draftPoint=null; // poi için tek nokta
let locs=[]; // lokasyonlar (yol uçları) — {id,code,name,type,x,y}
let roads=[]; // yollar — {id,a,b,step_cost,event_chance,waypoints}
let roadA=null,roadB=null;// çizilen yolun uç lokasyon code'ları
let roadWps=[]; // çizilen yolun ara noktaları [[x,y],...]
let roadEditId=null; // düzenlenen yol id (null = yeni)
const $=id=>document.getElementById(id);
const CONT_COLOR={Eldoria:"#3fa34d",["Kaldün"]:"#e0632a",Meridia:"#3a7bd5"};
let CATS={}; // kategori kataloğu {key:{kind,label,icon,color}}
let VENDOR_CATS=[],QUEST_CATS=[];
function cat(k){return k&&CATS[k]?CATS[k]:null;}
function catIcon(k){return cat(k)?cat(k).icon:"";}
function catLabel(k){return cat(k)?cat(k).label:"";}
function colorFor(t){return t.color||(cat(t.category)?cat(t.category).color:null)||CONT_COLOR[t.continent]||"#9aa7b5";}
function catOptions(kind,selected){
let html='<option value="">— (belirsiz) —</option>';
for(const k in CATS)if(CATS[k].kind===kind)
html+=`<option value="${k}"${k===selected?" selected":""}>${CATS[k].icon} ${esc(CATS[k].label)}</option>`;
return html;
}
async function loadCats(){
const r=await api("/map-tags/categories");
if(r.ok){CATS=r.body.categories||{};VENDOR_CATS=r.body.vendor||[];QUEST_CATS=r.body.questgiver||[];}
}
async function api(path,opts={}){
opts.headers=Object.assign({"Content-Type":"application/json"},opts.headers,{Authorization:"Bearer "+TOKEN});
const r=await fetch(API+path,opts);
let body={};try{body=await r.json();}catch(e){}
return {ok:r.ok,status:r.status,body};
}
/* ---- auth ---- */
async function login(){
$("loginMsg").textContent="…";
const r=await fetch(API+"/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},
body:JSON.stringify({email:$("email").value.trim(),password:$("pass").value})});
const b=await r.json().catch(()=>({}));
if(r.ok&&b.access_token){TOKEN=b.access_token;localStorage.setItem("ss_upload_token",TOKEN);showEditor();}
else{$("loginMsg").className="msg err";$("loginMsg").textContent=b.detail||"Giriş başarısız";}
}
async function showEditor(){
// izin kontrolü: map_edit → tam yetki, map_view → salt okunur, yoksa red
try{
const r=await fetch(API+"/auth/me",{headers:{Authorization:"Bearer "+TOKEN}});
if(!r.ok){
if(r.status===401){TOKEN="";localStorage.removeItem("ss_upload_token");}
$("loginMsg").className="msg err";$("loginMsg").textContent="Oturum geçersiz — tekrar giriş yap";
return;
}
const u=await r.json();
const perms=u.permissions||[];
const has=p=>perms.includes("*")||perms.includes(p);
CAN_EDIT=has("map_edit");
if(!CAN_EDIT&&!has("map_view")){
$("loginMsg").className="msg err";$("loginMsg").textContent="Bu hesabın harita editörü yetkisi yok";
return;
}
}catch(e){$("loginMsg").className="msg err";$("loginMsg").textContent="Bağlantı hatası";return;}
$("loginCard").classList.add("hide");$("editor").classList.remove("hide");
if(!CAN_EDIT){
document.querySelectorAll('.modebtn[data-mode="region"],.modebtn[data-mode="poi"],.modebtn[data-mode="road"]').forEach(b=>b.style.display="none");
document.querySelector(".sub").textContent="Salt okunur görünüm — etiketleri görebilirsin, değiştiremezsin.";
}
await loadCats();
await loadTags();
await loadLocsRoads();
}
/* ---- veri ---- */
async function loadTags(){
const r=await api("/map-tags");
if(!r.ok){$("listHint").textContent="Yüklenemedi";return;}
tags=r.body;render();
}
async function loadLocsRoads(){
const [l,r]=await Promise.all([api("/map-tags/locations"),api("/map-tags/roads")]);
if(l.ok)locs=l.body;
if(r.ok)roads=r.body;
render();
}
function locByCode(c){return locs.find(l=>l.code===c);}
// Yol ismi: "A'dan B'ye" (basit Türkçe ünlü uyumu — ayrılma + yönelme eki)
function _lastVowel(w){const m=(w||"").toLowerCase().match(/[aeıioöuü]/g);return m?m[m.length-1]:"e";}
function _back(v){return "aıou".includes(v);}
function _ablative(w){ // 'dan/'den/'tan/'ten
const b=_back(_lastVowel(w)); const d=/[çfhkpsşt]$/i.test((w||"").trim())?"t":"d";
return w+"'"+d+(b?"an":"en");
}
function _dative(w){ // 'a/'e/'ya/'ye
const b=_back(_lastVowel(w)); const y=/[aeıioöuü]$/i.test((w||"").trim())?"y":"";
return w+"'"+y+(b?"a":"e");
}
function roadLabel(rd){
const a=locByCode(rd.a),b=locByCode(rd.b);
return _ablative(a?a.name:rd.a)+" "+_dative(b?b.name:rd.b);
}
// yol tipine göre renk
const LOC_COLOR={village:"#e8b923",city:"#f0872a",forest:"#3fa34d",mine:"#9aa7b5",lake:"#3a7bd5",meadow:"#8fce5a",hunting:"#c06a3a"};
function locColor(l){return LOC_COLOR[l&&l.type]||"#c9d4e0";}
// verilen normalize noktaya en yakın lokasyonu bul (eşik içinde), yoksa null
function locAt(pt){
let best=null,bd=0.022;
for(const l of locs){const d=Math.hypot(l.x-pt[0],l.y-pt[1]);if(d<bd){bd=d;best=l;}}
return best;
}
/* ---- koordinat: tıklama -> normalize 0-1 ---- */
function evtToNorm(e){
const rect=$("svg").getBoundingClientRect();
let x=(e.clientX-rect.left)/rect.width, y=(e.clientY-rect.top)/rect.height;
return [Math.min(1,Math.max(0,+x.toFixed(4))),Math.min(1,Math.max(0,+y.toFixed(4)))];
}
function centroid(pts){
let x=0,y=0;for(const p of pts){x+=p[0];y+=p[1];}return [x/pts.length,y/pts.length];
}
function pointInPoly(pt,poly){
let inside=false,x=pt[0],y=pt[1];
for(let i=0,j=poly.length-1;i<poly.length;j=i++){
const xi=poly[i][0],yi=poly[i][1],xj=poly[j][0],yj=poly[j][1];
if(((yi>y)!==(yj>y))&&(x<(xj-xi)*(y-yi)/(yj-yi)+xi))inside=!inside;
}
return inside;
}
// verilen noktayı içeren kıta etiketinin kıta adını döndür (yoksa "")
function continentAt(pt){
for(const t of tags){
if(t.kind==="continent"&&t.points.length>=3&&pointInPoly(pt,t.points))
return t.continent||t.name||"";
}
return "";
}
// verilen noktayı içeren bölge (region) id'lerini döndür — üst üste binenlerin hepsi
function regionsAt(pt){
const out=[];
for(const t of tags){
if(t.kind==="region"&&t.points.length>=3&&pointInPoly(pt,t.points))out.push(t.id);
}
return out;
}
// bölge checkbox listesini çiz (checked = seçili, auto = otomatik tespit edilenler)
function renderRegionChecks(selected,autoIds){
const wrap=$("fRegions");wrap.innerHTML="";
const regs=tags.filter(t=>t.kind==="region");
if(!regs.length){wrap.innerHTML='<div class="gempty">Henüz bölge çizilmemiş.</div>';return;}
const sel=new Set(selected||[]),auto=new Set(autoIds||[]);
for(const r of regs){
const lb=document.createElement("label");
if(auto.has(r.id))lb.className="auto";
lb.innerHTML=`<input type="checkbox" value="${r.id}" ${sel.has(r.id)?"checked":""} ${CAN_EDIT?"":"disabled"}>
<span>${esc(r.name)}${r.continent?" · "+esc(r.continent):""}</span>`;
wrap.appendChild(lb);
}
}
function selectedRegions(){
return [...$("fRegions").querySelectorAll("input:checked")].map(i=>+i.value);
}
function toggleRegionsWrap(kind){
$("regionsWrap").classList.toggle("hide",kind!=="poi");
}
/* ---- render ---- */
function render(){
const svg=$("svg");svg.innerHTML="";
const showLbl=$("showLabels").checked;
// kayıtlı yollar (en altta) — lokasyon uçları + ara noktalardan geçen polyline
if($("showRoads").checked)
for(const rd of roads){
const a=locByCode(rd.a),b=locByCode(rd.b);if(!a||!b)continue;
const pts=[[a.x,a.y],...(rd.waypoints||[]),[b.x,b.y]];
const sel=rd.id===roadEditId;
const ln=el("polyline",{points:pts.map(p=>p.join(",")).join(" "),fill:"none",
stroke:sel?"#fff":"#b98a3a","stroke-width":sel?3.2:2});
ln.setAttribute("pointer-events",mode==="select"?"auto":"none");
ln.style.cursor="pointer";ln.onclick=ev=>{ev.stopPropagation();selectRoad(rd);};
svg.appendChild(ln);
}
// kayıtlı etiketler
for(const t of tags){
if(t.kind==="poi" && !$("showPois").checked) continue;
if(t.kind!=="poi" && !$("showRegions").checked) continue;
const c=colorFor(t),sel=t.id===editingId;
if(t.kind==="poi"){
const [x,y]=t.points[0];
const dot=el("circle",{cx:x,cy:y,r:0.008,fill:c,stroke:sel?"#fff":"#000","stroke-width":sel?2:1});
dot.style.cursor="pointer";dot.onclick=ev=>{ev.stopPropagation();selectTag(t);};
svg.appendChild(dot);
if(showLbl)svg.appendChild(text(x+0.012,y+0.004,(catIcon(t.category)?catIcon(t.category)+" ":"")+t.name));
}else{
const poly=el("polygon",{points:t.points.map(p=>p.join(",")).join(" "),
fill:c+"33",stroke:c,"stroke-width":sel?3:1.6});
poly.style.cursor="pointer";poly.onclick=ev=>{ev.stopPropagation();selectTag(t);};
svg.appendChild(poly);
if(showLbl){const [cx,cy]=centroid(t.points);svg.appendChild(text(cx,cy,(catIcon(t.category)?catIcon(t.category)+" ":"")+t.name,"middle"));}
}
}
// çizim taslağı
if(draft.length){
svg.appendChild(el("polyline",{points:draft.map(p=>p.join(",")).join(" "),
fill:"none",stroke:"#e8b923","stroke-width":2,"stroke-dasharray":"4 3"}));
for(const p of draft)svg.appendChild(el("circle",{cx:p[0],cy:p[1],r:0.006,fill:"#e8b923"}));
}
if(draftPoint)svg.appendChild(el("circle",{cx:draftPoint[0],cy:draftPoint[1],r:0.008,fill:"#e8b923",stroke:"#000"}));
// lokasyon pinleri (yol uçları) — yol modunda tıklanabilir, diğerlerinde referans
for(const l of locs){
const isEnd=mode==="road"&&(l.code===roadA||l.code===roadB);
const c=el("circle",{cx:l.x,cy:l.y,r:0.0075,fill:locColor(l),
stroke:isEnd?"#fff":"#0a0f16","stroke-width":isEnd?2.6:1.4});
c.setAttribute("pointer-events",mode==="road"?"auto":"none");
if(mode==="road"){c.style.cursor="pointer";c.onclick=ev=>{ev.stopPropagation();pickRoadEnd(l);};}
svg.appendChild(c);
if(showLbl&&mode==="road")svg.appendChild(text(l.x,l.y-0.016,l.name,"middle"));
}
// çizilen yol taslağı
if(mode==="road"&&roadA){
const a=locByCode(roadA);
const pts=[[a.x,a.y],...roadWps];
if(roadB)pts.push([locByCode(roadB).x,locByCode(roadB).y]);
svg.appendChild(el("polyline",{points:pts.map(p=>p.join(",")).join(" "),fill:"none",
stroke:"#e8b923","stroke-width":2.5,"stroke-dasharray":"4 3"}));
for(const p of roadWps)svg.appendChild(el("circle",{cx:p[0],cy:p[1],r:0.005,fill:"#e8b923"}));
}
// liste — türe göre 3 grup
const lists={continent:$("listContinent"),region:$("listRegion"),poi:$("listPoi")};
const counts={continent:0,region:0,poi:0};
for(const k in lists)lists[k].innerHTML="";
$("listHint").textContent=tags.length?"":"Henüz etiket yok. Bir mod seç, haritaya çiz.";
// zone↔pin çapraz: her bölgenin içindeki pinler + her pin'in bölgesi
const pinsByRegion={},tagById={};
for(const t of tags)tagById[t.id]=t;
for(const t of tags)if(t.kind==="poi")for(const rid of (t.region_ids||[])){(pinsByRegion[rid]=pinsByRegion[rid]||[]).push(t);}
for(const t of tags){
const row=document.createElement("div");
row.className="tagrow"+(t.id===editingId?" sel":"");
let meta=t.category?(catIcon(t.category)+" "+catLabel(t.category)):"";
if(t.kind==="region"){const ps=pinsByRegion[t.id]||[];if(ps.length)meta+=` · içinde ${ps.map(p=>catIcon(p.category)||"📍").join("")} ${ps.length}`;}
if(t.kind==="poi"){const zs=(t.region_ids||[]).map(id=>tagById[id]).filter(Boolean);if(zs.length)meta+=" · ⤷ "+zs.map(z=>esc(z.name)).join(", ");}
if(t.continent)meta+=(meta?" · ":"")+esc(t.continent);
row.innerHTML=`<span class="swatch" style="background:${colorFor(t)}"></span>
<span>${catIcon(t.category)?catIcon(t.category)+" ":""}${esc(t.name)}</span><span class="knd">${meta}</span>
${CAN_EDIT?'<button class="delx" title="Sil">✕</button>':""}`;
const dx=row.querySelector(".delx");
if(dx)dx.onclick=ev=>{ev.stopPropagation();delTag(t.id,t.name);};
row.onclick=()=>selectTag(t);
const k=lists[t.kind]?t.kind:"region";
lists[k].appendChild(row);counts[k]++;
}
$("cntContinent").textContent=counts.continent;
$("cntRegion").textContent=counts.region;
$("cntPoi").textContent=counts.poi;
for(const k in lists)
if(!counts[k])lists[k].innerHTML='<div class="gempty">Boş</div>';
// Yollar listesi — "A'dan B'ye"
const lr=$("listRoad");lr.innerHTML="";
for(const rd of roads){
const wpn=(rd.waypoints||[]).length;
const row=document.createElement("div");
row.className="tagrow"+(rd.id===roadEditId?" sel":"");
row.innerHTML=`<span class="swatch" style="background:#b98a3a"></span>
<span>${esc(roadLabel(rd))}</span>
<span class="knd">${rd.step_cost}👣${wpn?" · "+wpn+" nokta":""}</span>
${CAN_EDIT?'<button class="delx" title="Sil">✕</button>':""}`;
const dx=row.querySelector(".delx");
if(dx)dx.onclick=ev=>{ev.stopPropagation();delRoad(rd.id);};
row.onclick=()=>selectRoad(rd);
lr.appendChild(row);
}
$("cntRoad").textContent=roads.length;
if(!roads.length)lr.innerHTML='<div class="gempty">Boş — 🛣 Yol çiz moduyla ekle</div>';
$("drawInfo").textContent=draft.length+" köşe";
$("finishBtn").disabled=draft.length<3;
}
function el(tag,attrs){const n=document.createElementNS(SVGNS,tag);for(const k in attrs)n.setAttribute(k,attrs[k]);return n;}
function text(x,y,s,anchor){const t=el("text",{x,y,class:"veclabel","text-anchor":anchor||"start"});t.setAttribute("font-size","0.02");t.textContent=s;return t;}
function esc(s){return (s||"").replace(/[&<>]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;"}[c]));}
/* ---- mod ---- */
function setMode(m){
mode=m;draft=[];draftPoint=null;
if(m!=="road"){roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");}
document.querySelectorAll(".modebtn[data-mode]").forEach(b=>b.classList.toggle("on",b.dataset.mode===m));
$("drawCtl").style.display=(m==="region")?"flex":"none";
$("roadCtl").style.display=(m==="road")?"flex":"none";
// 'draw' sınıfı çocukların pointer-event'lerini kapatır — yol modunda pinler tıklanabilir kalmalı
$("svg").classList.toggle("draw",m==="region"||m==="poi");
$("svg").style.cursor=(m==="select")?"pointer":"crosshair";
if(m==="road")updateRoadInfo();
render();
}
function updateRoadInfo(){
let s;
if(!roadA)s="Başlangıç lokasyonuna tıkla";
else if(!roadB)s=`${nm(roadA)} → … (ara nokta koy, sonra varış lokasyonuna tıkla)`;
else s=`${nm(roadA)}${nm(roadB)}`;
$("roadInfo").textContent=s;
$("rWpInfo").textContent=roadWps.length+" ara nokta";
}
function nm(code){const l=locByCode(code);return l?l.name:code;}
// yol modunda lokasyon pinine tıklama: 1.=başlangıç, 2.=varış → form aç
function pickRoadEnd(l){
if(!roadA){roadA=l.code;roadWps=[];roadB=null;updateRoadInfo();render();return;}
if(l.code===roadA){return;} // aynı pine tekrar → yoksay
roadB=l.code;
openRoadForm(null);
updateRoadInfo();render();
}
/* ---- çizim ---- */
$("svg").addEventListener("click",e=>{
if(mode==="region"){draft.push(evtToNorm(e));render();}
else if(mode==="poi"){draftPoint=evtToNorm(e);openForm(null,"poi");render();}
else if(mode==="road"){
const pt=evtToNorm(e);
const near=locAt(pt);
if(near){pickRoadEnd(near);return;}
if(roadA&&!roadB){roadWps.push(pt);updateRoadInfo();render();}
}
});
$("undoPt").onclick=()=>{draft.pop();render();};
$("cancelDraw").onclick=()=>{draft=[];render();};
$("finishBtn").onclick=()=>{if(draft.length>=3)openForm(null,"region");};
/* ---- form ---- */
function openForm(tag,kind){
$("formCard").classList.remove("hide");$("formMsg").textContent="";
// salt okunurda kaydet/sil gizli, alanlar kilitli
$("saveBtn").style.display=CAN_EDIT?"":"none";
for(const id of ["fName","fKind","fCategory","fCont","fNote"])$(id).disabled=!CAN_EDIT;
if(tag){ // düzenleme
editingId=tag.id;$("formTitle").textContent="Düzenle: "+tag.name;
$("fName").value=tag.name;$("fKind").value=tag.kind;$("fCont").value=tag.continent||"";
$("fCategory").innerHTML=catOptions(tag.kind,tag.category||"");
$("fNote").value=tag.note||"";$("delBtn").classList.remove("hide");
// POI ise: mevcut bölge seçimini göster; noktanın altındaki bölgeleri "auto" vurgula
const autoR=tag.kind==="poi"&&tag.points[0]?regionsAt(tag.points[0]):[];
renderRegionChecks(tag.region_ids||[],autoR);
toggleRegionsWrap(tag.kind);
}else{ // yeni — çizilen alanın merkezi hangi kıtadaysa onu otomatik seç
editingId=null;$("formTitle").textContent="Yeni etiket";
const pt=(kind==="poi")?draftPoint:(draft.length?centroid(draft):null);
const cont=pt?continentAt(pt):"";
$("fName").value="";$("fKind").value=kind;$("fCont").value=cont;$("fNote").value="";
$("fCategory").innerHTML=catOptions(kind,"");
$("delBtn").classList.add("hide");
// POI: içinde olduğu bölgeleri otomatik seç
const autoR=(kind==="poi"&&pt)?regionsAt(pt):[];
renderRegionChecks(autoR,autoR);
toggleRegionsWrap(kind);
}
$("catWrap").classList.toggle("hide",$("fKind").value==="continent");
if(!CAN_EDIT)$("delBtn").classList.add("hide");
render();
$("formCard").scrollIntoView({behavior:"smooth",block:"nearest"});
$("fName").focus();
}
function selectTag(t){setMode("select");openForm(t,t.kind);}
$("closeForm").onclick=()=>{editingId=null;draft=[];draftPoint=null;$("formCard").classList.add("hide");render();};
$("saveBtn").onclick=async()=>{
const name=$("fName").value.trim();
if(!name){$("formMsg").className="msg err";$("formMsg").textContent="İsim gerekli";return;}
const kind=$("fKind").value;
let points;
if(editingId){ // mevcut geometriyi koru
const cur=tags.find(t=>t.id===editingId);points=cur.points;
}else if(kind==="poi"){
if(!draftPoint){$("formMsg").textContent="Haritada nokta seç";return;}points=[draftPoint];
}else{
if(draft.length<3){$("formMsg").textContent="En az 3 köşe";return;}points=draft.slice();
}
const category=$("fCategory").value||null;
const payload={name,kind,category,continent:$("fCont").value||null,points,
color:cat(category)?cat(category).color:null,
note:$("fNote").value.trim()||null,
region_ids:kind==="poi"?selectedRegions():[]};
$("formMsg").className="msg";$("formMsg").textContent="Kaydediliyor…";
const r=editingId?await api("/map-tags/"+editingId,{method:"POST",body:JSON.stringify(payload)})
:await api("/map-tags",{method:"POST",body:JSON.stringify(payload)});
if(!r.ok){$("formMsg").className="msg err";$("formMsg").textContent=r.body.detail||("Hata "+r.status);return;}
draft=[];draftPoint=null;editingId=null;$("formCard").classList.add("hide");
await loadTags();
};
async function delTag(id,name){
if(!confirm("Silinsin mi? — "+name))return;
const r=await api("/map-tags/"+id+"/delete",{method:"POST"});
if(!r.ok){alert("Silinemedi ("+r.status+")");return;}
if(editingId===id){editingId=null;$("formCard").classList.add("hide");}
await loadTags();
}
$("delBtn").onclick=()=>{if(editingId)delTag(editingId,$("fName").value);};
/* ---- yol formu ---- */
function openRoadForm(rd){
$("roadForm").classList.remove("hide");$("roadMsg").textContent="";
if(rd){ // mevcut yolu düzenle
roadEditId=rd.id;roadA=rd.a;roadB=rd.b;roadWps=(rd.waypoints||[]).map(p=>p.slice());
$("roadTitle").textContent="Yolu düzenle";
$("rCost").value=rd.step_cost;$("rEvent").value=rd.event_chance.toFixed(2);
$("delRoadBtn").classList.remove("hide");
}else{ // yeni yol (roadA/roadB zaten seçili)
roadEditId=null;$("roadTitle").textContent="Yeni yol";
$("delRoadBtn").classList.add("hide");
}
$("roadEnds").innerHTML=`🛣 <b>${esc(roadLabel({a:roadA,b:roadB}))}</b>`;
$("rWpInfo").textContent=roadWps.length+" ara nokta";
$("roadForm").scrollIntoView({behavior:"smooth",block:"nearest"});
}
function selectRoad(rd){setMode("road");openRoadForm(rd);render();}
$("saveRoad").onclick=async()=>{
if(!roadA||!roadB){$("roadMsg").className="msg err";$("roadMsg").textContent="İki uç lokasyon gerekli";return;}
const payload={a:roadA,b:roadB,step_cost:parseInt($("rCost").value)||0,
event_chance:parseFloat($("rEvent").value)||0,waypoints:roadWps};
$("roadMsg").className="msg";$("roadMsg").textContent="Kaydediliyor…";
const r=await api("/map-tags/roads",{method:"POST",body:JSON.stringify(payload)});
if(!r.ok){$("roadMsg").className="msg err";$("roadMsg").textContent=(r.body&&r.body.detail)||("Hata "+r.status);return;}
roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");
await loadLocsRoads();updateRoadInfo();
};
async function delRoad(id){
if(!confirm("Bu yol silinsin mi?"))return;
const r=await api("/map-tags/roads/"+id+"/delete",{method:"POST"});
if(!r.ok){alert("Silinemedi ("+r.status+")");return;}
roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");
await loadLocsRoads();updateRoadInfo();
}
$("delRoadBtn").onclick=()=>{if(roadEditId)delRoad(roadEditId);};
$("closeRoad").onclick=()=>{roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");updateRoadInfo();render();};
$("undoWp").onclick=()=>{roadWps.pop();updateRoadInfo();render();};
$("cancelRoad").onclick=()=>{roadA=roadB=roadEditId=null;roadWps=[];$("roadForm").classList.add("hide");updateRoadInfo();render();};
/* ---- bağla ---- */
$("loginBtn").onclick=login;
$("pass").addEventListener("keydown",e=>{if(e.key==="Enter")login();});
document.querySelectorAll(".modebtn[data-mode]").forEach(b=>b.onclick=()=>setMode(b.dataset.mode));
$("fKind").addEventListener("change",()=>{
const k=$("fKind").value;
toggleRegionsWrap(k);
$("catWrap").classList.toggle("hide",k==="continent");
$("fCategory").innerHTML=catOptions(k,"");
});
// kategori seçilince isim boşsa otomatik öner: "Orman 3"
$("fCategory").addEventListener("change",()=>{
const c=$("fCategory").value;
if(!c||$("fName").value.trim())return;
const n=tags.filter(t=>t.category===c).length+1;
$("fName").value=catLabel(c)+" "+n;
});
$("showLabels").onchange=render;
$("showRegions").onchange=render;
$("showRoads").onchange=render;
$("showPois").onchange=render;
/* ---- tam ekran düzenleme + pan/zoom (yalnız fullscreen'de aktif) ---- */
(function(){
const area=$("maparea"), view=$("mapview"), zoom=$("mapzoom"), toolbar=document.querySelector(".toolbar");
let fsActive=false, scale=1, tx=0, ty=0, MIN=1; const MAX=8;
function apply(){ zoom.style.transform=`translate(${tx}px,${ty}px) scale(${scale})`; }
function clamp(){
const vw=view.clientWidth, vh=view.clientHeight;
const cw=vw*scale, ch=zoom.offsetHeight*scale;
tx = cw<=vw ? (vw-cw)/2 : Math.max(vw-cw, Math.min(0, tx)); // sığıyorsa ortala
ty = ch<=vh ? (vh-ch)/2 : Math.max(vh-ch, Math.min(0, ty));
}
// tüm haritayı görünüme sığdır (contain), ortala — taban zoom
function fit(){
const baseH=zoom.offsetHeight;
if(!baseH || !view.clientHeight) return;
MIN=Math.min(1, view.clientHeight/baseH); // her şeyi görecek kadar uzaklaş
scale=MIN; tx=0; ty=0; clamp(); apply();
}
function zoomAt(cx,cy,ns){
ns=Math.max(MIN,Math.min(MAX,ns));
const r=view.getBoundingClientRect(), px=cx-r.left, py=cy-r.top;
tx=px-(px-tx)*(ns/scale); ty=py-(py-ty)*(ns/scale);
scale=ns; clamp(); apply();
}
function centerZoom(f){ const r=view.getBoundingClientRect(); zoomAt(r.left+view.clientWidth/2, r.top+view.clientHeight/2, scale*f); }
function fsOpen(){
if(fsActive) return; fsActive=true;
area.classList.add("fs");
toolbar.classList.add("fsfloat"); area.prepend(toolbar); // araçları overlay içine taşı
document.body.style.overflow="hidden";
requestAnimationFrame(fit);
}
function fsClose(){
if(!fsActive) return; fsActive=false;
toolbar.classList.remove("fsfloat"); area.parentNode.insertBefore(toolbar, area); // geri koy
area.classList.remove("fs");
document.body.style.overflow="";
scale=1; tx=0; ty=0; zoom.style.transform=""; // normal görünüme dön
}
$("fsOpenBtn").onclick=fsOpen;
$("fsClose").onclick=fsClose;
$("zoomIn").onclick=()=>centerZoom(1.4);
$("zoomOut").onclick=()=>centerZoom(1/1.4);
$("zoomReset").onclick=fit;
document.addEventListener("keydown",e=>{ if(e.key==="Escape"&&fsActive) fsClose(); });
view.addEventListener("wheel",e=>{ if(!fsActive) return; e.preventDefault(); zoomAt(e.clientX,e.clientY,scale*(e.deltaY<0?1.15:1/1.15)); },{passive:false});
let pts=new Map(), startDist=0, startScale=1, lastMid=null;
view.addEventListener("touchstart",e=>{
if(!fsActive) return;
for(const t of e.changedTouches) pts.set(t.identifier,{x:t.clientX,y:t.clientY});
if(pts.size===2){ const [a,b]=[...pts.values()]; startDist=Math.hypot(a.x-b.x,a.y-b.y); startScale=scale; lastMid={x:(a.x+b.x)/2,y:(a.y+b.y)/2}; }
},{passive:false});
view.addEventListener("touchmove",e=>{
if(!fsActive) return;
for(const t of e.changedTouches) if(pts.has(t.identifier)) pts.set(t.identifier,{x:t.clientX,y:t.clientY});
if(pts.size>=2){
e.preventDefault();
const [a,b]=[...pts.values()];
const dist=Math.hypot(a.x-b.x,a.y-b.y), mid={x:(a.x+b.x)/2,y:(a.y+b.y)/2};
if(lastMid){ tx+=mid.x-lastMid.x; ty+=mid.y-lastMid.y; } lastMid=mid;
if(startDist>0) zoomAt(mid.x,mid.y,startScale*(dist/startDist)); else { clamp(); apply(); }
}
},{passive:false});
function end(e){ for(const t of e.changedTouches) pts.delete(t.identifier); if(pts.size<2){ startDist=0; lastMid=null; } }
view.addEventListener("touchend",end); view.addEventListener("touchcancel",end);
window.addEventListener("resize",()=>{ if(fsActive) fit(); });
})();
if(TOKEN)showEditor();
</script>
</body>
</html>