1/** 2 * TKGM Megis API â tarayıcıdan (client-first). 3 * Parsel çizimi için ham veri buradan; Åekil etiketi arka planda /api/tkgm/enrich_shape/. 4 * İstisna: yalnızca günlük limit (TKGM_RATE_LIMIT) â sunucu /api/tkgm_proxy/parsel (Liva). 5 */ 6 7const TKGM_API_BASE = 'https://cbsapi.tkgm.gov.tr/megsiswebapi.v3.1/api/parsel'; 8const TKGM_TIMEOUT = 12000; 9const TKGM_PROXY_TIMEOUT = 25000; 10 11const TKGM_HEADERS = { 12 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", 13 "Accept": "application/json, text/plain, */*", 14 "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7", 15 "Origin": "https://parselsorgu.tkgm.gov.tr", 16 "Referer": "https://parselsorgu.tkgm.gov.tr/" 17}; 18 19function adaPathSegment(ada) { 20 const a = String(ada ?? '').trim(); 21 return a !== '' ? a : '0'; 22} 23 24async function applyPassiveParcelIfNeeded(data) { 25 if (data?.properties && !data?.geometry && window.TkgmPassiveParcel) { 26 return window.TkgmPassiveParcel.normalizeAndConfirm(data); 27 } 28 return data; 29} 30 31function normalizeFetchError(error) { 32 if (error?.name === 'AbortError') { 33 return { type: 'TIMEOUT', message: 'TKGM API zaman aÅımı' }; 34 } 35 if ( 36 error?.name === 'TypeError' || 37 error?.message?.includes?.('Failed to fetch') || 38 error?.message?.includes?.('NetworkError') 39 ) { 40 return { 41 type: 'CORS_OR_NETWORK_ERROR', 42 message: 'TKGM API\'ye baÄlanılamadı. İnternet veya tarayıcı CORS kısıtını kontrol edin.', 43 originalError: error, 44 }; 45 } 46 if (error?.type) return error; 47 return { type: 'UNKNOWN_ERROR', message: error?.message || 'Bilinmeyen hata', originalError: error }; 48} 49 50async function parseTkgmHttpResponse(response, url) { 51 if (response.status === 404) { 52 let errorData = null; 53 try { 54 errorData = await response.json(); 55 } catch (_) { 56 errorData = { Message: 'Parsel Bulunamadı' }; 57 } 58 if (window.TkgmPassiveParcel?.isPassiveParcelPayload?.(errorData)) { 59 return applyPassiveParcelIfNeeded(errorData); 60 } 61 const msg = errorData?.Message || errorData?.message || 'Parsel Bulunamadı'; 62 throw { type: 'TKGM_PARCEL_NOT_FOUND', status: 404, message: msg, detail: errorData }; 63 } 64 if (response.status === 403) { 65 let errorData = null; 66 try { 67 errorData = await response.json(); 68 } catch (_) { 69 errorData = { Message: 'Günlük sorgu limitini aÅtınız.' }; 70 } 71 const msg = 72 errorData?.Message || errorData?.message || errorData?.error || 'Günlük sorgu limitini aÅtınız.'; 73 try { 74 console.warn('[TKGM][client] rate_limit', { status: response.status, url, message: msg }); 75 } catch (_) {} 76 throw { type: 'TKGM_RATE_LIMIT', status: 403, message: msg, detail: errorData }; 77 } 78 if (!response.ok) { 79 throw { type: 'TKGM_ERROR', status: response.status, message: `HTTP ${response.status}` }; 80 } 81 let data = await response.json(); 82 data = await applyPassiveParcelIfNeeded(data); 83 if (!data?.geometry || !data?.properties) { 84 throw { type: 'TKGM_INVALID_DATA', message: 'Geçersiz veri formatı' }; 85 } 86 return data; 87} 88 89async function tkgmHttpGet(url, signal) { 90 const controller = new AbortController(); 91 const timeoutId = setTimeout(() => controller.abort(), TKGM_TIMEOUT); 92 const linked = signal 93 ? (() => { 94 if (signal.aborted) controller.abort(); 95 else signal.addEventListener('abort', () => controller.abort(), { once: true }); 96 })() 97 : null; 98 void linked; 99 try { 100 const response = await fetch(url, { 101 method: 'GET', 102 headers: TKGM_HEADERS, 103 signal: controller.signal, 104 }); 105 clearTimeout(timeoutId); 106 return await parseTkgmHttpResponse(response, url); 107 } catch (error) { 108 clearTimeout(timeoutId); 109 throw normalizeFetchError(error); 110 } 111} 112 113/** 114 * Yalnızca TKGM_RATE_LIMIT sonrası: sunucu Liva proxy üzerinden parsel. 115 * CORS/aÄ/timeout yedeÄi deÄildir. 116 */ 117async function fetchTkgmViaServerProxy({ mahalleTkgmValue, ada, parsel, lat, lon, signal = null }) { 118 const qs = new URLSearchParams(); 119 if (mahalleTkgmValue != null && String(parsel ?? '').trim() !== '') { 120 qs.set('mahalle', String(mahalleTkgmValue).trim()); 121 qs.set('ada', adaPathSegment(ada));
122 qs.set('parsel', String(parsel).trim()); 123 } else if (lat != null && lon != null) { 124 qs.set('lat', String(lat)); 125 qs.set('lon', String(lon)); 126 } else { 127 throw { type: 'INVALID_PARAMS', message: 'Proxy için mahalle+parsel veya lat/lon gerekli' }; 128 } 129 const url = `/api/tkgm_proxy/parsel?${qs.toString()}`; 130 const controller = new AbortController(); 131 const timeoutId = setTimeout(() => controller.abort(), TKGM_PROXY_TIMEOUT); 132 if (signal) { 133 if (signal.aborted) controller.abort(); 134 else signal.addEventListener('abort', () => controller.abort(), { once: true }); 135 } 136 try { 137 console.warn('[TKGM][client] rate_limit â server proxy', { url }); 138 const response = await fetch(url, { method: 'GET', signal: controller.signal }); 139 clearTimeout(timeoutId); 140 let body = null; 141 try { 142 body = await response.json(); 143 } catch (_) { 144 body = null; 145 } 146 if (!response.ok) { 147 const msg = 148 body?.Message || body?.message || body?.error || `HTTP ${response.status}`; 149 if (response.status === 403 || /günlük sorgu limiti/i.test(String(msg))) { 150 throw { type: 'TKGM_RATE_LIMIT', status: 403, message: msg, detail: body }; 151 } 152 if (response.status === 404 || body?.type === 'TKGM_PARCEL_NOT_FOUND') { 153 throw { type: 'TKGM_PARCEL_NOT_FOUND', status: 404, message: msg, detail: body }; 154 } 155 throw { type: 'TKGM_ERROR', status: response.status, message: msg, detail: body }; 156 } 157 let data = body; 158 if (data?.success === false) { 159 const msg = data?.error || data?.Message || 'TKGM proxy baÅarısız'; 160 throw { type: 'TKGM_ERROR', status: 502, message: msg, detail: data }; 161 } 162 data = await applyPassiveParcelIfNeeded(data); 163 if (!data?.geometry || !data?.properties) { 164 throw { type: 'TKGM_INVALID_DATA', message: 'Geçersiz veri formatı (proxy)' }; 165 } 166 return data; 167 } catch (error) { 168 clearTimeout(timeoutId); 169 if (error?.type) throw error; 170 throw normalizeFetchError(error); 171 } 172} 173 174async function withRateLimitProxyFallback(directFn, proxyArgs) { 175 try { 176 return await directFn(); 177 } catch (error) { 178 if (error?.type !== 'TKGM_RATE_LIMIT') throw error; 179 try { 180 return await fetchTkgmViaServerProxy({ ...proxyArgs }); 181 } catch (proxyErr) { 182 if (proxyErr?.type === 'TKGM_RATE_LIMIT' || proxyErr?.type === 'TKGM_PARCEL_NOT_FOUND') { 183 throw proxyErr; 184 } 185 // Proxy aÄ/diÄer hata â kullanıcıya günlük limit mesajını koru 186 throw error; 187 } 188 } 189} 190 191/** 192 * Ada/parsel ile TKGM ham Feature. 193 */ 194async function fetchTkgmByIds(mahalleTkgmValue, ada, parsel, signal = null) { 195 const m = String(mahalleTkgmValue ?? '').trim(); 196 const p = String(parsel ?? '').trim(); 197 const a = adaPathSegment(ada); 198 if (!m || !p) { 199 throw { type: 'INVALID_PARAMS', message: 'Mahalle TKGM kodu ve parsel zorunlu' }; 200 } 201 const url = `${TKGM_API_BASE}/${m}/${a}/${p}`; 202 return await withRateLimitProxyFallback( 203 () => tkgmHttpGet(url, signal), 204 { mahalleTkgmValue: m, ada: a, parsel: p, signal }, 205 ); 206} 207 208/** @deprecated fetchTkgmByIds kullanın */ 209const fetchTkgmDirectly = fetchTkgmByIds; 210 211/** 212 * Koordinat ile TKGM ham Feature. 213 */ 214async function fetchTkgmByCoords(lat, lon, signal = null) { 215 const url = `${TKGM_API_BASE}/${lat}/${lon}/`; 216 return await withRateLimitProxyFallback( 217 () => tkgmHttpGet(url, signal), 218 { lat, lon, signal }, 219 ); 220} 221 222/** 223 * Arka planda parsel Åekli (TKGM kotası yok). 224 */ 225function enrichTkgmShapeAsync(feature, signal = null) { 226 if (!feature?.geometry || !feature?.properties) return; 227 const props = feature.properties; 228 if (props.parcel_shape_type_label || props.parcel_shape_type) return; 229 230 const run = async () => { 231 try { 232 const response = await fetch('/api/tkgm/enrich_shape/', { 233 method: 'POST', 234 headers: { 'Content-Type': 'application/json' }, 235 body: JSON.stringify({ 236 type: feature.type || 'Feature', 237 geometry: feature.geometry, 238 properties: props, 239 }), 240 signal: signal,
241 }); 242 if (!response.ok) return; 243 const body = await response.json().catch(() => null); 244 const bp = body?.properties; 245 if (!bp) return; 246 if (bp.parcel_shape_type) props.parcel_shape_type = bp.parcel_shape_type; 247 if (bp.parcel_shape_type_label) props.parcel_shape_type_label = bp.parcel_shape_type_label; 248 try { 249 if (window.lastTkgmData?.properties === props) { 250 window.lastTkgmData.properties = props; 251 } 252 } catch (_) {} 253 } catch (err) { 254 try { 255 console.warn('[TKGM][shape] enrich_async_failed', err?.message || err); 256 } catch (_) {} 257 } 258 }; 259 260 if (typeof requestIdleCallback === 'function') { 261 requestIdleCallback(() => { run(); }, { timeout: 2500 }); 262 } else { 263 setTimeout(run, 0); 264 } 265} 266 267async function fetchTkgmByIdsWithShape(mahalleTkgmValue, ada, parsel, signal = null) { 268 const data = await fetchTkgmByIds(mahalleTkgmValue, ada, parsel, signal); 269 enrichTkgmShapeAsync(data, signal); 270 return data; 271} 272 273async function fetchTkgmByCoordsWithShape(lat, lon, signal = null) { 274 const data = await fetchTkgmByCoords(lat, lon, signal); 275 enrichTkgmShapeAsync(data, signal); 276 return data; 277} 278 279/** Geriye uyumluluk: isimler korunur; CORS/aÄ yedeÄi yok. Rate-limit â sunucu proxy (fetchTkgmByIds içinde). */ 280const fetchTkgmWithFallback = fetchTkgmByIdsWithShape; 281const fetchTkgmByCoordsWithFallback = fetchTkgmByCoordsWithShape; 282 283/** 284 * Mongo mahalle adayları (sunucu TKGM çaÄırmaz). 285 */ 286async function fetchMahalleCandidates(townId, proparcelValue, quarterId, signal = null) { 287 const qs = new URLSearchParams(); 288 if (townId != null && String(townId).trim() !== '') qs.set('town_id', String(townId).trim()); 289 if (proparcelValue != null && String(proparcelValue).trim() !== '') { 290 qs.set('proparcel_value', String(proparcelValue).trim()); 291 } 292 if (quarterId != null && String(quarterId).trim() !== '') qs.set('quarter_id', String(quarterId).trim()); 293 const r = await fetch(`/api/tkgm/mahalle_candidates/?${qs.toString()}`, { method: 'GET', signal }); 294 const j = await r.json().catch(() => ({})); 295 if (!r.ok || !j.success) { 296 const err = new Error(j.error || 'Mahalle adayları alınamadı'); 297 err.code = j.code; 298 throw err; 299 } 300 return j.candidates || []; 301} 302 303/** 304 * Aday listesine ek TKGM id ekle (map-click keÅfi); duplicate yok. 305 */ 306function mergeExtraTkgmCandidates(candidates, extraIds) { 307 const list = Array.isArray(candidates) ? candidates.slice() : []; 308 const seen = new Set( 309 list 310 .map((c) => { 311 const tv = c?.Tkgm_value ?? c?.tkgm_value; 312 return tv != null ? String(tv).trim() : ''; 313 }) 314 .filter(Boolean) 315 ); 316 const extras = Array.isArray(extraIds) ? extraIds : [extraIds]; 317 extras.forEach((raw) => { 318 String(raw || '') 319 .split(/[,;]/) 320 .map((s) => s.trim()) 321 .filter(Boolean) 322 .forEach((tv) => { 323 if (seen.has(tv)) return; 324 seen.add(tv); 325 list.push({ Tkgm_value: tv, Tkgm_text: '', Proparcel_text: '', Proparcel_value: null, Id: null }); 326 }); 327 }); 328 return list; 329} 330 331/** 332 * Collect-all: tüm adaylarda istemci TKGM by-ids. First-win yok. 333 * @returns {{ 334 * success: boolean, 335 * hit_count: number, 336 * needs_user_pick: boolean, 337 * mahalle_id: string|null, 338 * hits: Array<{mahalle_id: string, tkgm_text: string}>, 339 * tried: string[], 340 * candidates: Array, 341 * data?: object|null, 342 * type?: string, 343 * properties?: object, 344 * geometry?: object, 345 * code?: string, 346 * error?: string, 347 * }} 348 */ 349async function resolveTkgmMahalleCandidatesCollectAll(candidates, ada, parsel, signal = null) { 350 const tried = []; 351 const hits = []; 352 const hitPayloads = []; 353 let lastError = null; 354 const list = Array.isArray(candidates) ? candidates : []; 355 356 for (const cand of list) { 357 const tv = cand?.Tkgm_value ?? cand?.tkgm_value; 358 const tvStr = tv != null ? String(tv).trim() : ''; 359 if (!tvStr || tried.includes(tvStr)) continue; 360 tried.push(tvStr); 361 try { 362 const data = await fetchTkgmByIds(tvStr, ada, parsel, signal); 363 const props = (data && data.properties) || {}; 364 const canonical = String(props.mahalleId || props.mahalle_id || tvStr).trim();
365 if (hits.some((h) => h.mahalle_id === canonical)) continue; 366 const tkgmText = 367 String(cand?.Tkgm_text || cand?.tkgm_text || '').trim() || 368 String(props.mahalleAd || props.mahalle || '').trim() || 369 canonical; 370 hits.push({ mahalle_id: canonical, tkgm_text: tkgmText }); 371 hitPayloads.push(data); 372 } catch (e) { 373 lastError = e; 374 if (e?.type === 'TKGM_RATE_LIMIT') { 375 return { 376 success: false, 377 hit_count: 0, 378 needs_user_pick: false, 379 mahalle_id: null, 380 hits: [], 381 tried, 382 candidates: list, 383 code: 'TKGM_RATE_LIMIT', 384 error: e.message || 'TKGM günlük sorgu limiti aÅıldı', 385 }; 386 } 387 if (e?.type !== 'TKGM_PARCEL_NOT_FOUND') throw e; 388 } 389 } 390 391 const hitCount = hits.length; 392 if (hitCount === 0) { 393 const msg = 394 (lastError && lastError.message) || 395 `Parsel hiçbir TKGM mahalle adayında bulunamadı (ada=${ada}, parsel=${parsel}). Denenen TKGM ID'ler: ${tried.join(', ') || 'â'}`; 396 return { 397 success: false, 398 hit_count: 0, 399 needs_user_pick: false, 400 mahalle_id: null, 401 hits: [], 402 tried, 403 candidates: list, 404 code: (lastError && lastError.type) || 'TKGM_PARCEL_NOT_FOUND', 405 error: msg, 406 }; 407 } 408 409 if (hitCount === 1) { 410 const only = hitPayloads[0]; 411 return { 412 success: true, 413 hit_count: 1, 414 needs_user_pick: false, 415 mahalle_id: hits[0].mahalle_id, 416 hits, 417 tried, 418 candidates: list, 419 data: only, 420 type: (only && only.type) || 'Feature', 421 properties: (only && only.properties) || {}, 422 geometry: only && only.geometry, 423 }; 424 } 425 426 return { 427 success: true, 428 hit_count: hitCount, 429 needs_user_pick: true, 430 mahalle_id: null, 431 hits, 432 tried, 433 candidates: list, 434 data: null, 435 }; 436} 437 438/** 439 * Geriye uyumluluk: collect-all sonrası tek hit döner; 0/2+ için throw. 440 * @deprecated Ada/parsel için resolveTkgmMahalleCandidatesCollectAll kullanın. 441 */ 442async function resolveTkgmMahalleCandidates(candidates, ada, parsel, signal = null) { 443 const collected = await resolveTkgmMahalleCandidatesCollectAll(candidates, ada, parsel, signal); 444 if (collected.success && collected.hit_count === 1 && collected.data) { 445 return { 446 data: collected.data, 447 mahalleTkgmValue: collected.mahalle_id, 448 tried: collected.tried, 449 hit_count: 1, 450 }; 451 } 452 if (collected.success && collected.hit_count >= 2) { 453 const err = { 454 type: 'NEEDS_USER_PICK', 455 status: 200, 456 message: 'Birden fazla TKGM mahalle adayında parsel bulundu', 457 hits: collected.hits, 458 tried: collected.tried, 459 hit_count: collected.hit_count, 460 needs_user_pick: true, 461 }; 462 throw err; 463 } 464 throw { 465 type: collected.code || 'TKGM_PARCEL_NOT_FOUND', 466 status: 404, 467 message: collected.error || `Parsel bulunamadı (denenen TKGM: ${(collected.tried || []).join(', ') || 'â'})`, 468 tried: collected.tried, 469 }; 470} 471 472/** 473 * PV varsa tüm adaylarda collect-all; bilinen TKGM id adaylara eklenir (first-win yok). 474 */ 475async function queryTkgmParcel({ 476 mahalleTkgmValue, 477 ada, 478 parsel, 479 townId, 480 proparcelValue, 481 quarterId, 482 extraTkgmIds = null, 483 signal = null, 484}) { 485 const m0 = String(mahalleTkgmValue ?? '').trim(); 486 const a = ada; 487 const p = String(parsel ?? '').trim(); 488 if (!p) throw { type: 'INVALID_PARAMS', message: 'Parsel zorunlu' }; 489 490 if (townId && (proparcelValue || quarterId)) { 491 let candidates = await fetchMahalleCandidates(townId, proparcelValue, quarterId, signal); 492 candidates = mergeExtraTkgmCandidates(candidates, [m0, extraTkgmIds].flat().filter(Boolean)); 493 const collected = await resolveTkgmMahalleCandidatesCollectAll(candidates, a, p, signal); 494 if (collected.success && collected.hit_count === 1 && collected.data) { 495 enrichTkgmShapeAsync(collected.data, signal); 496 return { 497 data: collected.data, 498 mahalleTkgmValue: collected.mahalle_id, 499 tried: collected.tried, 500 hit_count: 1, 501 needs_user_pick: false, 502 hits: collected.hits, 503 }; 504 } 505 if (collected.success && collected.hit_count >= 2) { 506 throw { 507 type: 'NEEDS_USER_PICK', 508 status: 200, 509 message: 'Birden fazla TKGM mahalle adayında parsel bulundu', 510 hits: collected.hits, 511 tried: collected.tried, 512 hit_count: collected.hit_count, 513 needs_user_pick: true, 514 }; 515 } 516 throw { 517 type: collected.code || 'TKGM_PARCEL_NOT_FOUND', 518 status: 404, 519 message: collected.error || 'Parsel bulunamadı', 520 tried: collected.tried, 521 }; 522 } 523 524 if (m0) { 525 const data = await fetchTkgmByIdsWithShape(m0, a, p, signal); 526 return { data, mahalleTkgmValue: m0, hit_count: 1, needs_user_pick: false }; 527 } 528 529 throw { type: 'INVALID_PARAMS', message: 'proparcel_value veya mahalle TKGM zorunlu' }; 530} 531 532if (typeof window !== 'undefined') { 533 window.fetchTkgmByIds = fetchTkgmByIds; 534 window.fetchTkgmDirectly = fetchTkgmDirectly; 535 window.fetchTkgmByCoords = fetchTkgmByCoords; 536 window.fetchTkgmByIdsWithShape = fetchTkgmByIdsWithShape; 537 window.fetchTkgmByCoordsWithShape = fetchTkgmByCoordsWithShape; 538 window.fetchTkgmWithFallback = fetchTkgmWithFallback; 539 window.fetchTkgmByCoordsWithFallback = fetchTkgmByCoordsWithFallback; 540 window.enrichTkgmShapeAsync = enrichTkgmShapeAsync; 541 window.fetchMahalleCandidates = fetchMahalleCandidates; 542 window.mergeExtraTkgmCandidates = mergeExtraTkgmCandidates; 543 window.resolveTkgmMahalleCandidates = resolveTkgmMahalleCandidates; 544 window.resolveTkgmMahalleCandidatesCollectAll = resolveTkgmMahalleCandidatesCollectAll; 545 window.queryTkgmParcel = queryTkgmParcel; 546}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.