PageSourceSearch

https://proparcel.com/static/js/utils/tkgm_api.js

js proparcel.com collected 2026-09-24 14:51:09 UTC 18,842 bytes, 546 lines download raw bytes

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.