1/** 2 * Coordinate Utils - Core Module 3 * Handles coordinate transformations and validations 4 */ 5 6class CoordinateUtils { 7 constructor() { 8 // Turkey coordinate bounds 9 this.TURKEY_BOUNDS = { 10 minLon: 25.0, 11 maxLon: 45.0, 12 minLat: 36.0, 13 maxLat: 43.0 14 }; 15 16 // Layers to normalize by default (swap lat/lon) 17 this.normalizedLayerIds = new Set([]); 18 } 19 20 /** 21 * Detect coordinate order [lon,lat] or [lat,lon] 22 */ 23 detectCoordinateOrder(coord) { 24 if (!coord || coord.length !== 2) { 25 return "unknown"; 26 } 27 28 const [a, b] = coord; 29 const aNum = Number(a); 30 const bNum = Number(b); 31 32 // Türkiye için sıkı sınırlar ile önce belirlemeye çalıŠ33 const isLonLatTR = aNum >= this.TURKEY_BOUNDS.minLon && aNum <= this.TURKEY_BOUNDS.maxLon && 34 bNum >= this.TURKEY_BOUNDS.minLat && bNum <= this.TURKEY_BOUNDS.maxLat; 35 const isLatLonTR = aNum >= this.TURKEY_BOUNDS.minLat && aNum <= this.TURKEY_BOUNDS.maxLat && 36 bNum >= this.TURKEY_BOUNDS.minLon && bNum <= this.TURKEY_BOUNDS.maxLon; 37 if (isLonLatTR && !isLatLonTR) return "lonlat"; 38 if (isLatLonTR && !isLonLatTR) return "latlon"; 39 40 // Genel küresel aralıklarla ikincil kontrol 41 const looksLonLat = Math.abs(aNum) <= 180 && Math.abs(bNum) <= 90; 42 const looksLatLon = Math.abs(aNum) <= 90 && Math.abs(bNum) <= 180; 43 if (looksLonLat && !looksLatLon) return "lonlat"; 44 if (looksLatLon && !looksLonLat) return "latlon"; 45 46 // İkisi de mümkün görünüyorsa lon,lat varsay 47 return "lonlat"; 48 } 49 50 /** 51 * Convert coordinates to [lat, lon] format for Leaflet 52 */ 53 toLatLng(coords) { 54 if (!Array.isArray(coords) || coords.length !== 2) { 55 throw new Error('Invalid coordinates format'); 56 } 57 58 const order = this.detectCoordinateOrder(coords); 59 if (order === "lonlat") { 60 return [coords[1], coords[0]]; // [lat, lon] 61 } else if (order === "latlon") { 62 return [coords[0], coords[1]]; // [lat, lon] 63 } else { 64 throw new Error('Unable to detect coordinate order'); 65 } 66 } 67 68 /** 69 * Convert coordinates to [lon, lat] format for Mapbox 70 */ 71 toLngLat(coords) { 72 if (!Array.isArray(coords) || coords.length !== 2) { 73 throw new Error('Invalid coordinates format'); 74 } 75 76 const order = this.detectCoordinateOrder(coords); 77 if (order === "lonlat") { 78 return [coords[0], coords[1]]; // [lon, lat] 79 } else if (order === "latlon") { 80 return [coords[1], coords[0]]; // [lon, lat] 81 } else { 82 throw new Error('Unable to detect coordinate order'); 83 } 84 } 85 86 /** 87 * Convert coordinate array to Leaflet format 88 */ 89 coordsToLatLngs(coords) { 90 if (!Array.isArray(coords)) return []; 91 92 if (Array.isArray(coords[0]) && Array.isArray(coords[0][0])) { 93 // Polygon rings - use exterior ring 94 return coords[0].map(pt => this.toLatLng(pt)); 95 } 96 97 return coords.map(pt => this.toLatLng(pt)); 98 } 99 100 /** 101 * Convert coordinate array to Mapbox format 102 */ 103 coordsToLngLats(coords) { 104 if (!Array.isArray(coords)) return []; 105 106 if (Array.isArray(coords[0]) && Array.isArray(coords[0][0])) { 107 // Polygon rings - use exterior ring 108 return coords[0].map(pt => this.toLngLat(pt)); 109 } 110 111 return coords.map(pt => this.toLngLat(pt)); 112 } 113 114 /** 115 * Extract polygons from GeoJSON 116 */ 117 extractPolygonsFromGeoJSON(geojson) { 118 // Extracting polygons from geojson 119 const polys = []; 120 121 const pushGeom = (geom) => { 122 if (!geom) { 123 return; 124 } 125 126 if (geom.type === "Polygon") { 127 polys.push(this.coordsToLatLngs(geom.coordinates)); 128 } else if (geom.type === "MultiPolygon") {
129 (geom.coordinates || []).forEach(rings => { 130 polys.push(this.coordsToLatLngs(rings)); 131 }); 132 } else if (geom.type === "LineString") { 133 polys.push((geom.coordinates || []).map(pt => this.toLatLng(pt))); 134 } 135 }; 136 137 if (!geojson) return polys; 138 139 if (geojson.type === "FeatureCollection") { 140 (geojson.features || []).forEach(f => pushGeom(f && f.geometry)); 141 } else if (geojson.type === "Feature") { 142 pushGeom(geojson.geometry); 143 } else if (geojson.type) { 144 pushGeom(geojson); 145 } else if (Array.isArray(geojson)) { 146 polys.push(this.coordsToLatLngs(geojson)); 147 } 148 149 return polys; 150 } 151 152 /** 153 * Build GeoJSON FeatureCollection for Mapbox 154 */ 155 buildGeoJSONFeatureCollection(data) { 156 // Building FeatureCollection from data 157 const fc = { type: "FeatureCollection", features: [] }; 158 const qb = data && data.quarter_border; 159 160 if (!qb) { 161 return fc; 162 } 163 164 const addFeature = (geom) => { 165 if (!geom) { 166 return; 167 } 168 // Adding feature with geometry 169 fc.features.push({ type: "Feature", properties: {}, geometry: geom }); 170 }; 171 172 if (qb.type === "FeatureCollection") { 173 // Processing as FeatureCollection 174 (qb.features || []).forEach((f, index) => { 175 // Processing feature 176 addFeature(f && f.geometry); 177 }); 178 } else if (qb.type === "Feature") { 179 // Processing as Feature 180 addFeature(qb.geometry); 181 } else if (qb.type === "Polygon" || qb.type === "MultiPolygon" || qb.type === "LineString") { 182 // Processing as Geometry 183 addFeature(qb); 184 } else if (Array.isArray(qb)) { 185 // Processing as Array 186 addFeature({ type: "Polygon", coordinates: [qb] }); 187 } else { 188 } 189 190 // Final FeatureCollection created 191 return fc; 192 } 193 194 /** 195 * Validate coordinates are within Turkey bounds 196 */ 197 validateTurkeyCoordinates(coords) { 198 if (!Array.isArray(coords) || coords.length !== 2) { 199 return false; 200 } 201 202 const [lon, lat] = this.toLngLat(coords); 203 204 return (lon >= this.TURKEY_BOUNDS.minLon && lon <= this.TURKEY_BOUNDS.maxLon && 205 lat >= this.TURKEY_BOUNDS.minLat && lat <= this.TURKEY_BOUNDS.maxLat); 206 } 207 208 /** 209 * Calculate distance between two points (Haversine formula) 210 */ 211 calculateDistance(lat1, lon1, lat2, lon2) { 212 const R = 6371; // Earth's radius in kilometers 213 const dLat = this.toRadians(lat2 - lat1); 214 const dLon = this.toRadians(lon2 - lon1); 215 216 const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 217 Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * 218 Math.sin(dLon / 2) * Math.sin(dLon / 2); 219 220 const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 221 return R * c; 222 } 223 224 /** 225 * Convert degrees to radians 226 */ 227 toRadians(degrees) { 228 return degrees * (Math.PI / 180); 229 } 230 231 /** 232 * Convert radians to degrees 233 */ 234 toDegrees(radians) { 235 return radians * (180 / Math.PI); 236 } 237 238 /** 239 * Calculate center point of multiple coordinates 240 */ 241 calculateCenter(coords) { 242 if (!Array.isArray(coords) || coords.length === 0) { 243 return null; 244 } 245 246 let sumLat = 0; 247 let sumLon = 0; 248 249 coords.forEach(coord => { 250 const [lat, lon] = this.toLatLng(coord); 251 sumLat += lat; 252 sumLon += lon; 253 }); 254 255 return [sumLat / coords.length, sumLon / coords.length]; 256 } 257 258 /** 259 * Calculate bounding box 260 */ 261 calculateBoundingBox(coords) { 262 if (!Array.isArray(coords) || coords.length === 0) { 263 return null; 264 } 265 266 let minLat = Infinity; 267 let maxLat = -Infinity; 268 let minLon = Infinity; 269 let maxLon = -Infinity; 270 271 coords.forEach(coord => { 272 const [lat, lon] = this.toLatLng(coord); 273 minLat = Math.min(minLat, lat); 274 maxLat = Math.max(maxLat, lat); 275 minLon = Math.min(minLon, lon);
276 maxLon = Math.max(maxLon, lon); 277 }); 278 279 return { 280 north: maxLat, 281 south: minLat, 282 east: maxLon, 283 west: minLon 284 }; 285 } 286 287 /** 288 * Format coordinates for display 289 */ 290 formatCoordinates(lat, lon, precision = 6) { 291 const latStr = lat.toFixed(precision); 292 const lonStr = lon.toFixed(precision); 293 return `${latStr}, ${lonStr}`; 294 } 295 296 /** 297 * Parse coordinate string 298 */ 299 parseCoordinateString(str) { 300 if (!str || typeof str !== 'string') { 301 return null; 302 } 303 304 const parts = str.split(',').map(s => s.trim()); 305 if (parts.length !== 2) { 306 return null; 307 } 308 309 const lat = parseFloat(parts[0]); 310 const lon = parseFloat(parts[1]); 311 312 if (isNaN(lat) || isNaN(lon)) { 313 return null; 314 } 315 316 return [lat, lon]; 317 } 318 319 /** 320 * Simple deep swap helper for arrays of coordinates 321 */ 322 _swapPairsDeep(input) { 323 const isPair = (p) => Array.isArray(p) && p.length === 2 && isFinite(p[0]) && isFinite(p[1]); 324 const swap = (p) => isPair(p) ? [p[1], p[0]] : p; 325 const walk = (node) => Array.isArray(node) ? node.map(el => Array.isArray(el) ? walk(el) : swap(el)) : node; 326 return walk(input); 327 } 328 329 /** 330 * Swap lat/lon pairs inside a WKT string 331 * Supports: POINT, LINESTRING, POLYGON, MULTI*, GEOMETRYCOLLECTION 332 */ 333 swapLatLonInWKT(wkt) { return wkt; } 334 335 /** 336 * Convert basic WKT to GeoJSON Geometry/Feature 337 */ 338 wktToGeoJSON(wkt) { return null; } 339 340 /** 341 * Normalize any geometry-like input to swap lat/lon order. 342 * - GeoJSON Feature/FeatureCollection/Geometry 343 * - Arrays of coordinates 344 * - WKT strings (returns WKT string with swapped pairs) 345 * This is a simple swap; no CRS detection. 346 */ 347 normalizeGeometrySimple(input) { 348 if (!input) return input; 349 350 // WKT string 351 if (typeof input === 'string') { 352 const upper = input.trim().toUpperCase(); 353 if (/(POINT|LINESTRING|POLYGON|MULTI|GEOMETRYCOLLECTION)/.test(upper)) { 354 return this.swapLatLonInWKT(input); 355 } 356 return input; 357 } 358 359 // GeoJSON Feature 360 if (input && input.type === 'Feature') { 361 const geom = input.geometry || null; 362 const swapped = geom && geom.coordinates ? this._swapPairsDeep(geom.coordinates) : geom; 363 return { type: 'Feature', properties: input.properties || {}, geometry: geom ? { ...geom, coordinates: swapped } : geom }; 364 } 365 366 // GeoJSON FeatureCollection 367 if (input && input.type === 'FeatureCollection') { 368 const feats = (input.features || []).map(f => this.normalizeGeometrySimple(f)); 369 return { type: 'FeatureCollection', features: feats }; 370 } 371 372 // GeoJSON Geometry 373 if (input && typeof input.type === 'string' && input.coordinates) { 374 return { type: input.type, coordinates: this._swapPairsDeep(input.coordinates) }; 375 } 376 377 // Array of coordinates 378 if (Array.isArray(input)) { 379 return this._swapPairsDeep(input); 380 } 381 382 // Unknown type => return as-is 383 return input; 384 } 385 386 /** 387 * Decide if a layer should be normalized (swap lat/lon) 388 */ 389 shouldNormalizeLayer(layerId) { 390 return !!(layerId && this.normalizedLayerIds.has(layerId)); 391 } 392 393 /** 394 * Normalize input for a given layer (no-op if layer not enabled) 395 */ 396 normalizeForLayer(layerId, input) { 397 if (!this.shouldNormalizeLayer(layerId)) return input; 398 return input; 399 } 400 401 /** 402 * Manage layer normalization list 403 */ 404 setNormalizeLayers(layerIds) { 405 this.normalizedLayerIds = new Set(Array.isArray(layerIds) ? layerIds : []); 406 } 407 408 addNormalizeLayer(layerId) { 409 try { if (layerId) this.normalizedLayerIds.add(layerId); } catch(_) {} 410 } 411 412 removeNormalizeLayer(layerId) { 413 try { if (layerId) this.normalizedLayerIds.delete(layerId); } catch(_) {} 414 } 415} 416 417// Create global instance 418window.coordinateUtils = new CoordinateUtils(); 419 420// Export for module systems 421if (typeof module !== 'undefined' && module.exports) { 422 module.exports = CoordinateUtils; 423}
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.