-->

Advertisement

ArcGIS - Chương trình tạo hệ tọa độ VN 2000 trong ArcGIS Pro

Chức năng công cụ

  1. Chọn tỉnh/thành phố từ danh sách có sẵn.
  2. Tự động xác định kinh tuyến trục tương ứng với tỉnh/thành phố đã chọn.
  3. Tạo hoặc sử dụng lại file hệ tọa độ VN-2000 / TM-3 định dạng .prj.
  4. Tự động sử dụng thư mục lưu PRJ mặc định:
    Documents\ArcGIS\Coordinate Systems
  5. Cho phép chọn nhiều File Geodatabase định dạng .gdb để xử lý hàng loạt.
  6. Tự động duyệt toàn bộ Feature Class trong mỗi Geodatabase, bao gồm:
    • Feature Class nằm trực tiếp tại thư mục gốc của GDB.
    • Feature Class nằm bên trong các Feature Dataset.
  7. Gán hệ tọa độ mới cho các Feature Class bằng công cụ Define Projection.
  8. Kiểm tra file PRJ trước khi tạo:
    • Nếu file PRJ chưa tồn tại: tự động tạo mới.
    • Nếu file PRJ đã tồn tại: không ghi đè.
    • Thông báo cho người dùng và sử dụng lại file PRJ hiện có.

Quy trình xử lý

Chọn tỉnh/thành phố → xác định kinh tuyến trục → kiểm tra/tạo PRJ VN-2000 TM-3 → chọn nhiều GDB → duyệt toàn bộ Feature Class → Define Projection → thông báo kết quả.

Lưu ý: Chức năng Define Projection chỉ gán thông tin hệ tọa độ cho dữ liệu, không thực hiện chuyển đổi tọa độ hình học. Công cụ phù hợp khi dữ liệu đã có tọa độ VN-2000 đúng nhưng đang thiếu hoặc bị sai thông tin Spatial Reference.


import arcpy import os import re import unicodedata # ============================================================ # BẢNG KINH TUYẾN TRỤC # ============================================================ PROVINCE_MERIDIANS = { "Lai Châu": (103, 0), "Điện Biên": (103, 0), "Sơn La": (104, 0), "Lào Cai": (104, 45), "Yên Bái": (104, 45), "Hà Giang": (105, 30), "Tuyên Quang": (106, 0), "Phú Thọ": (104, 45), "Vĩnh Phúc": (105, 0), "Cao Bằng": (105, 45), "Lạng Sơn": (107, 15), "Bắc Cạn": (106, 30), "Thái Nguyên": (106, 30), "Bắc Giang": (107, 0), "Bắc Ninh": (105, 30), "Quảng Ninh": (107, 45), "TP. Hải Phòng": (105, 45), "Hải Dương": (105, 30), "Hưng Yên": (105, 30), "TP. Hà Nội": (105, 0), "Hoà Bình": (106, 0), "Hà Nam": (105, 0), "Nam Định": (105, 30), "Thái Bình": (105, 30), "Ninh Bình": (105, 0), "Thanh Hoá": (105, 0), "Nghệ An": (104, 45), "Hà Tĩnh": (105, 30), "Quảng Bình": (106, 0), "Quảng Trị": (106, 15), "Thừa Thiên – Huế": (107, 0), "TP. Đà Nẵng": (107, 45), "Quảng Nam": (107, 45), "Quảng Ngãi": (108, 0), "Bình Định": (108, 15), "Kon Tum": (107, 30), "Gia Lai": (108, 30), "Đắk Lắk": (108, 30), "Đắc Nông": (108, 30), "Phú Yên": (108, 30), "Khánh Hoà": (108, 15), "Ninh Thuận": (108, 15), "Bình Thuận": (108, 30), "Lâm Đồng": (107, 45), "Bình Dương": (105, 45), "Bình Phước": (106, 15), "Đồng Nai": (107, 45), "Bà Rịa – Vũng Tàu": (107, 45), "Tây Ninh": (105, 30), "Long An": (105, 45), "Tiền Giang": (105, 45), "Bến Tre": (105, 45), "Đồng Tháp": (105, 0), "Vĩnh Long": (105, 30), "Trà Vinh": (105, 30), "An Giang": (104, 45), "Kiên Giang": (104, 30), "TP. Cần Thơ": (105, 0), "Hậu Giang": (105, 0), "Sóc Trăng": (105, 30), "Bạc Liêu": (105, 0), "Cà Mau": (104, 30), "TP. Hồ Chí Minh": (105, 45), } # ============================================================ # HÀM HỖ TRỢ # ============================================================ def meridian_decimal(degree, minute): return degree + minute / 60.0 def meridian_text(degree, minute): return "{}°{:02d}'".format(degree, minute) def safe_name(text): text = text.replace("Đ", "D").replace("đ", "d") text = unicodedata.normalize("NFD", text) text = "".join(c for c in text if unicodedata.category(c) != "Mn") text = re.sub(r"[^A-Za-z0-9]+", "_", text) return text.strip("_") def default_prj_folder(): """ Thư mục mặc định thân thiện với người dùng: Documents\ArcGIS\Coordinate Systems """ docs = os.path.join(os.path.expanduser("~"), "Documents") folder = os.path.join(docs, "ArcGIS", "Coordinate Systems") try: os.makedirs(folder, exist_ok=True) except Exception: # fallback nếu Documents bị giới hạn folder = arcpy.env.scratchFolder or os.path.expanduser("~") return folder def build_vn2000_tm3_wkt(degree, minute): cm = meridian_decimal(degree, minute) sr_name = "VN_2000_TM3_{}_{}".format(degree, str(minute).zfill(2)) wkt = ( 'PROJCS["{name}",' 'GEOGCS["GCS_VN_2000",' 'DATUM["D_Vietnam_2000",' 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' 'PRIMEM["Greenwich",0.0],' 'UNIT["Degree",0.0174532925199433]],' 'PROJECTION["Transverse_Mercator"],' 'PARAMETER["False_Easting",500000.0],' 'PARAMETER["False_Northing",0.0],' 'PARAMETER["Central_Meridian",{cm}],' 'PARAMETER["Scale_Factor",0.9999],' 'PARAMETER["Latitude_Of_Origin",0.0],' 'UNIT["Meter",1.0]]' ).format(name=sr_name, cm=cm) return wkt, sr_name def create_spatial_reference(province): degree, minute = PROVINCE_MERIDIANS[province] wkt, sr_name = build_vn2000_tm3_wkt(degree, minute) sr = arcpy.SpatialReference() sr.loadFromString(wkt) return sr, sr_name, degree, minute def parse_multivalue(value_as_text): """ GP MultiValue thường trả về chuỗi phân cách bằng dấu ; và có thể có dấu nháy. """ if not value_as_text: return [] values = [] for item in value_as_text.split(";"): item = item.strip().strip("'").strip('"') if item: values.append(item) return values def list_feature_classes_in_gdb(gdb): """ Trả về danh sách toàn bộ Feature Class trong GDB, bao gồm root và Feature Dataset. """ found = [] old_workspace = arcpy.env.workspace try: arcpy.env.workspace = gdb # Feature class ở root root_fcs = arcpy.ListFeatureClasses() or [] for fc in root_fcs: found.append(os.path.join(gdb, fc)) # Feature datasets datasets = arcpy.ListDatasets(feature_type="feature") or [] for ds in datasets: arcpy.env.workspace = os.path.join(gdb, ds) fcs = arcpy.ListFeatureClasses() or [] for fc in fcs: found.append(os.path.join(gdb, ds, fc)) arcpy.env.workspace = gdb finally: arcpy.env.workspace = old_workspace return found def ensure_prj(prj_path, spatial_ref): """ Nếu PRJ đã tồn tại: dùng lại. Nếu chưa có: tạo mới. """ if os.path.exists(prj_path): return False with open(prj_path, "w", encoding="utf-8") as f: f.write(spatial_ref.exportToString()) return True # ============================================================ # TOOLBOX # ============================================================ class Toolbox(object): def __init__(self): self.label = "VN-2000 Province Coordinate System" self.alias = "VN2000ProvinceCRS" self.tools = [ApplyVN2000ToMultipleGDB] # ============================================================ # TOOL # ============================================================ class ApplyVN2000ToMultipleGDB(object): def __init__(self): self.label = "Gán VN-2000 theo tỉnh cho nhiều GDB" self.description = ( "Chọn tỉnh/thành phố và nhiều File Geodatabase. " "Tool tạo hoặc dùng lại PRJ, sau đó Define Projection " "cho toàn bộ Feature Class." ) self.canRunInBackground = False def getParameterInfo(self): p0 = arcpy.Parameter( displayName="1. Tỉnh / Thành phố", name="province", datatype="GPString", parameterType="Required", direction="Input" ) p0.filter.type = "ValueList" p0.filter.list = list(PROVINCE_MERIDIANS.keys()) p1 = arcpy.Parameter( displayName="2. Kinh tuyến trục", name="central_meridian", datatype="GPString", parameterType="Derived", direction="Output" ) p2 = arcpy.Parameter( displayName="3. Chọn các File Geodatabase (.gdb)", name="input_gdbs", datatype="DEWorkspace", parameterType="Required", direction="Input", multiValue=True ) p2.filter.list = ["Local Database"] p3 = arcpy.Parameter( displayName="4. Thư mục lưu PRJ", name="output_folder", datatype="DEFolder", parameterType="Optional", direction="Input" ) p3.value = default_prj_folder() p4 = arcpy.Parameter( displayName="5. Ghi đè hệ tọa độ hiện có bằng Define Projection", name="overwrite_existing_sr", datatype="GPBoolean", parameterType="Optional", direction="Input" ) p4.value = True p5 = arcpy.Parameter( displayName="File PRJ sử dụng", name="output_prj", datatype="DEFile", parameterType="Derived", direction="Output" ) return [p0, p1, p2, p3, p4, p5] def isLicensed(self): return True def updateParameters(self, parameters): province = parameters[0].valueAsText if province and province in PROVINCE_MERIDIANS: degree, minute = PROVINCE_MERIDIANS[province] parameters[1].value = meridian_text(degree, minute) if not parameters[3].value: parameters[3].value = default_prj_folder() return def updateMessages(self, parameters): province = parameters[0].valueAsText if province and province not in PROVINCE_MERIDIANS: parameters[0].setErrorMessage( "Tỉnh/thành phố không có trong bảng kinh tuyến trục." ) gdbs = parse_multivalue(parameters[2].valueAsText) for gdb in gdbs: if not gdb.lower().endswith(".gdb"): parameters[2].setErrorMessage( "Chỉ chọn File Geodatabase (.gdb)." ) break return def execute(self, parameters, messages): province = parameters[0].valueAsText gdbs = parse_multivalue(parameters[2].valueAsText) output_folder = parameters[3].valueAsText or default_prj_folder() overwrite_existing_sr = bool(parameters[4].value) if not province: raise arcpy.ExecuteError("Chưa chọn tỉnh/thành phố.") if not gdbs: raise arcpy.ExecuteError("Chưa chọn File Geodatabase.") os.makedirs(output_folder, exist_ok=True) sr, sr_name, degree, minute = create_spatial_reference(province) prj_name = safe_name(sr_name) + ".prj" prj_path = os.path.join(output_folder, prj_name) created = ensure_prj(prj_path, sr) arcpy.AddMessage("") arcpy.AddMessage("==============================================") arcpy.AddMessage("HỆ TỌA ĐỘ") arcpy.AddMessage("==============================================") arcpy.AddMessage("Tỉnh / Thành phố : {}".format(province)) arcpy.AddMessage("Kinh tuyến trục : {}".format( meridian_text(degree, minute) )) arcpy.AddMessage("Central Meridian : {:.8f}°".format( meridian_decimal(degree, minute) )) arcpy.AddMessage("Scale Factor : 0.9999") arcpy.AddMessage("False Easting : 500000 m") arcpy.AddMessage("False Northing : 0 m") if created: arcpy.AddMessage("Đã tạo PRJ : {}".format(prj_path)) else: arcpy.AddWarning( "File PRJ đã tồn tại. Tool sẽ dùng lại file hiện có:" ) arcpy.AddWarning(prj_path) parameters[5].value = prj_path total_fc = 0 success_fc = 0 skipped_fc = 0 failed_fc = 0 for gdb in gdbs: arcpy.AddMessage("") arcpy.AddMessage("----------------------------------------------") arcpy.AddMessage("GDB: {}".format(gdb)) arcpy.AddMessage("----------------------------------------------") if not arcpy.Exists(gdb): arcpy.AddWarning("Không tồn tại, bỏ qua: {}".format(gdb)) continue fcs = list_feature_classes_in_gdb(gdb) if not fcs: arcpy.AddWarning("Không có Feature Class trong GDB.") continue arcpy.AddMessage("Tìm thấy {} Feature Class.".format(len(fcs))) for fc in fcs: total_fc += 1 try: desc = arcpy.Describe(fc) old_sr = getattr(desc, "spatialReference", None) if not overwrite_existing_sr: if old_sr and old_sr.name not in (None, "", "Unknown"): skipped_fc += 1 arcpy.AddMessage( "[SKIP] {} | SR hiện tại: {}".format( fc, old_sr.name ) ) continue arcpy.management.DefineProjection(fc, sr) success_fc += 1 arcpy.AddMessage("[OK] {}".format(fc)) except Exception as ex: failed_fc += 1 arcpy.AddWarning( "[LỖI] {} | {}".format(fc, str(ex)) ) arcpy.AddMessage("") arcpy.AddMessage("==============================================") arcpy.AddMessage("KẾT QUẢ") arcpy.AddMessage("==============================================") arcpy.AddMessage("Số GDB đã chọn : {}".format(len(gdbs))) arcpy.AddMessage("Tổng Feature Class : {}".format(total_fc)) arcpy.AddMessage("Gán thành công : {}".format(success_fc)) arcpy.AddMessage("Bỏ qua : {}".format(skipped_fc)) arcpy.AddMessage("Lỗi : {}".format(failed_fc)) arcpy.AddMessage("PRJ sử dụng : {}".format(prj_path)) arcpy.AddMessage("==============================================") if failed_fc > 0: arcpy.AddWarning( "Có {} Feature Class không gán được hệ tọa độ. " "Kiểm tra khóa dữ liệu hoặc quyền truy cập.".format(failed_fc) ) return def postExecute(self, parameters): return