758 lines
37 KiB
HTML
758 lines
37 KiB
HTML
<!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=>({"&":"&","<":"<",">":">"}[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>
|