İlk commit: Stepstead web (statik panel + db)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
dea01dd6a8
18 changed files with 3764 additions and 0 deletions
758
assets/map-editor.html
Normal file
758
assets/map-editor.html
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
<!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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue