<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบแปลงค่าพิกัด Geospatial Coordinate Converter</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.0/proj4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
<style>
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.coordinate-section {
background-color: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
}
.result-section {
background-color: #f5f3ff;
border-radius: 10px;
padding: 20px;
margin-top: 20px;
border: 1px solid #e9e4ff;
}
.error-section {
background-color: #fff5f5;
border: 1px solid #fed7d7;
color: #c53030;
border-radius: 10px;
padding: 15px;
margin-top: 20px;
}
.csv-preview {
max-height: 300px;
overflow-y: auto;
background-color: white;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 15px;
}
.footer {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
padding: 20px 0;
margin-top: 50px;
}
.btn-success {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border: none !important;
transition: 0.3s;
}
.btn-success:hover {
filter: brightness(1.1);
transform: translateY(-1px);
}
.btn-outline-success {
color: #764ba2 !important;
border-color: #764ba2 !important;
}
.btn-outline-success:hover {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
}
.table-striped > tbody > tr:nth-of-type(odd) > * {
background-color: rgba(102, 126, 234, 0.05) !important;
box-shadow: none !important;
}
.table thead th {
color: #000000 !important;
border-bottom: 2px solid #e9e4ff;
font-weight: 700;
}
.table-danger {
--bs-table-bg: #fff5f5 !important;
color: #c53030 !important;
}
</style>
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<header class="row">
<div class="col-12">
<div class="card mt-3">
<div class="card-header text-center">
<h1 class="mb-0"><i class="bi bi-geo-alt"></i> ระบบแปลงค่าพิกัด Geospatial</h1>
<p class="mb-0">แปลงค่าพิกัดระหว่าง WGS84/UTM (47N/48N) และ DOL Indian 1975 (24047/24048)</p>
</div>
</div>
</div>
</header>
<div class="row mt-4">
<!-- Single Point Conversion Section -->
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<h4><i class="bi bi-bullseye"></i> แปลงพิกัดจุดเดียว</h4>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 border-end"> <h5 class="text-primary mb-3"><i class="bi bi-box-arrow-in-right"></i> ข้อมูลต้นทาง</h5>
<div class="mb-3">
<label class="form-label">ระบบพิกัดต้นทาง</label>
<select class="form-select" id="sourceCRS">
<option value="EPSG:4326" selected>EPSG:4326 (WGS84 Lat/Lon)</option>
<option value="EPSG:32647">EPSG:32647 (UTM Zone 47N / WGS84)</option>
<option value="EPSG:32648">EPSG:32648 (UTM Zone 48N / WGS84)</option>
<option value="EPSG:4240">EPSG:4240 (Lat/Lon / Indian 1975 - DOL)</option>
<option value="EPSG:24047">EPSG:24047 (UTM Zone 47N / Indian 1975 - DOL)</option>
<option value="EPSG:24048">EPSG:24048 (UTM Zone 48N / Indian 1975 - DOL)</option>
<option value="EPSG:3857">EPSG:3857 (Web Mercator)</option>
</select>
</div>
<div class="mb-3" id="singleLatLonFormatDiv" style="display: none;">
<label class="form-label">รูปแบบพิกัด (Lat/Lon)</label>
<select class="form-select" id="singleLatLonFormat">
<option value="DD" selected>ทศนิยม (Decimal Degrees)</option>
<option value="DMS">องศา ลิปดา ฟิลิปดา (DMS)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label" id="labelInputX">X / Longitude</label>
<input type="number" class="form-control" id="inputX" placeholder="" step="any">
<div class="input-group" id="inputX_dms_group" style="display:none;">
<input type="number" class="form-control" id="inputX_d" placeholder="">
<span class="input-group-text bg-light">°</span>
<input type="number" class="form-control" id="inputX_m" placeholder="">
<span class="input-group-text bg-light">'</span>
<input type="number" class="form-control" id="inputX_s" placeholder="">
<span class="input-group-text bg-light">"</span>
</div>
</div>
<div class="mb-3">
<label class="form-label" id="labelInputY">Y / Latitude</label>
<input type="number" class="form-control" id="inputY" placeholder="" step="any">
<div class="input-group" id="inputY_dms_group" style="display:none;">
<input type="number" class="form-control" id="inputY_d" placeholder="">
<span class="input-group-text bg-light">°</span>
<input type="number" class="form-control" id="inputY_m" placeholder="">
<span class="input-group-text bg-light">'</span>
<input type="number" class="form-control" id="inputY_s" placeholder="">
<span class="input-group-text bg-light">"</span>
</div>
</div>
<button class="btn btn-primary w-100 mt-3" id="convertBtn">
<i class="bi bi-arrow-repeat"></i> แปลงพิกัด
</button>
<div id="errorMessage" class="alert alert-danger mt-3" style="display: none;">
<i class="bi bi-exclamation-triangle"></i> <span id="errorText"></span>
</div>
</div>
<div class="col-md-6">
<h5 class="text-success mb-3"><i class="bi bi-box-arrow-right"></i> ข้อมูลปลายทาง</h5>
<div class="mb-3">
<label class="form-label">ระบบพิกัดปลายทาง</label>
<select class="form-select" id="targetCRS">
<option value="EPSG:32647" selected>EPSG:32647 (UTM Zone 47N / WGS84)</option>
<option value="EPSG:32648">EPSG:32648 (UTM Zone 48N / WGS84)</option>
<option value="EPSG:4240">EPSG:4240 (Lat/Lon / Indian 1975 - DOL)</option>
<option value="EPSG:24047">EPSG:24047 (UTM Zone 47N / Indian 1975 - DOL)</option>
<option value="EPSG:24048">EPSG:24048 (UTM Zone 48N / Indian 1975 - DOL)</option>
<option value="EPSG:3857">EPSG:3857 (Web Mercator)</option>
<option value="EPSG:4326">EPSG:4326 (WGS84 Lat/Lon)</option>
</select>
</div>
<div id="singleResult" style="display: none; background-color: #f1f8ff; padding: 15px; border-radius: 10px; border: 1px solid #d0e3ff;">
<div class="mb-3">
<label class="form-label fw-bold" id="labelOutputX">X / Longitude / Easting</label>
<input type="text" class="form-control bg-white" id="outputX" readonly>
</div>
<div class="mb-3">
<label class="form-label fw-bold" id="labelOutputY">Y / Latitude / Northing</label>
<input type="text" class="form-control bg-white" id="outputY" readonly>
</div>
<hr>
<div class="row">
<div class="col-6">
<label class="form-label small">Scale Factor (k)</label>
<div class="input-group input-group-sm">
<input type="number" class="form-control" id="scaleFactor" step="0.00000001">
<button class="btn btn-outline-secondary" type="button" onclick="recalcFromFactors()">แก้</button>
</div>
</div>
<div class="col-6">
<label class="form-label small">Convergence (γ)</label>
<input type="text" class="form-control form-control-sm" id="gridConvergence" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">
<h4><i class="bi bi-file-earmark-spreadsheet"></i> แปลงไฟล์ CSV</h4>
</div>
<div class="card-body">
<div class="coordinate-section">
<h5>อัปโหลดไฟล์ CSV</h5>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">ระบบพิกัดต้นทาง</label>
<select class="form-select" id="csvSourceCRS">
<option value="EPSG:4326" selected>EPSG:4326 (WGS84 Lat/Lon)</option>
<option value="EPSG:32647">EPSG:32647 (UTM Zone 47N / WGS84)</option>
<option value="EPSG:32648">EPSG:32648 (UTM Zone 48N / WGS84)</option>
<option value="EPSG:4240">EPSG:4240 (Lat/Lon / Indian 1975 - DOL)</option>
<option value="EPSG:24047">EPSG:24047 (UTM Zone 47N / Indian 1975 - DOL)</option>
<option value="EPSG:24048">EPSG:24048 (UTM Zone 48N / Indian 1975 - DOL)</option>
<option value="EPSG:3857">EPSG:3857 (Web Mercator)</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">ระบบพิกัดปลายทาง</label>
<select class="form-select" id="csvTargetCRS">
<option value="EPSG:32647" selected>EPSG:32647 (UTM Zone 47N / WGS84)</option>
<option value="EPSG:32648">EPSG:32648 (UTM Zone 48N / WGS84)</option>
<option value="EPSG:4240">EPSG:4240 (Lat/Lon / Indian 1975 - DOL)</option>
<option value="EPSG:24047">EPSG:24047 (UTM Zone 47N / Indian 1975 - DOL)</option>
<option value="EPSG:24048">EPSG:24048 (UTM Zone 48N / Indian 1975 - DOL)</option>
<option value="EPSG:3857">EPSG:3857 (Web Mercator)</option>
<option value="EPSG:4326">EPSG:4326 (WGS84 Lat/Lon)</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">รูปแบบ Column</label>
<select class="form-select" id="csvFormat">
<option value="Point_X_Y" selected>Point, X, Y </option>
<option value="Point_Y_X">Point, Y, X </option>
<option value="Point_E_N">Point, E, N </option>
<option value="Point_N_E">Point, N, E </option>
<option value="Point_Lat_Lon">Point, Lat, Lon </option>
<option value="Point_Lon_Lat">Point, Lon, Lat </option>
</select>
</div>
<div class="mb-3" id="csvLatLonFormatDiv" style="display: none;">
<label class="form-label">รูปแบบข้อมูล Lat/Lon ในไฟล์</label>
<select class="form-select" id="csvLatLonFormat">
<option value="DD" selected>ทศนิยม (Decimal Degrees)</option>
<option value="DMS">องศา ลิปดา ฟิลิปดา (DMS) เช่น 100° 20' 30"</option>
</select>
</div>
<div class="mb-3">
<label for="csvFile" class="form-label">เลือกไฟล์ CSV</label>
<input class="form-control" type="file" id="csvFile" accept=".csv">
</div>
<div class="mb-3">
<label class="form-label">กำหนดค่า Scale Factor (Optional)</label>
<input type="number" class="form-control" id="csvManualScale" step="0.00000001" placeholder="ปล่อยว่างเพื่อคำนวณอัตโนมัติ">
<div class="form-text text-muted">
<i class="bi bi-info-circle"></i> หากใส่ค่านี้ ระบบจะปรับแก้พิกัดผลลัพธ์เป็น Ground Distance (Grid / k) ให้ทุกจุด
</div>
</div>
<button class="btn btn-success w-100" id="processCSVBtn" disabled>
<i class="bi bi-gear"></i> ประมวลผลไฟล์ CSV
</button>
</div>
<div id="csvResults" style="display: none;">
<div class="result-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 style="color: #000000;"><i class="bi bi-table text-primary"></i> ผลลัพธ์ CSV</h5>
<button class="btn btn-outline-success" id="downloadBtn">
<i class="bi bi-download"></i> ดาวน์โหลด CSV
</button>
</div>
<div id="csvPreview" class="csv-preview"></div>
<div class="mt-2">
<small class="text-muted">
<span id="recordCount"></span> รายการ | แสดงเพียง 10 รายการแรก
</small>
</div>
</div>
</div>
<div id="csvError" class="error-section" style="display: none;">
<i class="bi bi-exclamation-triangle text-danger"></i>
<span id="csvErrorText"></span>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4><i class="bi bi-info-circle"></i> ข้อมูลระบบพิกัด</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4 col-lg-3">
<div class="card border-primary">
<div class="card-body">
<h6 class="card-title text-primary">EPSG:4326</h6>
<p class="card-text">WGS84 Latitude/Longitude<br>
หน่วย: องศา (degrees)<br>
ใช้งาน: GPS, แผนที่ออนไลน์</p>
</div>
</div>
</div>
<div class="col-md-4 col-lg-3">
<div class="card border-success">
<div class="card-body">
<h6 class="card-title text-success">EPSG:32647</h6>
<p class="card-text">UTM Zone 47N (WGS84)<br>
หน่วย: เมตร (meters)<br>
ใช้งาน: ประเทศไทยฝั่งตะวันตก</p>
</div>
</div>
</div>
<div class="col-md-4 col-lg-3">
<div class="card border-warning">
<div class="card-body">
<h6 class="card-title text-warning">EPSG:32648</h6>
<p class="card-text">UTM Zone 48N (WGS84)<br>
หน่วย: เมตร (meters)<br>
ใช้งาน: ประเทศไทยฝั่งตะวันออก</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card border-info">
<div class="card-body">
<h6 class="card-title text-info">EPSG:24047</h6>
<p class="card-text">UTM Zone 47N (Indian 1975 - DOL)<br>
หน่วย: เมตร (meters)<br>
ใช้งาน: งานรังวัด/แผนที่เก่า (ฝั่งตะวันตก)</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card border-info">
<div class="card-body">
<h6 class="card-title text-info">EPSG:24048</h6>
<p class="card-text">UTM Zone 48N (Indian 1975 - DOL)<br>
หน่วย: เมตร (meters)<br>
ใช้งาน: งานรังวัด/แผนที่เก่า (ฝั่งตะวันออก)</p>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card border-secondary">
<div class="card-body">
<h6 class="card-title text-secondary">EPSG:3857</h6>
<p class="card-text">Web Mercator<br>
หน่วย: เมตร (meters)<br>
ใช้งาน: แผนที่ออนไลน์ (XYZ tiles)</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="footer">
<p>© 2024 Geospatial Coordinate Converter | Powered by Proj4.js & Bootstrap</p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
proj4.defs("EPSG:4326", "+proj=longlat +datum=WGS84 +no_defs");
proj4.defs("EPSG:32647", "+proj=utm +zone=47 +datum=WGS84 +units=m +no_defs");
proj4.defs("EPSG:32648", "+proj=utm +zone=48 +datum=WGS84 +units=m +no_defs");
proj4.defs("EPSG:4240", "+proj=longlat +ellps=evrst30 +towgs84=204.4798,837.8940,294.7765,0,0,0,0 +no_defs +type=crs");
proj4.defs("EPSG:24047", "+proj=utm +zone=47 +a=6377276.345 +b=6356075.41314024 +towgs84=204.4798,837.8940,294.7765,0,0,0,0 +units=m +no_defs +type=crs");
proj4.defs("EPSG:24048", "+proj=utm +zone=48 +ellps=evrst30 +towgs84=204.4798,837.8940,294.7765,0,0,0,0 +units=m +no_defs +type=crs");
proj4.defs("EPSG:3857", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +type=crs");
let csvData = [];
let convertedData = [];
let csvParsed = null;
function parseDmsStringToDD(dmsStr) {
if (!dmsStr) return NaN;
const s = String(dmsStr).trim().toUpperCase();
let sign = (s.includes('S') || s.includes('W') || s.startsWith('-')) ? -1 : 1;
const parts = s.match(/\d+(\.\d+)?/g);
if (!parts || parts.length === 0) return NaN;
let deg = parseFloat(parts[0]) || 0;
let min = parts.length > 1 ? parseFloat(parts[1]) : 0;
let sec = parts.length > 2 ? parseFloat(parts[2]) : 0;
return sign * (deg + (min / 60) + (sec / 3600));
}
function stripBom(text) {
if (!text) return text;
return text.charAt(0) === '\ufeff' ? text.slice(1) : text;
}
function normalizeHeaderName(name) {
return String(name || '')
.replace(/^\ufeff/, '')
.trim()
.toLowerCase()
.replace(/["']/g, '')
.replace(/[\s\-_]+/g, '');
}
function parseCsvFile(csvText, fileName) {
const results = Papa.parse(csvText, {
skipEmptyLines: true,
guessHeader: false
});
if (results.errors.length > 0) {
console.warn("CSV Parse Warnings:", results.errors);
}
const rows = results.data;
if (!rows || rows.length === 0) {
throw new Error('ไฟล์ว่าง หรือไม่พบข้อมูล CSV');
}
const delimiter = results.meta.delimiter;
const maxColumns = rows.reduce((m, r) => Math.max(m, r.length), 0);
const firstRow = rows[0] || [];
const knownHeaderNames = [
'point', 'pt', 'name', 'id',
'x', 'y', 'e', 'n',
'lon', 'lat', 'long', 'longitude', 'latitude',
'easting', 'northing'
].map(normalizeHeaderName);
const firstRowLooksLikeHeader = firstRow
.map(normalizeHeaderName)
.some(h => knownHeaderNames.includes(h));
let headers = [];
let dataRows = [];
let hasHeader = false;
if (firstRowLooksLikeHeader) {
hasHeader = true;
headers = firstRow.map(h => stripBom(String(h || '')).trim());
dataRows = rows.slice(1);
} else {
hasHeader = false;
headers = Array.from({ length: maxColumns }, (_, i) => `col${i + 1}`);
dataRows = rows;
}
while (headers.length < maxColumns) {
headers.push(`extra_${headers.length + 1}`);
}
return {
fileName: fileName || 'uploaded.csv',
delimiter,
headers,
rows: dataRows,
hasHeader,
maxColumns
};
}
function escapeCsvField(value, delimiter) {
const str = value === null || value === undefined ? '' : String(value);
const mustQuote = str.includes('"') || str.includes('\n') || str.includes('\r') || str.includes(delimiter);
if (!mustQuote) return str;
return `"${str.replace(/"/g, '""')}"`;
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function parseNumberFlexible(value, inputDelimiter) {
let s = String(value ?? '').trim();
if (!s) return NaN;
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
s = s.slice(1, -1).trim();
}
s = s.replace(/\s+/g, '');
const hasDot = s.includes('.');
const hasComma = s.includes(',');
if (hasDot && hasComma) {
const lastDot = s.lastIndexOf('.');
const lastComma = s.lastIndexOf(',');
if (lastComma > lastDot) {
s = s.replace(/\./g, '').replace(/,/g, '.');
} else {
s = s.replace(/,/g, '');
}
} else if (hasComma && !hasDot) {
if (inputDelimiter && inputDelimiter !== ',') {
s = s.replace(/,/g, '.');
} else {
s = s.replace(/,/g, '');
}
}
const num = Number(s);
return Number.isFinite(num) ? num : NaN;
}
function isLikelyLonLatSwap4326(x, y) {
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
const ax = Math.abs(x);
const ay = Math.abs(y);
return ax <= 90 && ay > 90 && ay <= 180;
}
function validateSourceCoordinates(sourceCRS, x, y) {
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return { ok: false, message: 'ค่าพิกัดไม่เป็นตัวเลข' };
}
if (sourceCRS === 'EPSG:4326' || sourceCRS === 'EPSG:4240') {
const lon = x;
const lat = y;
if (Math.abs(lon) > 180) return { ok: false, message: 'Longitude เกินช่วง (-180..180)' };
if (Math.abs(lat) > 90) return { ok: false, message: 'Latitude เกินช่วง (-90..90)' };
return { ok: true };
}
if (sourceCRS === 'EPSG:3857') {
const limit = 20037508.3427892;
if (Math.abs(x) > limit || Math.abs(y) > limit) {
return { ok: false, message: 'พิกัด Web Mercator เกินช่วงที่เป็นไปได้' };
}
return { ok: true };
}
if (sourceCRS.startsWith('EPSG:326') || sourceCRS.startsWith('EPSG:240')) {
const eastingOk = x >= 0 && x <= 1000000;
const northingOk = y >= 0 && y <= 10000000;
if (!eastingOk || !northingOk) {
return { ok: false, message: 'ค่า Easting/Northing อยู่นอกช่วงที่เป็นไปได้' };
}
return { ok: true };
}
return { ok: true };
}
function getOutputFieldNamesByTargetCrs(targetCRS) {
if (targetCRS === 'EPSG:4326' || targetCRS === 'EPSG:4240') return { outX: 'lon_out', outY: 'lat_out' };
if (targetCRS.startsWith('EPSG:326') || targetCRS.startsWith('EPSG:240')) return { outX: 'e_out', outY: 'n_out' };
return { outX: 'x_out', outY: 'y_out' };
}
function buildUniqueOutputHeaders(baseHeaders, extraHeaders) {
const headers = [...baseHeaders];
const mapping = {};
extraHeaders.forEach(h => {
let name = h;
let n = 1;
while (headers.includes(name)) {
n++;
name = `${h}_${n}`;
}
headers.push(name);
mapping[h] = name;
});
return { headers, mapping };
}
function findColumnIndexByCandidates(headers, candidates) {
const normalizedHeaders = headers.map(normalizeHeaderName);
for (const cand of candidates) {
const idx = normalizedHeaders.indexOf(normalizeHeaderName(cand));
if (idx !== -1) return idx;
}
return -1;
}
function degToRad(deg) { return deg * (Math.PI / 180); }
function radToDmsStr(rad) {
let d = rad * (180 / Math.PI);
const sign = d < 0 ? "-" : "";
d = Math.abs(d);
const deg = Math.floor(d);
const minFloat = (d - deg) * 60;
const min = Math.floor(minFloat);
const sec = ((minFloat - min) * 60).toFixed(4);
return `${sign}${deg}° ${min}' ${sec}"`;
}
function calculateGridFactors(lat, lon, zoneNumber, datum) {
let a, f_inv;
if (datum.includes('Indian') || datum.includes('240')) {
a = 6377276.345; f_inv = 300.8017;
} else {
a = 6378137.0; f_inv = 298.257223563;
}
const f = 1 / f_inv;
const e2 = 2*f - f*f;
const k0 = 0.9996;
const cmLon = (zoneNumber * 6) - 183;
const phi = degToRad(lat);
const lambda = degToRad(lon);
const lambda0 = degToRad(cmLon);
const dLambda = lambda - lambda0;
const t = Math.tan(phi);
const nu = a / Math.sqrt(1 - e2 * Math.sin(phi) * Math.sin(phi));
const gammaRad = dLambda * Math.sin(phi);
const x_approx = dLambda * Math.cos(phi) * nu;
const k = k0 * (1 + (x_approx * x_approx) / (2 * k0 * k0 * a * a));
return { k: k.toFixed(8), gammaStr: radToDmsStr(gammaRad) };
}
function recalcFromFactors() {
const currentX = parseFloat(document.getElementById('outputX').value);
const currentY = parseFloat(document.getElementById('outputY').value);
const userK = parseFloat(document.getElementById('scaleFactor').value);
if (isNaN(currentX) || isNaN(currentY) || isNaN(userK)) {
alert("กรุณาแปลงพิกัดก่อน หรือใส่ค่าสเกลให้ถูกต้อง");
return;
}
const falseE = 500000;
const falseN = 0;
const groundE = ((currentX - falseE) / userK) + falseE;
const groundN = ((currentY - falseN) / userK) + falseN;
document.getElementById('outputX').value = groundE.toFixed(4);
document.getElementById('outputY').value = groundN.toFixed(4);
}
function showError(message) {
document.getElementById('errorText').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('singleResult').style.display = 'none';
}
function showCSVError(message) {
document.getElementById('csvErrorText').textContent = message;
document.getElementById('csvError').style.display = 'block';
document.getElementById('csvResults').style.display = 'none';
}
function ddToDmsStr(dd) {
const d = Math.floor(Math.abs(dd));
const minFloat = (Math.abs(dd) - d) * 60;
const m = Math.floor(minFloat);
const s = ((minFloat - m) * 60).toFixed(4);
const sign = dd < 0 ? "-" : "";
return `${sign}${d}° ${m}' ${s}"`;
}
function getDmsInputAsDD(idPrefix) {
const dStr = document.getElementById(idPrefix + '_d').value;
const mStr = document.getElementById(idPrefix + '_m').value;
const sStr = document.getElementById(idPrefix + '_s').value;
if (dStr === '' && mStr === '' && sStr === '') {
return NaN;
}
const d = parseFloat(dStr) || 0;
const m = parseFloat(mStr) || 0;
const s = parseFloat(sStr) || 0;
return d + (m / 60) + (s / 3600);
}
function convertSinglePoint() {
const sourceCRS = document.getElementById('sourceCRS').value;
const targetCRS = document.getElementById('targetCRS').value;
const isLatLonSource = (sourceCRS === 'EPSG:4326' || sourceCRS === 'EPSG:4240');
const singleLatLonFormat = document.getElementById('singleLatLonFormat').value;
let inputX, inputY;
if (isLatLonSource) {
if (singleLatLonFormat === 'DMS') {
inputX = getDmsInputAsDD('inputX');
inputY = getDmsInputAsDD('inputY');
} else {
inputX = parseFloat(document.getElementById('inputX').value);
inputY = parseFloat(document.getElementById('inputY').value);
}
} else {
inputX = parseFloat(document.getElementById('inputX').value);
inputY = parseFloat(document.getElementById('inputY').value);
}
document.getElementById('singleResult').style.display = 'none';
document.getElementById('errorMessage').style.display = 'none';
if (isNaN(inputX) || isNaN(inputY)) {
document.getElementById('errorText').innerText = 'กรุณากรอกค่าพิกัดให้ครบถ้วน';
document.getElementById('errorMessage').style.display = 'block';
return;
}
if (sourceCRS === targetCRS) {
document.getElementById('errorText').innerText = 'ระบบพิกัดต้นทางและปลายทางต้องแตกต่างกัน';
document.getElementById('errorMessage').style.display = 'block';
return;
}
try {
const result = proj4(sourceCRS, targetCRS, [inputX, inputY]);
const isUtmTarget = targetCRS.startsWith('EPSG:326') || targetCRS.startsWith('EPSG:240');
const isLatLonTarget = (targetCRS === 'EPSG:4326' || targetCRS === 'EPSG:4240');
if (isLatLonTarget) {
document.getElementById('outputX').value = ddToDmsStr(result[0]);
document.getElementById('outputY').value = ddToDmsStr(result[1]);
} else {
document.getElementById('outputX').value = result[0].toFixed(3);
document.getElementById('outputY').value = result[1].toFixed(3);
}
if (isUtmTarget) {
const wgs84Pt = proj4(targetCRS, 'EPSG:4326', [result[0], result[1]]);
let zone = 47;
if (targetCRS.includes('48')) zone = 48;
const factors = calculateGridFactors(wgs84Pt[1], wgs84Pt[0], zone, targetCRS);
document.getElementById('scaleFactor').value = factors.k;
document.getElementById('gridConvergence').value = factors.gammaStr;
} else {
document.getElementById('scaleFactor').value = "";
document.getElementById('gridConvergence').value = "-";
}
document.getElementById('singleResult').style.display = 'block';
} catch (error) {
console.error(error);
let msg = error.message;
if (msg.includes('finite numbers')) {
msg = 'ค่าพิกัดต้องเป็นตัวเลขที่ถูกต้อง';
} else if (msg.includes('latitude')) {
msg = 'ค่า Latitude อยู่นอกขอบเขต';
}
document.getElementById('errorText').innerText = 'เกิดข้อผิดพลาด: ' + msg;
document.getElementById('errorMessage').style.display = 'block';
}
}
function handleFileUpload(event) {
const file = event.target.files[0];
const btn = document.getElementById('processCSVBtn');
const errorSection = document.getElementById('csvError');
const resultsSection = document.getElementById('csvResults');
btn.disabled = true;
errorSection.style.display = 'none';
resultsSection.style.display = 'none';
csvParsed = null;
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const text = e.target.result;
csvParsed = parseCsvFile(text, file.name);
btn.disabled = false;
} catch (error) {
document.getElementById('csvErrorText').textContent = error.message;
errorSection.style.display = 'block';
}
};
reader.onerror = function() {
document.getElementById('csvErrorText').textContent = 'ไม่สามารถอ่านไฟล์ได้';
errorSection.style.display = 'block';
};
reader.readAsText(file);
}
function processCSV() {
const sourceCRS = document.getElementById('csvSourceCRS').value;
const targetCRS = document.getElementById('csvTargetCRS').value;
const format = document.getElementById('csvFormat').value;
const btn = document.getElementById('processCSVBtn');
const manualScaleElement = document.getElementById('csvManualScale');
const manualScale = manualScaleElement ? parseFloat(manualScaleElement.value) : NaN;
document.getElementById('csvError').style.display = 'none';
document.getElementById('csvResults').style.display = 'none';
if (sourceCRS === targetCRS) {
showCSVError('ระบบพิกัดต้นทางและปลายทางต้องแตกต่างกัน');
return;
}
if (!csvParsed || !Array.isArray(csvParsed.rows) || csvParsed.rows.length === 0) {
showCSVError('กรุณาอัปโหลดไฟล์ CSV ก่อน');
return;
}
try {
if (btn) {
btn.disabled = true;
btn.dataset.originalText = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> กำลังประมวลผล...';
}
const headers = csvParsed.headers;
const rows = csvParsed.rows;
const delimiter = csvParsed.delimiter;
const fmt = format;
let pointIndex = 0;
let xIndex = 1;
let yIndex = 2;
if (fmt === 'Point_Y_X' || fmt === 'Point_N_E' || fmt === 'Point_Lat_Lon') {
xIndex = 2;
yIndex = 1;
}
if (csvParsed.maxColumns === 2) {
pointIndex = -1;
xIndex = 0;
yIndex = 1;
if (fmt === 'Point_Y_X' || fmt === 'Point_N_E' || fmt === 'Point_Lat_Lon') {
xIndex = 1; yIndex = 0;
}
}
if (xIndex < 0 || yIndex < 0) {
throw new Error(`ไม่พบคอลัมน์พิกัดในไฟล์ (ตรวจสอบชื่อ Header)`);
}
const xColName = csvParsed.hasHeader ? headers[xIndex] : `Col ${xIndex+1}`;
const yColName = csvParsed.hasHeader ? headers[yIndex] : `Col ${yIndex+1}`;
const { outX, outY } = getOutputFieldNamesByTargetCrs(targetCRS);
const extraOutHeaders = [outX, outY, 'scale_k', 'convergence', 'error'];
const { headers: outputHeaders, mapping } = buildUniqueOutputHeaders(headers, extraOutHeaders);
convertedData = [];
let successCount = 0;
let errorCount = 0;
let swapCount = 0;
let errorLogList = [];
const isUtmTarget = targetCRS.startsWith('EPSG:326') || targetCRS.startsWith('EPSG:240');
const csvLatLonFormat = document.getElementById('csvLatLonFormat') ? document.getElementById('csvLatLonFormat').value : 'DD';
const isLatLonSource = (sourceCRS === 'EPSG:4326' || sourceCRS === 'EPSG:4240');
rows.forEach((r, idx) => {
const rowIndex = idx + (csvParsed.hasHeader ? 2 : 1);
const rowObj = {};
for (let c = 0; c < outputHeaders.length; c++) rowObj[outputHeaders[c]] = '';
headers.forEach((h, i) => { rowObj[h] = (r[i] ?? '').toString().trim(); });
const pointValue = pointIndex >= 0 ? (r[pointIndex] ?? '').toString().trim() : '';
const pointDisplay = pointValue || `P${idx + 1}`;
const rawX = r[xIndex];
const rawY = r[yIndex];
let x, y;
if (isLatLonSource && csvLatLonFormat === 'DMS') {
x = parseDmsStringToDD(rawX);
y = parseDmsStringToDD(rawY);
} else {
x = parseNumberFlexible(rawX, delimiter);
y = parseNumberFlexible(rawY, delimiter);
}
let swapped = false;
if ((sourceCRS === 'EPSG:4326' || sourceCRS === 'EPSG:4240') && isLikelyLonLatSwap4326(x, y)) {
const tmp = x; x = y; y = tmp;
swapped = true;
swapCount++;
}
let outXVal = '';
let outYVal = '';
let err = '';
let factors = { k: '-', gammaStr: '-' };
if (!Number.isFinite(x)) {
err = `บรรทัด ${rowIndex}: ค่า "${rawX}" ไม่ถูกต้อง (คอลัมน์: ${xColName})`;
} else if (!Number.isFinite(y)) {
err = `บรรทัด ${rowIndex}: ค่า "${rawY}" ไม่ถูกต้อง (คอลัมน์: ${yColName})`;
} else {
const v = validateSourceCoordinates(sourceCRS, x, y);
if (!v.ok) {
err = `บรรทัด ${rowIndex}: ${v.message}`;
} else {
try {
let result = proj4(sourceCRS, targetCRS, [x, y]);
if (isUtmTarget) {
try {
const wgs84Pt = proj4(targetCRS, 'EPSG:4326', [result[0], result[1]]);
let zone = 47;
if (targetCRS.includes('48')) zone = 48;
factors = calculateGridFactors(wgs84Pt[1], wgs84Pt[0], zone, targetCRS);
} catch (e) { console.log(e); }
}
if (isUtmTarget && !isNaN(manualScale) && manualScale > 0) {
const falseE = 500000;
const falseN = 0;
result[0] = ((result[0] - falseE) / manualScale) + falseE;
result[1] = ((result[1] - falseN) / manualScale) + falseN;
factors.k = manualScale.toFixed(8) + " (Manual)";
}
const decimals = targetCRS === 'EPSG:4326' ? 8 : 3;
outXVal = Number(result[0]).toFixed(decimals);
outYVal = Number(result[1]).toFixed(decimals);
successCount++;
} catch (e) {
err = `บรรทัด ${rowIndex}: คำนวณผิดพลาด (${e.message})`;
}
}
}
if (err) {
errorCount++;
if (errorLogList.length < 5) errorLogList.push(err);
}
rowObj[mapping[outX]] = outXVal;
rowObj[mapping[outY]] = outYVal;
rowObj[mapping['scale_k']] = factors.k;
rowObj[mapping['convergence']] = factors.gammaStr;
rowObj[mapping['error']] = err ? (swapped ? `${err} (auto swap)` : err) : (swapped ? 'auto-swapped' : '');
convertedData.push({
point: pointDisplay,
inX: rawX ?? '',
inY: rawY ?? '',
outX: outXVal,
outY: outYVal,
scale: factors.k,
conv: factors.gammaStr,
error: rowObj[mapping['error']],
outputHeaders,
outputRow: rowObj,
delimiter
});
});
displayCSVResults();
let summaryText = `ทั้งหมด ${convertedData.length} | สำเร็จ ${successCount} | ผิดพลาด ${errorCount}`;
if (swapCount > 0) summaryText += ` | สลับ lon/lat ${swapCount}`;
document.getElementById('recordCount').textContent = summaryText;
const resultDiv = document.getElementById('csvResults');
const oldAlert = document.getElementById('errorSummaryAlert');
if (oldAlert) oldAlert.remove();
if (errorCount > 0) {
const alertDiv = document.createElement('div');
alertDiv.id = 'errorSummaryAlert';
alertDiv.className = 'error-section mt-3';
let logHtml = errorLogList.map(e => `<li>${e}</li>`).join('');
if (errorCount > 5) logHtml += `<li>...และอีก ${errorCount - 5} รายการ</li>`;
alertDiv.innerHTML = `
<h5><i class="bi bi-exclamation-circle"></i> พบข้อผิดพลาด ${errorCount} รายการ</h5>
<ul class="mb-0 small">${logHtml}</ul>
`;
resultDiv.insertBefore(alertDiv, resultDiv.firstChild);
}
} catch (error) {
showCSVError('เกิดข้อผิดพลาดในการประมวลผล: ' + error.message);
} finally {
if (btn) {
btn.disabled = false;
if (btn.dataset.originalText) btn.innerHTML = btn.dataset.originalText;
}
}
}
function displayCSVResults() {
const preview = document.getElementById('csvPreview');
if (!convertedData || convertedData.length === 0) {
preview.innerHTML = '';
return;
}
const targetCRS = document.getElementById('csvTargetCRS').value;
const format = document.getElementById('csvFormat').value.toLowerCase();
const outLabels = getOutputFieldNamesByTargetCrs(targetCRS);
let label1 = `${outLabels.outX.replace('_out','').toUpperCase()} (out)`;
let label2 = `${outLabels.outY.replace('_out','').toUpperCase()} (out)`;
const isSwap = (format === 'point_y_x' || format === 'point_n_e' || format === 'point_lat_lon');
if (isSwap) {
const tmp = label1; label1 = label2; label2 = tmp;
}
let html = '<table class="table table-sm table-striped">';
html += '<thead><tr>';
html += '<th>POINT</th>';
html += `<th>${label1}</th><th>${label2}</th>`;
html += `<th>Scale (k)</th><th>Convergence (γ)</th>`;
html += '</tr></thead><tbody>';
const displayData = convertedData.slice(0, 10);
displayData.forEach(row => {
const errClass = row.error ? 'table-danger' : '';
let val1 = row.outX;
let val2 = row.outY;
if (isSwap) {
const tmpVal = val1; val1 = val2; val2 = tmpVal;
}
html += `<tr class="${errClass}">` +
`<td>${escapeHtml(row.point)}</td>` +
`<td>${escapeHtml(val1)}</td>` +
`<td>${escapeHtml(val2)}</td>` +
`<td>${escapeHtml(row.scale)}</td>` +
`<td>${escapeHtml(row.conv)}</td>` +
`</tr>`;
});
html += '</tbody></table>';
preview.innerHTML = html;
document.getElementById('csvResults').style.display = 'block';
}
function downloadCSV() {
if (convertedData.length === 0) {
alert('ไม่มีข้อมูลให้ดาวน์โหลด');
return;
}
const sourceCRS = document.getElementById('csvSourceCRS').value;
const targetCRS = document.getElementById('csvTargetCRS').value;
const format = document.getElementById('csvFormat').value.toLowerCase();
const delimiter = (convertedData[0] && convertedData[0].delimiter) ? convertedData[0].delimiter : ',';
const isSwap = (format === 'point_y_x' || format === 'point_n_e' || format === 'point_lat_lon');
const outLabels = getOutputFieldNamesByTargetCrs(targetCRS);
const labelMap = {
'lon_out': 'Lon',
'lat_out': 'Lat',
'e_out': 'E',
'n_out': 'N',
'x_out': 'X',
'y_out': 'Y'
};
let header1 = labelMap[outLabels.outX] || 'X';
let header2 = labelMap[outLabels.outY] || 'Y';
if (isSwap) {
const tmp = header1; header1 = header2; header2 = tmp;
}
const headers = ['Point', header1, header2, 'Scale Factor (k)', 'Convergence'];
let csvContent = headers.map(h => escapeCsvField(h, delimiter)).join(delimiter) + '\r\n';
convertedData.forEach(row => {
let val1 = row.outX;
let val2 = row.outY;
if (isSwap) {
const tmp = val1; val1 = val2; val2 = tmp;
}
const line = [
escapeCsvField(row.point, delimiter),
escapeCsvField(val1, delimiter),
escapeCsvField(val2, delimiter),
escapeCsvField(row.scale, delimiter),
escapeCsvField(row.conv, delimiter)
].join(delimiter);
csvContent += line + '\r\n';
});
const csvWithBom = '\ufeff' + csvContent;
const blob = new Blob([csvWithBom], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
const baseName = (csvParsed && csvParsed.fileName) ? csvParsed.fileName.replace(/\.csv$/i, '') : 'converted_coordinates';
const filename = `${baseName}_${sourceCRS.replace(':', '')}_to_${targetCRS.replace(':', '')}_${new Date().getTime()}.csv`;
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
}
function updateLabels() {
const sourceCRS = document.getElementById('sourceCRS').value;
const targetCRS = document.getElementById('targetCRS').value;
const isLatLonSource = (sourceCRS === 'EPSG:4326' || sourceCRS === 'EPSG:4240');
const singleLatLonFormat = document.getElementById('singleLatLonFormat').value;
if (isLatLonSource) {
document.getElementById('singleLatLonFormatDiv').style.display = 'block';
if (singleLatLonFormat === 'DMS') {
document.getElementById('inputX').style.display = 'none';
document.getElementById('inputY').style.display = 'none';
document.getElementById('inputX_dms_group').style.display = 'flex';
document.getElementById('inputY_dms_group').style.display = 'flex';
if(document.getElementById('labelInputX')) document.getElementById('labelInputX').innerText = 'Longitude (DMS)';
if(document.getElementById('labelInputY')) document.getElementById('labelInputY').innerText = 'Latitude (DMS)';
} else {
document.getElementById('inputX').style.display = 'block';
document.getElementById('inputY').style.display = 'block';
document.getElementById('inputX_dms_group').style.display = 'none';
document.getElementById('inputY_dms_group').style.display = 'none';
if(document.getElementById('labelInputX')) document.getElementById('labelInputX').innerText = 'Longitude (DD)';
if(document.getElementById('labelInputY')) document.getElementById('labelInputY').innerText = 'Latitude (DD)';
}
} else {
document.getElementById('singleLatLonFormatDiv').style.display = 'none';
document.getElementById('inputX').style.display = 'block';
document.getElementById('inputY').style.display = 'block';
document.getElementById('inputX_dms_group').style.display = 'none';
document.getElementById('inputY_dms_group').style.display = 'none';
const labelMap = {
'EPSG:3857': { x: 'X (Web Mercator)', y: 'Y (Web Mercator)' },
'default': { x: 'Easting (m)', y: 'Northing (m)' }
};
const txt = labelMap[sourceCRS] || labelMap['default'];
if(document.getElementById('labelInputX')) document.getElementById('labelInputX').innerText = txt.x;
if(document.getElementById('labelInputY')) document.getElementById('labelInputY').innerText = txt.y;
}
const getTargetLabel = (crs) => {
if (crs === 'EPSG:4326' || crs === 'EPSG:4240') return { x: 'Longitude (DMS)', y: 'Latitude (DMS)' };
if (crs === 'EPSG:3857') return { x: 'X (Web Mercator)', y: 'Y (Web Mercator)' };
return { x: 'Easting (m)', y: 'Northing (m)' };
};
const tgtTxt = getTargetLabel(targetCRS);
if(document.getElementById('labelOutputX')) document.getElementById('labelOutputX').innerText = tgtTxt.x;
if(document.getElementById('labelOutputY')) document.getElementById('labelOutputY').innerText = tgtTxt.y;
}
document.addEventListener('DOMContentLoaded', function() {
updateLabels();
document.getElementById('sourceCRS').addEventListener('change', updateLabels);
document.getElementById('targetCRS').addEventListener('change', updateLabels);
const singleFormatElement = document.getElementById('singleLatLonFormat');
if (singleFormatElement) {
singleFormatElement.addEventListener('change', updateLabels);
}
const csvSourceCRS = document.getElementById('csvSourceCRS');
if (csvSourceCRS) {
csvSourceCRS.addEventListener('change', function() {
const val = this.value;
const formatDiv = document.getElementById('csvLatLonFormatDiv');
if (formatDiv) {
if (val === 'EPSG:4326' || val === 'EPSG:4240') {
formatDiv.style.display = 'block';
} else {
formatDiv.style.display = 'none';
}
}
});
csvSourceCRS.dispatchEvent(new Event('change'));
}
document.getElementById('convertBtn').addEventListener('click', convertSinglePoint);
document.getElementById('csvFile').addEventListener('change', handleFileUpload);
document.getElementById('processCSVBtn').addEventListener('click', processCSV);
document.getElementById('downloadBtn').addEventListener('click', downloadCSV);
document.getElementById('inputX').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
convertSinglePoint();
}
});
document.getElementById('inputY').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
convertSinglePoint();
}
});
const dmsInputs = ['inputX_d', 'inputX_m', 'inputX_s', 'inputY_d', 'inputY_m', 'inputY_s'];
dmsInputs.forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
convertSinglePoint();
}
});
}
});
});
</script>
</body>
</html>