# -*- coding: utf-8 -*-
"""LVT Map Layout - Dialog UI built programmatically with PyQt5."""

from datetime import date

from qgis.PyQt.QtCore import Qt, QSize, QTimer
from qgis.PyQt.QtWidgets import (
    QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
    QLabel, QLineEdit, QComboBox, QSpinBox,
    QCheckBox, QRadioButton, QButtonGroup, QTextEdit,
    QPushButton, QMessageBox, QGridLayout,
    QScrollArea, QDoubleSpinBox
)

from .engine import LvtEngine
from ..shared.i18n import tr
from .._crs_list import CRS_LIST as _MASTER_CRS_LIST
from qgis.core import QgsProject
from ..shared.qt_compat import QtCompat, MsgButton


# Paper sizes in mm (width x height for landscape)
PAPER_SIZES = {
    # A-series (ISO 216)
    "A5 (210 × 148 mm)": (210, 148),
    "A4 (297 × 210 mm)": (297, 210),
    "A3 (420 × 297 mm)": (420, 297),
    "A2 (594 × 420 mm)": (594, 420),
    "A1 (841 × 594 mm)": (841, 594),
    "A0 (1189 × 841 mm)": (1189, 841),
    # B-series (ISO 216)
    "B5 (250 × 176 mm)": (250, 176),
    "B4 (353 × 250 mm)": (353, 250),
    "B3 (500 × 353 mm)": (500, 353),
    "B2 (707 × 500 mm)": (707, 500),
    "B1 (1000 × 707 mm)": (1000, 707),
    "B0 (1414 × 1000 mm)": (1414, 1000),
}

# Layout modes
MODE_SLIDE = "slide"
MODE_PRINT = "print"

# Standard scales for suggestion
STANDARD_SCALES = [
    500, 1000, 2000, 2500, 5000, 10000, 15000,
    20000, 25000, 50000, 100000, 250000, 500000, 1000000,
]

# Margins around the Map Frame (mm) to calculate total page size.
# Format: (left, top, right, bottom)
# These match the template frame positions.
MARGINS = {
    "slide": (5, 5, 5, 5),
    "print_small": (19.15, 32.65, 5, 30.9),   # A5, A4
    "print_medium": (21.85, 34.85, 5, 30.9),  # A3
    "print_large": (25.85, 59.85, 5, 30.9),   # A2, A1
    "print_xlarge": (35.85, 99.85, 5, 30.9),  # A0+
}


class LvtDialog(QDialog):
    """Main dialog for LVT Map Layout plugin."""

    # CRS list imported from centralized _crs_list.py
    CRS_LIST = _MASTER_CRS_LIST

    def __init__(self, iface, plugin_dir, parent=None):
        super().__init__(parent or iface.mainWindow())
        self.iface = iface
        self.plugin_dir = plugin_dir
        self.engine = LvtEngine(iface, plugin_dir)
        self.setWindowTitle("LVT Map Layout")
        self.setMinimumSize(QSize(560, 500))
        # Resize to 90% of screen height
        try:
            from qgis.PyQt.QtWidgets import QApplication
            screen = QApplication.primaryScreen()
            if screen:
                sh = int(screen.availableGeometry().height() * 0.9)
                self.resize(680, sh)
        except Exception:
            self.resize(680, 800)
        self._build_ui()

    # ── UI Construction ──────────────────────────────────────────

    def _build_ui(self):
        root = QVBoxLayout(self)
        self._tabs = QTabWidget()
        self._tabs.addTab(self._tab_general(), tr("General"))
        self._tabs.addTab(self._tab_map(), tr("Map Settings"))
        self._tabs.addTab(self._tab_content(), tr("Legend/Content"))
        root.addWidget(self._tabs)

        # Buttons
        btn_row = QHBoxLayout()

        btn_help = QPushButton(f"❓ {tr('Help')}")
        btn_help.setStyleSheet(
            "QPushButton{background:#ff8f00;color:#fff;font-weight:bold;"
            "padding:6px 14px;border-radius:4px}"
            "QPushButton:hover{background:#ffa000}"
        )
        btn_help.clicked.connect(self._show_help)

        self.btn_create = QPushButton(f"🗺️ {tr('Create Layout')}")
        self.btn_create.setStyleSheet(
            "QPushButton{background:#2e7d32;color:#fff;font-weight:bold;"
            "padding:6px 20px;border-radius:4px}"
            "QPushButton:hover{background:#388e3c}"
        )
        self.btn_create.clicked.connect(self._on_create)

        btn_close = QPushButton(tr('Close'))
        btn_close.clicked.connect(self.close)

        btn_row.addWidget(btn_help)

        btn_crs = QPushButton("🌐 CRS")
        btn_crs.setStyleSheet(
            "QPushButton{background:#1565c0;color:#fff;font-weight:bold;"
            "padding:6px 14px;border-radius:4px}"
            "QPushButton:hover{background:#1976d2}"
        )
        btn_crs.clicked.connect(self._show_crs)
        btn_row.addWidget(btn_crs)

        # ── Preset toolbar ──
        preset_row = QHBoxLayout()
        preset_row.addWidget(QLabel(f"\U0001f4be {tr('Preset:')}"))
        self.cmb_presets = QComboBox()
        self.cmb_presets.setMinimumWidth(180)
        self._load_preset_list()
        preset_row.addWidget(self.cmb_presets)
        btn_load = QPushButton(f"\U0001f4c2 {tr('Load')}")
        btn_load.setMaximumWidth(80)
        btn_load.clicked.connect(self._load_preset)
        preset_row.addWidget(btn_load)
        btn_save = QPushButton(f"\U0001f4be {tr('Save')}")
        btn_save.setMaximumWidth(80)
        btn_save.clicked.connect(self._save_preset)
        preset_row.addWidget(btn_save)
        btn_del_preset = QPushButton("\U0001f5d1")
        btn_del_preset.setMaximumWidth(30)
        btn_del_preset.setToolTip(tr("Delete selected preset"))
        btn_del_preset.clicked.connect(self._delete_preset)
        preset_row.addWidget(btn_del_preset)
        btn_reset = QPushButton(f"\u21ba {tr('Reset')}")
        btn_reset.setMaximumWidth(80)
        btn_reset.setToolTip(tr("Reset all settings to defaults"))
        btn_reset.clicked.connect(self._reset_defaults)
        preset_row.addWidget(btn_reset)
        preset_row.addStretch()
        root.addLayout(preset_row)

        btn_row.addStretch()
        btn_row.addWidget(btn_close)
        btn_row.addWidget(self.btn_create)
        root.addLayout(btn_row)

    # ── Tab: General ─────────────────────────────────────────────

    def _tab_general(self):
        w = QWidget()
        g = QGridLayout(w)
        row = 0

        # Layout mode selector
        g.addWidget(QLabel(f"{tr('Layout Mode')}:"), row, 0)
        mode_box = QHBoxLayout()
        self.rad_print = QRadioButton("Print (full frame)")
        self.rad_slide = QRadioButton("Slide (simple)")
        self.rad_print.setChecked(True)
        bg_mode = QButtonGroup(w)
        bg_mode.addButton(self.rad_slide)
        bg_mode.addButton(self.rad_print)
        self.rad_slide.toggled.connect(self._toggle_mode_fields)
        mode_box.addWidget(self.rad_print)
        mode_box.addWidget(self.rad_slide)
        g.addLayout(mode_box, row, 1)
        row += 1

        # Language selector — default follows current app language setting
        g.addWidget(QLabel(f"{tr('Language')}:"), row, 0)
        self.cmb_lang = QComboBox()
        self.cmb_lang.addItems(["English (EN)", "Tiếng Việt (VN)"])
        # Match app language: vi -> VN (index 1), en -> EN (index 0)
        from ..shared.i18n import current_language
        _default_lang_idx = 1 if current_language() == 'vi' else 0
        self.cmb_lang.setCurrentIndex(_default_lang_idx)
        g.addWidget(self.cmb_lang, row, 1)
        row += 1

        # ── Print-only fields ─────────────────────────────────────
        self.lbl_title = QLabel(f"{tr('Map Title')}:")
        g.addWidget(self.lbl_title, row, 0)
        self.txt_title = QLineEdit()
        self.txt_title.setPlaceholderText("e.g. Land Use Map 2026")
        g.addWidget(self.txt_title, row, 1)
        row += 1

        self.lbl_org = QLabel(f"{tr('Organization')}:")
        g.addWidget(self.lbl_org, row, 0)
        self.txt_org = QLineEdit()
        self.txt_org.setPlaceholderText("e.g. Tan Cao Nguyên JSC")
        g.addWidget(self.txt_org, row, 1)
        row += 1

        self.lbl_study = QLabel(f"{tr('Study Area')}:")
        g.addWidget(self.lbl_study, row, 0)
        self.txt_study = QLineEdit()
        self.txt_study.setPlaceholderText("e.g. Gia Lai Province")
        g.addWidget(self.txt_study, row, 1)
        row += 1

        # References / Viện dẫn: fixed in template, not editable

        # Store print-only widgets for toggle
        self._print_widgets = [
            self.lbl_title, self.txt_title,
            self.lbl_org, self.txt_org,
            self.lbl_study, self.txt_study,
        ]
        for wgt in self._print_widgets:
            wgt.setVisible(True)  # visible by default (Print mode)

        # ── Common fields ─────────────────────────────────────────
        g.addWidget(QLabel(f"{tr('Author')}:"), row, 0)
        self.txt_author = QLineEdit()
        self.txt_author.setPlaceholderText(
            "e.g. Tác giả bản đồ / Author of Map")
        g.addWidget(self.txt_author, row, 1)
        row += 1

        g.addWidget(QLabel(f"{tr('Date')}:"), row, 0)
        self.txt_date = QLineEdit(date.today().strftime("%d/%m/%Y"))
        g.addWidget(self.txt_date, row, 1)
        row += 1

        # ── Project CRS ──────────────────────────────────────────
        from qgis.core import QgsProject, QgsCoordinateReferenceSystem  # noqa: F811
        from qgis.PyQt.QtWidgets import QMessageBox

        cur_crs = QgsProject.instance().crs()
        self._crs_label = QLabel(
            f"📍 <b>{cur_crs.authid()}</b> — {cur_crs.description()}"
        )
        self._crs_label.setStyleSheet(
            "padding:4px 8px;border-radius:3px;"
            "font-size:11px;font-weight:bold;border:1px solid #999"
        )
        self._crs_label.setWordWrap(True)
        self._crs_label.setTextFormat(QtCompat.RichText)
        g.addWidget(QLabel("🌐 Project CRS:"), row, 0)
        g.addWidget(self._crs_label, row, 1)
        row += 1

        crs_row = QHBoxLayout()
        self.cmb_crs = QComboBox()
        self.cmb_crs.setMinimumWidth(280)
        for label, _ in self.CRS_LIST:
            self.cmb_crs.addItem(label)
        for i, (_, code) in enumerate(self.CRS_LIST):
            if not code:
                self.cmb_crs.model().item(i).setEnabled(False)
        # Pre-select
        for i, (_, code) in enumerate(self.CRS_LIST):
            if code == cur_crs.authid():
                self.cmb_crs.setCurrentIndex(i)
                break

        btn_apply_crs = QPushButton("✅ Apply")
        btn_apply_crs.setStyleSheet(
            "QPushButton{background:#2e7d32;color:#fff;font-weight:bold;"
            "padding:4px 12px;border-radius:3px}"
            "QPushButton:hover{background:#388e3c}"
        )

        def _apply_project_crs():
            idx = self.cmb_crs.currentIndex()
            code = self.CRS_LIST[idx][1]
            if not code:
                return
            new_crs = QgsCoordinateReferenceSystem(code)
            if not new_crs.isValid():
                QMessageBox.warning(self, "CRS Error", f"Invalid CRS: {code}")
                return
            QgsProject.instance().setCrs(new_crs)
            self._crs_label.setText(
                f"📍 <b>{new_crs.authid()}</b> — {new_crs.description()}"
            )

        btn_apply_crs.clicked.connect(_apply_project_crs)
        crs_row.addWidget(self.cmb_crs)
        crs_row.addWidget(btn_apply_crs)
        g.addWidget(QLabel("⚙️ Set CRS:"), row, 0)
        g.addLayout(crs_row, row, 1)
        row += 1

        # ── Spacer to push quote toward center ────────────────────
        g.setRowStretch(row, 2)
        row += 1

        # ── Inspirational quote ───────────────────────────────────
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        quote_vi = ('\U0001f3a8 "Hãy biến bản đồ của bạn thành một tác phẩm '
                    '— đẹp như tranh vẽ, chính xác đến từng chi tiết!"')
        quote_en = ('\U0001f3a8 "Turn your map into a masterpiece '
                    '— as beautiful as a painting, as precise as science!"')
        lbl_quote = QLabel(quote_vi if _vi else quote_en)
        lbl_quote.setWordWrap(True)
        lbl_quote.setAlignment(QtCompat.AlignCenter)
        lbl_quote.setStyleSheet(
            "color:#4E342E;font-size:15px;font-style:italic;"
            "font-weight:bold;padding:24px 20px;"
            "background:qlineargradient(x1:0,y1:0,x2:1,y2:1,"
            "stop:0 #FFF8E1,stop:0.5 #FFF3E0,stop:1 #F1F8E9);"
            "border-radius:10px;border:2px solid #BCAAA4;"
        )
        g.addWidget(lbl_quote, row, 0, 1, 2)
        row += 1

        # ── Spacer between quote and arrow ────────────────────────
        g.setRowStretch(row, 1)
        row += 1

        # ── Step arrow → Map Settings ─────────────────────────────
        arrow_vi = "▶▶  Tiếp: Cài đặt bản đồ  ▶▶"
        arrow_en = "▶▶  Next: Map Settings  ▶▶"
        self._arrow1 = QPushButton(arrow_vi if _vi else arrow_en)
        self._arrow1.setCursor(QtCompat.PointingHandCursor)
        self._arrow1.setStyleSheet(
            "QPushButton{background:#E8F5E9;color:#2e7d32;"
            "font-weight:bold;font-size:13px;padding:10px;"
            "border:2px solid #A5D6A7;border-radius:6px;}"
            "QPushButton:hover{background:#C8E6C9;border-color:#66BB6A;}"
        )
        self._arrow1.clicked.connect(lambda: self._tabs.setCurrentIndex(1))
        g.addWidget(self._arrow1, row, 0, 1, 2)
        row += 1
        self._arrow1_visible = True
        self._arrow_timer1 = QTimer(self)
        self._arrow_timer1.timeout.connect(self._pulse_arrow1)
        self._arrow_timer1.start(800)

        g.setRowStretch(row, 2)
        return w

    def _pulse_arrow1(self):
        """Toggle arrow opacity for pulse effect."""
        self._arrow1_visible = not self._arrow1_visible
        if self._arrow1_visible:
            self._arrow1.setStyleSheet(
                "QPushButton{background:#E8F5E9;color:#2e7d32;"
                "font-weight:bold;font-size:13px;padding:10px;"
                "border:2px solid #A5D6A7;border-radius:6px;}"
                "QPushButton:hover{background:#C8E6C9;border-color:#66BB6A;}"
            )
        else:
            self._arrow1.setStyleSheet(
                "QPushButton{background:#C8E6C9;color:#1B5E20;"
                "font-weight:bold;font-size:13px;padding:10px;"
                "border:2px solid #66BB6A;border-radius:6px;}"
                "QPushButton:hover{background:#C8E6C9;border-color:#66BB6A;}"
            )

    def _toggle_mode_fields(self, checked):
        """Show/hide print-only fields based on mode selection."""
        is_print = self.rad_print.isChecked()
        for wgt in self._print_widgets:
            wgt.setVisible(is_print)
        # Signature block only in Print mode
        if hasattr(self, '_sig_print_widgets'):
            for wgt in self._sig_print_widgets:
                wgt.setVisible(is_print)
            if not is_print:
                self._sig_container.setVisible(False)
                self.chk_signature.setChecked(False)
        # Legend block: available in BOTH Print and Slide modes
        # (no longer hidden when switching to Slide)

    def _toggle_signature(self, state):
        """Show/hide signature block inputs."""
        self._sig_container.setVisible(state == QtCompat.Checked)

    def _toggle_legend(self, state):
        """Show/hide legend block inputs."""
        self._leg_container.setVisible(state == QtCompat.Checked)

    def _refresh_legend_layers(self):
        """Build checkbox + rename + icon symbol picker for legend layers."""
        # Clear existing
        for chk in self._leg_layer_checks.values():
            chk.setParent(None)
            chk.deleteLater()
        self._leg_layer_checks = {}
        for cmb in self._leg_symbol_combos.values():
            cmb.setParent(None)
            cmb.deleteLater()
        self._leg_symbol_combos = {}
        for le in self._leg_layer_renames.values():
            le.setParent(None)
            le.deleteLater()
        self._leg_layer_renames = {}
        self._leg_layer_rows = {}
        while self._leg_layer_layout.count():
            item = self._leg_layer_layout.takeAt(0)
            w = item.widget()
            if w:
                w.deleteLater()
        # Reset search box
        if hasattr(self, '_leg_search') and self._leg_search:
            self._leg_search.clear()

        # Build symbol icon lists from Style Manager
        from qgis.core import (
            QgsStyle, QgsSymbol, QgsWkbTypes,
            QgsSymbolLayerUtils)
        from qgis.PyQt.QtCore import QSize

        ICON_SIZE = QSize(24, 16)
        style = QgsStyle.defaultStyle()

        # Cache: {type: [(name, QIcon), ...]}
        sym_cache = {
            QgsSymbol.SymbolType.Marker: [],
            QgsSymbol.SymbolType.Line: [],
            QgsSymbol.SymbolType.Fill: [],
        }
        try:
            for sn in sorted(style.symbolNames()):
                sym = style.symbol(sn)
                if not sym or sym.type() not in sym_cache:
                    continue
                icon = QgsSymbolLayerUtils.symbolPreviewIcon(sym, ICON_SIZE)
                sym_cache[sym.type()].append((sn, icon))
        except Exception:  # nosec B110
            pass  # nosec B110

        TYPE_MAP = {
            QgsWkbTypes.GeometryType.PointGeometry: QgsSymbol.SymbolType.Marker,
            QgsWkbTypes.GeometryType.LineGeometry: QgsSymbol.SymbolType.Line,
            QgsWkbTypes.GeometryType.PolygonGeometry: QgsSymbol.SymbolType.Fill,
        }

        try:
            for lid, layer in QgsProject.instance().mapLayers().items():
                name = layer.name()
                row_w = QWidget()
                row_ly = QHBoxLayout(row_w)
                row_ly.setContentsMargins(0, 2, 0, 2)

                # Checkbox
                geom_label = ""
                sym_type = None
                if hasattr(layer, 'geometryType'):
                    gt = layer.geometryType()
                    sym_type = TYPE_MAP.get(gt)
                    if gt == QgsWkbTypes.GeometryType.PointGeometry:
                        geom_label = " 🔵"
                    elif gt == QgsWkbTypes.GeometryType.LineGeometry:
                        geom_label = " 📏"
                    elif gt == QgsWkbTypes.GeometryType.PolygonGeometry:
                        geom_label = " ⬛"

                chk = QCheckBox(f"{name}{geom_label}")
                chk.setChecked(True)
                chk.setMinimumWidth(140)
                row_ly.addWidget(chk)

                # Rename field
                le = QLineEdit()
                le.setPlaceholderText(name)
                le.setToolTip(tr("Legend display name (blank = original)"))
                le.setMaximumWidth(120)
                le.setMinimumWidth(80)
                row_ly.addWidget(le)

                # Symbol picker combobox with icon previews
                cmb = QComboBox()
                cmb.setIconSize(ICON_SIZE)
                cmb.setMinimumWidth(140)
                cmb.setMaximumWidth(180)

                # Default: show layer's current symbol as first item
                current_icon = None
                try:
                    if hasattr(layer, 'renderer') and layer.renderer():
                        renderer = layer.renderer()
                        if hasattr(renderer, 'symbol') and renderer.symbol():
                            current_icon = QgsSymbolLayerUtils.symbolPreviewIcon(
                                renderer.symbol(), ICON_SIZE)
                except Exception:  # nosec B110
                    pass  # nosec B110

                if current_icon:
                    cmb.addItem(current_icon, name[:18], "")
                else:
                    cmb.addItem("-- Layer --", "")

                # Add all matching symbols with preview icons
                if sym_type is not None and sym_type in sym_cache:
                    for sn, icon in sym_cache[sym_type]:
                        cmb.addItem(icon, sn, sn)

                row_ly.addWidget(cmb)

                self._leg_layer_layout.addWidget(row_w)
                self._leg_layer_checks[lid] = chk
                self._leg_layer_renames[lid] = le
                self._leg_symbol_combos[lid] = cmb
                self._leg_layer_rows[lid] = row_w
        except Exception:  # nosec B110
            pass  # nosec B110

    def _leg_select_all(self):
        """Check all legend layer checkboxes."""
        for chk in self._leg_layer_checks.values():
            chk.setChecked(True)

    def _leg_deselect_all(self):
        """Uncheck all legend layer checkboxes."""
        for chk in self._leg_layer_checks.values():
            chk.setChecked(False)

    def _leg_search_filter(self, text):
        """Filter layer list rows by search text."""
        text = text.strip().lower()
        for lid, row_w in self._leg_layer_rows.items():
            if not text:
                row_w.setVisible(True)
            else:
                chk = self._leg_layer_checks.get(lid)
                layer_name = chk.text() if chk else ""
                row_w.setVisible(text in layer_name.lower())

    def _rebuild_sig_blocks(self, count):
        """Rebuild block input rows when count changes."""
        # Clear existing
        for widgets in self._sig_block_inputs:
            for w in widgets.values():
                w.setParent(None)
                w.deleteLater()
        self._sig_block_inputs = []
        # Remove old rows from layout
        while self._sig_blocks_layout.count():
            item = self._sig_blocks_layout.takeAt(0)
            if item.widget():
                item.widget().deleteLater()
            elif item.layout():
                while item.layout().count():
                    sub = item.layout().takeAt(0)
                    if sub.widget():
                        sub.widget().deleteLater()

        today = date.today()
        date_default = (
            f"Ngày {today.day} tháng {today.month} năm {today.year}")

        for i in range(count):
            lbl = QLabel(f"Block {i + 1}:")
            lbl.setStyleSheet("font-weight:bold;margin-top:4px")
            self._sig_blocks_layout.addWidget(lbl)
            grid = QGridLayout()
            t = QLineEdit(f"ĐẠI DIỆN BÊN {chr(65 + i)}")
            d = QLineEdit(date_default)
            n = QLineEdit()
            n.setPlaceholderText("Nhập họ tên...")
            grid.addWidget(QLabel("  Tiêu đề:"), 0, 0)
            grid.addWidget(t, 0, 1)
            grid.addWidget(QLabel("  Ngày:"), 1, 0)
            grid.addWidget(d, 1, 1)
            grid.addWidget(QLabel("  Họ tên:"), 2, 0)
            grid.addWidget(n, 2, 1)
            self._sig_blocks_layout.addLayout(grid)
            self._sig_block_inputs.append(
                {"label": lbl, "title": t, "date": d, "name": n})

    # ── Tab: Map Settings ────────────────────────────────────────

    def _tab_map(self):
        w = QWidget()
        g = QGridLayout(w)
        row = 0

        # Hidden combobox for engine compatibility
        self.cmb_paper = QComboBox()
        self.cmb_paper.addItems(
            list(PAPER_SIZES.keys()) + ["Custom / Tự chọn"])
        self.cmb_paper.setCurrentIndex(1)  # A4 default
        self.cmb_paper.setVisible(False)
        g.addWidget(self.cmb_paper, row, 1)

        # ── Map Frame Method selector ─────────────────────────────
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        method_lbl = QLabel(
            "📐 Chọn phương thức khung bản đồ:" if _vi
            else "📐 Map Frame Method:")
        method_lbl.setStyleSheet(
            "font-weight:bold;font-size:12px;color:#1B5E20;"
            "padding:4px 0;margin-top:4px;")
        g.addWidget(method_lbl, row, 0, 1, 2)
        row += 1

        # Radio: Predefined
        self.rad_predefined = QRadioButton(
            "📄 Khổ giấy định sẵn / Predefined Size" if _vi
            else "📄 Predefined Paper Size")
        self.rad_predefined.setChecked(True)
        self.rad_predefined.setStyleSheet(
            "font-weight:bold;font-size:11px;padding:4px 0;")
        g.addWidget(self.rad_predefined, row, 0, 1, 2)
        row += 1

        # Predefined panel
        self._predefined_panel = QWidget()
        pred_ly = QGridLayout(self._predefined_panel)
        pred_ly.setContentsMargins(24, 4, 0, 8)
        pred_ly.addWidget(QLabel(f"{tr('Paper Size')}:"), 0, 0)
        self.cmb_paper_visible = QComboBox()
        self.cmb_paper_visible.addItems(list(PAPER_SIZES.keys()))
        self.cmb_paper_visible.setCurrentIndex(1)  # A4
        self.cmb_paper_visible.currentIndexChanged.connect(
            self._on_predefined_changed)
        pred_ly.addWidget(self.cmb_paper_visible, 0, 1)
        g.addWidget(self._predefined_panel, row, 0, 1, 2)
        row += 1

        # Radio: Draw
        self.rad_draw_mode = QRadioButton(
            "🖊 Vẽ tự do / Draw Map Frame" if _vi
            else "🖊 Draw Map Frame (free)")
        self.rad_draw_mode.setStyleSheet(
            "font-weight:bold;font-size:11px;padding:4px 0;")
        bg_map_method = QButtonGroup(w)
        bg_map_method.addButton(self.rad_predefined)
        bg_map_method.addButton(self.rad_draw_mode)
        self.rad_predefined.toggled.connect(self._toggle_map_method)
        g.addWidget(self.rad_draw_mode, row, 0, 1, 2)
        row += 1

        # Draw panel
        self._draw_panel = QWidget()
        draw_ly = QVBoxLayout(self._draw_panel)
        draw_ly.setContentsMargins(24, 4, 0, 8)

        self.btn_draw = QPushButton(
            "🖊 Draw Map Frame / Vẽ khung bản đồ")
        self.btn_draw.setStyleSheet(
            "QPushButton{background:#1565c0;color:#fff;font-weight:bold;"
            "padding:8px 16px;border-radius:4px;font-size:13px}"
            "QPushButton:hover{background:#1976d2}"
        )
        self.btn_draw.clicked.connect(self._on_draw_extent)
        draw_ly.addWidget(self.btn_draw)

        custom_box = QHBoxLayout()
        self.spn_custom_w = QSpinBox()
        self.spn_custom_w.setRange(50, 3000)
        self.spn_custom_w.setValue(297)
        self.spn_custom_w.setSuffix(" mm")
        self.spn_custom_w.setPrefix("Map W: ")
        self.spn_custom_h = QSpinBox()
        self.spn_custom_h.setRange(50, 3000)
        self.spn_custom_h.setValue(210)
        self.spn_custom_h.setSuffix(" mm")
        self.spn_custom_h.setPrefix("Map H: ")
        custom_box.addWidget(self.spn_custom_w)
        custom_box.addWidget(QLabel("×"))
        custom_box.addWidget(self.spn_custom_h)
        draw_ly.addLayout(custom_box)

        self.lbl_drawn = QLabel("")
        self.lbl_drawn.setStyleSheet("color:#888;font-style:italic")
        self.lbl_drawn.setWordWrap(True)
        draw_ly.addWidget(self.lbl_drawn)

        self._draw_panel.setVisible(False)  # hidden by default
        g.addWidget(self._draw_panel, row, 0, 1, 2)
        row += 1

        # Orientation (hidden when Custom + Draw is used)
        self.lbl_orient = QLabel(f"{tr('Orientation')}:")
        g.addWidget(self.lbl_orient, row, 0)
        orient_box = QHBoxLayout()
        self.rad_landscape = QRadioButton("Landscape / Ngang")
        self.rad_portrait = QRadioButton("Portrait / Dọc")
        self.rad_landscape.setChecked(True)
        bg = QButtonGroup(w)
        bg.addButton(self.rad_landscape)
        bg.addButton(self.rad_portrait)
        orient_box.addWidget(self.rad_landscape)
        orient_box.addWidget(self.rad_portrait)
        self.orient_widget = QWidget()
        self.orient_widget.setLayout(orient_box)
        g.addWidget(self.orient_widget, row, 1)
        row += 1

        # Scale
        g.addWidget(QLabel(f"{tr('Scale')}:"), row, 0)
        scale_box = QHBoxLayout()
        self.chk_auto_scale = QCheckBox("Auto fit / Tự động")
        self.chk_auto_scale.setChecked(True)
        self.chk_auto_scale.stateChanged.connect(self._toggle_scale)
        self.spn_scale = QSpinBox()
        self.spn_scale.setRange(100, 10000000)
        self.spn_scale.setValue(10000)
        self.spn_scale.setPrefix("1 : ")
        self.spn_scale.setEnabled(False)
        self.spn_scale.valueChanged.connect(self._on_scale_changed)
        scale_box.addWidget(self.chk_auto_scale)
        scale_box.addWidget(self.spn_scale)
        g.addLayout(scale_box, row, 1)
        row += 1

        # Scale suggestion label
        self.lbl_scale_hint = QLabel("")
        self.lbl_scale_hint.setWordWrap(True)
        self.lbl_scale_hint.setStyleSheet(
            "color:#1565c0;font-style:italic;padding:2px 0"
        )
        g.addWidget(self.lbl_scale_hint, row, 0, 1, 2)
        row += 1
        # ── Signature Block (Print mode only) ─────────────────────
        self.chk_signature = QCheckBox(
            "✍ Thêm ô ký / Add Signature Block")
        self.chk_signature.setStyleSheet(
            "font-weight:bold;font-size:12px;margin-top:8px")
        self.chk_signature.stateChanged.connect(self._toggle_signature)
        g.addWidget(self.chk_signature, row, 0, 1, 2)
        row += 1

        self._sig_container = QWidget()
        sig_ly = QVBoxLayout(self._sig_container)
        sig_ly.setContentsMargins(10, 4, 0, 0)

        # ── Size & Count ──
        size_row = QHBoxLayout()
        size_row.addWidget(QLabel("Số lượng:"))
        self.spn_sig_count = QSpinBox()
        self.spn_sig_count.setRange(1, 8)
        self.spn_sig_count.setValue(2)
        self.spn_sig_count.valueChanged.connect(self._rebuild_sig_blocks)
        size_row.addWidget(self.spn_sig_count)
        size_row.addWidget(QLabel("Cao:"))
        self.spn_sig_h = QSpinBox()
        self.spn_sig_h.setRange(30, 200)
        self.spn_sig_h.setValue(50)
        self.spn_sig_h.setSuffix("mm")
        size_row.addWidget(self.spn_sig_h)
        size_row.addWidget(QLabel("Rộng:"))
        self.spn_sig_w = QSpinBox()
        self.spn_sig_w.setRange(30, 200)
        self.spn_sig_w.setValue(50)
        self.spn_sig_w.setSuffix("mm")
        size_row.addWidget(self.spn_sig_w)
        size_row.addStretch()
        sig_ly.addLayout(size_row)

        # ── Position offsets ──
        pos_row = QHBoxLayout()
        pos_row.addWidget(QLabel("Cách trái / Left:"))
        self.spn_sig_left = QSpinBox()
        self.spn_sig_left.setRange(0, 100)
        self.spn_sig_left.setValue(3)
        self.spn_sig_left.setSuffix("mm")
        pos_row.addWidget(self.spn_sig_left)
        pos_row.addWidget(QLabel("Cách dưới / Bottom:"))
        self.spn_sig_bottom = QSpinBox()
        self.spn_sig_bottom.setRange(0, 100)
        self.spn_sig_bottom.setValue(3)
        self.spn_sig_bottom.setSuffix("mm")
        pos_row.addWidget(self.spn_sig_bottom)
        pos_row.addStretch()
        sig_ly.addLayout(pos_row)

        # ── Per-component font controls ──
        def _font_row(label, def_size, def_bold, def_italic):
            row_ly = QHBoxLayout()
            lbl = QLabel(f"  {label}:")
            lbl.setMinimumWidth(60)
            row_ly.addWidget(lbl)
            cmb = QComboBox()
            cmb.addItems(["Times New Roman", "Arial"])
            cmb.setMinimumWidth(120)
            row_ly.addWidget(cmb)
            spn = QSpinBox()
            spn.setRange(6, 36)
            spn.setValue(def_size)
            spn.setSuffix("pt")
            row_ly.addWidget(spn)
            chk_b = QCheckBox("B")
            chk_b.setChecked(def_bold)
            chk_b.setStyleSheet("font-weight:bold")
            row_ly.addWidget(chk_b)
            chk_i = QCheckBox("I")
            chk_i.setChecked(def_italic)
            chk_i.setStyleSheet("font-style:italic")
            row_ly.addWidget(chk_i)
            row_ly.addStretch()
            return row_ly, cmb, spn, chk_b, chk_i

        sig_ly.addWidget(QLabel("── Kiểu chữ / Font Style ──"))
        r1, self._sf1, self._ss1, self._sb1, self._si1 = _font_row(
            "Tiêu đề", 12, True, False)
        sig_ly.addLayout(r1)
        r2, self._sf2, self._ss2, self._sb2, self._si2 = _font_row(
            "Ngày", 10, False, True)
        sig_ly.addLayout(r2)
        r3, self._sf3, self._ss3, self._sb3, self._si3 = _font_row(
            "Họ tên", 11, True, False)
        sig_ly.addLayout(r3)

        # ── Dynamic block content area ──
        sig_ly.addWidget(QLabel("── Nội dung / Content ──"))
        self._sig_blocks_widget = QWidget()
        self._sig_blocks_layout = QVBoxLayout(self._sig_blocks_widget)
        self._sig_blocks_layout.setContentsMargins(0, 0, 0, 0)
        sig_ly.addWidget(self._sig_blocks_widget)
        self._sig_block_inputs = []
        self._rebuild_sig_blocks(2)  # default 2 blocks

        self._sig_container.setVisible(False)
        g.addWidget(self._sig_container, row, 0, 1, 2)
        row += 1

        # Store for Print mode toggle
        self._sig_print_widgets = [self.chk_signature]

        # ── Step arrow → Legend/Content ────────────────────────────
        from ..shared.i18n import current_language
        _vi2 = current_language() == 'vi'
        arrow2_vi = "▶▶  Tiếp: Chú dẫn / Nội dung  ▶▶"
        arrow2_en = "▶▶  Next: Legend/Content  ▶▶"
        self._arrow2 = QPushButton(arrow2_vi if _vi2 else arrow2_en)
        self._arrow2.setCursor(QtCompat.PointingHandCursor)
        self._arrow2.setStyleSheet(
            "QPushButton{background:#E3F2FD;color:#1565c0;"
            "font-weight:bold;font-size:13px;padding:10px;"
            "border:2px solid #90CAF9;border-radius:6px;}"
            "QPushButton:hover{background:#BBDEFB;border-color:#42A5F5;}"
        )
        self._arrow2.clicked.connect(lambda: self._tabs.setCurrentIndex(2))
        g.addWidget(self._arrow2, row, 0, 1, 2)
        row += 1
        self._arrow2_visible = True
        self._arrow_timer2 = QTimer(self)
        self._arrow_timer2.timeout.connect(self._pulse_arrow2)
        self._arrow_timer2.start(800)

        g.setRowStretch(row, 1)
        return w

    def _pulse_arrow2(self):
        """Toggle arrow2 opacity for pulse effect."""
        self._arrow2_visible = not self._arrow2_visible
        if self._arrow2_visible:
            self._arrow2.setStyleSheet(
                "QPushButton{background:#E3F2FD;color:#1565c0;"
                "font-weight:bold;font-size:13px;padding:10px;"
                "border:2px solid #90CAF9;border-radius:6px;}"
                "QPushButton:hover{background:#BBDEFB;border-color:#42A5F5;}"
            )
        else:
            self._arrow2.setStyleSheet(
                "QPushButton{background:#BBDEFB;color:#0D47A1;"
                "font-weight:bold;font-size:13px;padding:10px;"
                "border:2px solid #42A5F5;border-radius:6px;}"
                "QPushButton:hover{background:#BBDEFB;border-color:#42A5F5;}"
            )

    # ── Tab: Content ─────────────────────────────────────────────

    def _tab_content(self):
        w = QWidget()
        g = QGridLayout(w)
        row = 0

        # ── Legend Block ──────────────────────────────────────────
        self.chk_legend = QCheckBox(
            "\U0001f4cb Th\u00eam ch\u00fa d\u1eabn / Add Legend Block")
        self.chk_legend.setStyleSheet(
            "font-weight:bold;font-size:12px")
        self.chk_legend.stateChanged.connect(self._toggle_legend)
        g.addWidget(self.chk_legend, row, 0, 1, 2)
        row += 1

        self._leg_container = QWidget()
        leg_ly = QVBoxLayout(self._leg_container)
        leg_ly.setContentsMargins(10, 4, 0, 0)

        # Title
        title_row = QHBoxLayout()
        title_row.addWidget(QLabel(tr("Title:")))
        self.txt_leg_title = QLineEdit("CH\u00da D\u1eaaN")
        self.txt_leg_title.setMaximumWidth(200)
        title_row.addWidget(self.txt_leg_title)
        title_row.addWidget(QLabel(tr("Size:")))
        self.spn_leg_title_size = QSpinBox()
        self.spn_leg_title_size.setRange(8, 36)
        self.spn_leg_title_size.setValue(14)
        self.spn_leg_title_size.setSuffix("pt")
        title_row.addWidget(self.spn_leg_title_size)
        title_row.addStretch()
        leg_ly.addLayout(title_row)

        # Columns & Layout
        col_row = QHBoxLayout()
        col_row.addWidget(QLabel(tr("Columns:")))
        self.spn_leg_cols = QSpinBox()
        self.spn_leg_cols.setRange(1, 6)
        self.spn_leg_cols.setValue(2)
        col_row.addWidget(self.spn_leg_cols)
        self.chk_leg_split = QCheckBox("Split layers")
        self.chk_leg_split.setChecked(True)
        col_row.addWidget(self.chk_leg_split)
        self.chk_leg_filter_map = QCheckBox("L\u1ecdc theo Map")
        self.chk_leg_filter_map.setChecked(True)
        col_row.addWidget(self.chk_leg_filter_map)
        self.chk_leg_auto_update = QCheckBox("T\u1ef1 c\u1eadp nh\u1eadt")
        self.chk_leg_auto_update.setChecked(True)
        col_row.addWidget(self.chk_leg_auto_update)
        col_row.addStretch()
        leg_ly.addLayout(col_row)

        # Symbol & Spacing
        size_row2 = QHBoxLayout()
        size_row2.addWidget(QLabel(tr("Symbol W:")))
        self.spn_leg_sym_w = QSpinBox()
        self.spn_leg_sym_w.setRange(3, 40)
        self.spn_leg_sym_w.setValue(23)
        self.spn_leg_sym_w.setSuffix("mm")
        size_row2.addWidget(self.spn_leg_sym_w)
        size_row2.addWidget(QLabel("H:"))
        self.spn_leg_sym_h = QSpinBox()
        self.spn_leg_sym_h.setRange(3, 20)
        self.spn_leg_sym_h.setValue(8)
        self.spn_leg_sym_h.setSuffix("mm")
        size_row2.addWidget(self.spn_leg_sym_h)
        size_row2.addWidget(QLabel(tr("Bottom margin:")))
        self.spn_leg_mb = QSpinBox()
        self.spn_leg_mb.setRange(0, 50)
        self.spn_leg_mb.setValue(6)
        self.spn_leg_mb.setSuffix("mm")
        size_row2.addWidget(self.spn_leg_mb)
        size_row2.addWidget(QLabel("Max W:"))
        self.spn_leg_max_w = QSpinBox()
        self.spn_leg_max_w.setRange(60, 350)
        self.spn_leg_max_w.setValue(120)
        self.spn_leg_max_w.setSuffix("mm")
        size_row2.addWidget(self.spn_leg_max_w)
        size_row2.addStretch()
        leg_ly.addLayout(size_row2)

        # Margin row (Left + Bottom — same as Signature Block)
        margin_row = QHBoxLayout()
        margin_row.addWidget(QLabel(tr("Cách trái / Left:")))
        self.spn_leg_ml = QSpinBox()
        self.spn_leg_ml.setRange(0, 50)
        self.spn_leg_ml.setValue(3)
        self.spn_leg_ml.setSuffix("mm")
        margin_row.addWidget(self.spn_leg_ml)
        margin_row.addWidget(QLabel(tr("Cách dưới / Bottom:")))
        self.spn_leg_margin_bottom = QSpinBox()
        self.spn_leg_margin_bottom.setRange(0, 50)
        self.spn_leg_margin_bottom.setValue(6)
        self.spn_leg_margin_bottom.setSuffix("mm")
        margin_row.addWidget(self.spn_leg_margin_bottom)
        margin_row.addStretch()
        leg_ly.addLayout(margin_row)

        # Font
        font_row2 = QHBoxLayout()
        font_row2.addWidget(QLabel(tr("Font:")))
        self.cmb_leg_font = QComboBox()
        self.cmb_leg_font.addItems(["Times New Roman", "Arial"])
        self.cmb_leg_font.setMinimumWidth(120)
        font_row2.addWidget(self.cmb_leg_font)
        font_row2.addWidget(QLabel(tr("Font size:")))
        self.spn_leg_font_size = QSpinBox()
        self.spn_leg_font_size.setRange(5, 18)
        self.spn_leg_font_size.setValue(8)
        self.spn_leg_font_size.setSuffix("pt")
        font_row2.addWidget(self.spn_leg_font_size)
        font_row2.addWidget(QLabel(tr("Line spacing:")))
        self.spn_leg_spacing = QDoubleSpinBox()
        self.spn_leg_spacing.setRange(0.8, 3.0)
        self.spn_leg_spacing.setValue(1.4)
        self.spn_leg_spacing.setSingleStep(0.1)
        font_row2.addWidget(self.spn_leg_spacing)
        self.chk_leg_hdr_bold = QCheckBox("Header B")
        self.chk_leg_hdr_bold.setChecked(True)
        self.chk_leg_hdr_bold.setStyleSheet("font-weight:bold")
        font_row2.addWidget(self.chk_leg_hdr_bold)
        font_row2.addStretch()
        leg_ly.addLayout(font_row2)

        # \u2500\u2500 Layer selection \u2500\u2500
        leg_ly.addWidget(QLabel(
            f"\u2500\u2500 {tr('Layers in Legend')} \u2500\u2500"))

        # Toolbar
        toolbar_row = QHBoxLayout()
        btn_refresh = QPushButton(f"\U0001f504 {tr('Refresh')}")
        btn_refresh.setMaximumWidth(100)
        btn_refresh.clicked.connect(self._refresh_legend_layers)
        toolbar_row.addWidget(btn_refresh)
        btn_sel_all = QPushButton(f"\u2611 {tr('Select All')}")
        btn_sel_all.setMaximumWidth(110)
        btn_sel_all.clicked.connect(self._leg_select_all)
        toolbar_row.addWidget(btn_sel_all)
        btn_desel = QPushButton(f"\u2610 {tr('Deselect')}")
        btn_desel.setMaximumWidth(100)
        btn_desel.clicked.connect(self._leg_deselect_all)
        toolbar_row.addWidget(btn_desel)
        self.chk_leg_hide_groups = QCheckBox("\u1ea8n nh\u00f3m / Hide groups")
        self.chk_leg_hide_groups.setChecked(False)
        toolbar_row.addWidget(self.chk_leg_hide_groups)
        toolbar_row.addStretch()
        leg_ly.addLayout(toolbar_row)

        # Search box
        search_row = QHBoxLayout()
        search_row.addWidget(QLabel("\U0001f50d"))
        self._leg_search = QLineEdit()
        self._leg_search.setPlaceholderText(tr("Search layer..."))
        self._leg_search.setClearButtonEnabled(True)
        self._leg_search.textChanged.connect(self._leg_search_filter)
        search_row.addWidget(self._leg_search)
        leg_ly.addLayout(search_row)

        # Column headers
        hdr_row = QHBoxLayout()
        hdr_row.addWidget(QLabel(f"  {tr('Layer')}"), 1)
        lbl_rename = QLabel(tr("Display name"))
        lbl_rename.setMaximumWidth(120)
        lbl_rename.setMinimumWidth(80)
        hdr_row.addWidget(lbl_rename)
        lbl_sym = QLabel(tr("Symbol"))
        lbl_sym.setMinimumWidth(140)
        lbl_sym.setMaximumWidth(180)
        hdr_row.addWidget(lbl_sym)
        leg_ly.addLayout(hdr_row)

        self._leg_layer_scroll = QScrollArea()
        self._leg_layer_scroll.setWidgetResizable(True)
        self._leg_layer_scroll.setMinimumHeight(200)
        self._leg_layer_scroll.setMaximumHeight(500)
        self._leg_layer_widget = QWidget()
        self._leg_layer_layout = QVBoxLayout(self._leg_layer_widget)
        self._leg_layer_layout.setContentsMargins(4, 2, 4, 2)
        self._leg_layer_scroll.setWidget(self._leg_layer_widget)
        leg_ly.addWidget(self._leg_layer_scroll)
        self._leg_layer_checks = {}
        self._leg_symbol_combos = {}
        self._leg_layer_renames = {}
        self._refresh_legend_layers()

        self._leg_container.setVisible(False)
        g.addWidget(self._leg_container, row, 0, 1, 2)
        row += 1

        # \u2500\u2500 Data Sources \u2500\u2500
        g.addWidget(QLabel(f"{tr('Data Sources')}:"), row, 0, 1, 2)
        row += 1
        self.txt_sources = QTextEdit()
        self.txt_sources.setPlaceholderText(
            "e.g. National land-use map 2020, Sentinel-2 imagery..."
        )
        self.txt_sources.setMaximumHeight(80)
        g.addWidget(self.txt_sources, row, 0, 1, 2)
        row += 1

        # Elements always shown (template-driven)
        class _AlwaysTrue:
            def isChecked(self):
                return True

        class _DefaultArrow:
            def currentData(self):
                return ""
        self.chk_north = _AlwaysTrue()
        self.chk_scalebar = _AlwaysTrue()
        self.cmb_arrow = _DefaultArrow()

        # ── North Arrow position & size ───────────────────────────
        from ..shared.i18n import current_language
        _vi_c = current_language() == 'vi'
        sep_na = QLabel(
            "🧭 Mũi tên hướng Bắc / North Arrow" if _vi_c
            else "🧭 North Arrow Settings")
        sep_na.setStyleSheet(
            "font-weight:bold;font-size:12px;color:#1B5E20;"
            "padding:6px 0;margin-top:8px;")
        g.addWidget(sep_na, row, 0, 1, 2)
        row += 1

        na_pos = QHBoxLayout()
        na_pos.addWidget(QLabel(
            "Lề trái / Left:" if _vi_c else "Left:"))
        self.spn_na_left = QSpinBox()
        self.spn_na_left.setRange(0, 500)
        self.spn_na_left.setValue(50)
        self.spn_na_left.setSuffix(" mm")
        na_pos.addWidget(self.spn_na_left)
        na_pos.addWidget(QLabel(
            "Lề trên / Top:" if _vi_c else "Top:"))
        self.spn_na_top = QSpinBox()
        self.spn_na_top.setRange(0, 500)
        self.spn_na_top.setValue(50)
        self.spn_na_top.setSuffix(" mm")
        na_pos.addWidget(self.spn_na_top)
        g.addLayout(na_pos, row, 0, 1, 2)
        row += 1

        na_size = QHBoxLayout()
        na_size.addWidget(QLabel(
            "Rộng / Width:" if _vi_c else "Width:"))
        self.spn_na_w = QSpinBox()
        self.spn_na_w.setRange(10, 200)
        self.spn_na_w.setValue(40)
        self.spn_na_w.setSuffix(" mm")
        na_size.addWidget(self.spn_na_w)
        na_size.addWidget(QLabel(
            "Cao / Height:" if _vi_c else "Height:"))
        self.spn_na_h = QSpinBox()
        self.spn_na_h.setRange(10, 200)
        self.spn_na_h.setValue(40)
        self.spn_na_h.setSuffix(" mm")
        na_size.addWidget(self.spn_na_h)
        g.addLayout(na_size, row, 0, 1, 2)
        row += 1

        g.setRowStretch(row, 1)
        return w

    # ── Slots ────────────────────────────────────────────────────

    def _toggle_map_method(self, checked):
        """Toggle between Predefined paper and Draw mode."""
        is_predefined = self.rad_predefined.isChecked()
        self._predefined_panel.setVisible(is_predefined)
        self._draw_panel.setVisible(not is_predefined)
        # Sync orientation visibility
        self.lbl_orient.setVisible(is_predefined)
        self.orient_widget.setVisible(is_predefined)
        # Sync hidden combobox for engine
        if is_predefined:
            self._on_predefined_changed(self.cmb_paper_visible.currentIndex())
            self.lbl_drawn.setText("")
            self.lbl_scale_hint.setText("")
        else:
            self.cmb_paper.setCurrentIndex(self.cmb_paper.count() - 1)

    def _on_predefined_changed(self, idx):
        """Sync visible paper combo to hidden engine combo."""
        if idx < self.cmb_paper.count() - 1:  # not Custom
            self.cmb_paper.setCurrentIndex(idx)

    def _toggle_custom_size(self, idx):
        """Legacy compat — now handled by _toggle_map_method."""

    def _toggle_scale(self, state):
        self.spn_scale.setEnabled(state == QtCompat.Unchecked)
        if state == QtCompat.Unchecked and hasattr(
                self, 'drawn_extent') and self.drawn_extent:
            self._on_scale_changed(self.spn_scale.value())

    def refresh_layers(self):
        pass  # QgsMapLayerComboBox auto-refreshes

    # ── Draw Extent on Map ───────────────────────────────────────

    def _on_draw_extent(self):
        """Activate rectangle drawing tool on the map canvas."""
        from .extent_tool import LvtExtentTool
        canvas = self.iface.mapCanvas()
        self.extent_tool = LvtExtentTool(canvas, self._on_extent_drawn)
        canvas.setMapTool(self.extent_tool)
        self.iface.messageBar().pushInfo(
            "LVT",
            "Draw a rectangle on the map to define the Map Frame area."
            " / Vẽ phạm vi khung bản đồ (khung xanh)."
        )
        self.hide()  # Hide dialog while drawing

    def _on_extent_drawn(self, rect):
        """Callback: store drawn extent and calculate initial page size.

        Workflow: draw extent → suggest scales → user picks scale → page auto-sizes.
        """
        self.drawn_extent = rect

        # Store extent in real-world meters
        crs = self.iface.mapCanvas().mapSettings().destinationCrs()
        ext_w = rect.width()
        ext_h = rect.height()
        if crs.isGeographic():
            ext_w *= 111320
            ext_h *= 111320
        self._extent_w_m = ext_w
        self._extent_h_m = ext_h

        # Calculate natural scale from current canvas view
        canvas = self.iface.mapCanvas()
        mu_per_mm = canvas.mapUnitsPerPixel() * (canvas.logicalDpiX() / 25.4)
        view_map_w = rect.width() / mu_per_mm
        view_map_h = rect.height() / mu_per_mm
        natural_scale = max(
            ext_w / view_map_w * 1000 if view_map_w > 0 else 10000,
            ext_h / view_map_h * 1000 if view_map_h > 0 else 10000
        )

        # Suggest scales
        suggestions = [s for s in STANDARD_SCALES
                       if s >= natural_scale * 0.5 and s <= natural_scale * 3]
        if not suggestions:
            diffs = sorted([(abs(s - natural_scale), s)
                           for s in STANDARD_SCALES])
            suggestions = [d[1] for d in diffs[:3]]
        suggestions.sort()
        suggestion_str = ", ".join([f"1:{s:,}" for s in suggestions])
        self.lbl_scale_hint.setText(
            f"💡 Suggested / Đề xuất: {suggestion_str}\n"
            f"   (Natural / Tự nhiên ≈ 1:{int(natural_scale):,})"
        )

        # Set best scale → triggers _on_scale_changed → sets page size
        best = min(STANDARD_SCALES, key=lambda s: abs(s - natural_scale))
        self.spn_scale.setValue(best)

        # Also calculate page for current scale right away
        self._recalc_page_from_scale(best)

        self.lbl_orient.setVisible(False)
        self.orient_widget.setVisible(False)

        self.show()
        self.raise_()

    def _on_scale_changed(self, scale_val):
        """When user changes scale, recalculate page size from drawn extent."""
        if not hasattr(self, '_extent_w_m') or not self._extent_w_m:
            return
        if self.chk_auto_scale.isChecked():
            return
        self._recalc_page_from_scale(scale_val)

    def _get_margins(self):
        """Return (left, top, right, bottom) margins in mm
        based on current mode and estimated map frame size."""
        is_print = self.rad_print.isChecked()
        if not is_print:
            return MARGINS["slide"]
        # Estimate total page width to choose margin tier
        map_w = self.spn_custom_w.value()
        if map_w >= 1100:
            return MARGINS["print_xlarge"]
        elif map_w >= 550:
            return MARGINS["print_large"]
        elif map_w >= 380:
            return MARGINS["print_medium"]
        else:
            return MARGINS["print_small"]

    def _recalc_page_from_scale(self, scale_val):
        """Calculate Map Frame size from drawn extent and scale.

        The spinboxes represent the Map Frame (green frame) size.
        Total page size = Map Frame + margins (calculated at create time).
        """
        if scale_val <= 0:
            return

        map_w_mm = self._extent_w_m / scale_val * 1000
        map_h_mm = self._extent_h_m / scale_val * 1000

        map_w = int(round(map_w_mm))
        map_h = int(round(map_h_mm))
        map_w = max(50, min(3000, map_w))
        map_h = max(50, min(3000, map_h))

        # Calculate page size for display
        ml, mt, mr, mb = self._get_margins()
        page_w = int(round(map_w + ml + mr))
        page_h = int(round(map_h + mt + mb))

        if page_w > 3000 or page_h > 3000:
            self.lbl_drawn.setStyleSheet(
                "color:#c62828;font-weight:bold")
            self.lbl_drawn.setText(
                f"⚠️ Page too large! / Trang quá lớn! "
                f"({page_w}×{page_h} mm)\n"
                f"   Max 3000mm. Try a smaller scale "
                f"/ Thử tỷ lệ nhỏ hơn."
            )
            return

        self.spn_custom_w.setValue(map_w)
        self.spn_custom_h.setValue(map_h)

        if map_w >= map_h:
            self.rad_landscape.setChecked(True)
        else:
            self.rad_portrait.setChecked(True)

        self.lbl_drawn.setText(
            f"✅ Extent: "
            f"{self.drawn_extent.xMinimum():.1f}, "
            f"{self.drawn_extent.yMinimum():.1f} → "
            f"{self.drawn_extent.xMaximum():.1f}, "
            f"{self.drawn_extent.yMaximum():.1f}\n"
            f"   Scale 1:{scale_val:,}  →  "
            f"🟢 Map: {map_w}×{map_h} mm  |  "
            f"📄 Page: {page_w}×{page_h} mm"
        )
        self.lbl_drawn.setStyleSheet(
            "color:#2e7d32;font-weight:bold")

    # ── Help ─────────────────────────────────────────────────────

    def _show_help(self):
        """Show help dialog with tabs: Plugin Guide + CRS Guide."""
        dlg = QDialog(self)
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        dlg.setWindowTitle(
            "📖 LVT Map Layout — Trợ giúp" if _vi
            else "📖 LVT Map Layout — Help")
        dlg.setMinimumSize(700, 560)
        lay = QVBoxLayout(dlg)

        # Language toggle
        btn_row = QHBoxLayout()
        btn_en = QPushButton("🇬🇧 English")
        btn_vn = QPushButton("🇻🇳 Tiếng Việt")
        for b in [btn_en, btn_vn]:
            b.setCheckable(True)
            b.setStyleSheet(
                "QPushButton{padding:6px 16px;border-radius:4px;"
                "font-weight:bold;border:2px solid #ccc}"
                "QPushButton:checked{background:#1565c0;color:#fff;border-color:#1565c0}")
        btn_vn.setChecked(True)
        btn_row.addStretch()
        btn_row.addWidget(btn_en)
        btn_row.addWidget(btn_vn)
        btn_row.addStretch()
        lay.addLayout(btn_row)

        # Tabbed content
        from qgis.PyQt.QtWidgets import QTabWidget
        tabs = QTabWidget()
        tabs.setStyleSheet(
            "QTabWidget::pane{border:1px solid #ccc;border-radius:4px}"
            "QTabBar::tab{padding:8px 16px;font-weight:bold}"
            "QTabBar::tab:selected{background:#1565c0;color:#fff;"
            "border-radius:4px 4px 0 0}"
        )
        lay.addWidget(tabs)

        def _make_scroll():
            s = QScrollArea()
            s.setWidgetResizable(True)
            lbl = QLabel()
            lbl.setWordWrap(True)
            lbl.setTextFormat(QtCompat.RichText)
            lbl.setOpenExternalLinks(True)
            lbl.setMargin(12)
            s.setWidget(lbl)
            return s, lbl

        s1, lbl1 = _make_scroll()
        tabs.addTab(s1, "🗺️ Plugin Guide")

        s2, lbl2 = _make_scroll()
        tabs.addTab(s2, "🌐 CRS Guide")

        h_en, h_vn = self._help_text_en(), self._help_text_vn()
        c_en, c_vn = self._crs_guide_en(), self._crs_guide_vn()
        lbl1.setText(h_vn)
        lbl2.setText(c_vn)

        def switch_en():
            btn_en.setChecked(True)
            btn_vn.setChecked(False)
            lbl1.setText(h_en)
            lbl2.setText(c_en)

        def switch_vn():
            btn_vn.setChecked(True)
            btn_en.setChecked(False)
            lbl1.setText(h_vn)
            lbl2.setText(c_vn)

        btn_en.clicked.connect(switch_en)
        btn_vn.clicked.connect(switch_vn)

        btn_close = QPushButton("OK")
        btn_close.clicked.connect(dlg.accept)
        lay.addWidget(btn_close)
        dlg.exec()

    # ── CRS Library Dialog ───────────────────────────────────────

    def _show_crs(self):
        """Show CRS Library dialog with Set Project CRS feature."""
        from qgis.core import QgsProject, QgsCoordinateReferenceSystem  # noqa: F811
        from qgis.PyQt.QtWidgets import QComboBox, QMessageBox

        dlg = QDialog(self)
        from ..shared.i18n import current_language
        _vi = current_language() == 'vi'
        dlg.setWindowTitle(
            "🌐 Thư viện Hệ tọa độ" if _vi
            else "🌐 CRS Library")
        dlg.setMinimumSize(700, 560)
        lay = QVBoxLayout(dlg)

        # ── Current CRS info bar ──
        current_crs = QgsProject.instance().crs()
        crs_info = QLabel(
            f"📍 Current / Hiện tại: "
            f"<b>{current_crs.authid()}</b> — {current_crs.description()}"
        )
        crs_info.setStyleSheet(
            "padding:8px;border-radius:4px;"
            "font-size:12px;font-weight:bold;border:1px solid #999"
        )
        crs_info.setWordWrap(True)
        lay.addWidget(crs_info)

        # ── Set Project CRS ──
        set_row = QHBoxLayout()
        set_row.addWidget(QLabel("⚙️ <b>Set Project CRS / Đổi CRS dự án:</b>"))
        cmb_crs = QComboBox()
        cmb_crs.setMinimumWidth(340)
        for label, _ in self.CRS_LIST:
            cmb_crs.addItem(label)
        for i, (_, code) in enumerate(self.CRS_LIST):
            if not code:
                cmb_crs.model().item(i).setEnabled(False)

        cur_auth = current_crs.authid()
        for i, (_, code) in enumerate(self.CRS_LIST):
            if code == cur_auth:
                cmb_crs.setCurrentIndex(i)
                break

        btn_apply = QPushButton("✅ Apply / Áp dụng")
        btn_apply.setStyleSheet(
            "QPushButton{background:#2e7d32;color:#fff;font-weight:bold;"
            "padding:6px 16px;border-radius:4px}"
            "QPushButton:hover{background:#388e3c}"
        )

        def _apply_crs():
            idx = cmb_crs.currentIndex()
            code = self.CRS_LIST[idx][1]
            if not code:
                return
            new_crs = QgsCoordinateReferenceSystem(code)
            if not new_crs.isValid():
                QMessageBox.warning(dlg, "CRS Error",
                                    f"Invalid CRS: {code}")
                return
            QgsProject.instance().setCrs(new_crs)
            crs_info.setText(
                f"📍 Current / Hiện tại: "
                f"<b>{new_crs.authid()}</b> — {new_crs.description()}"
            )
            # Sync General tab label
            if hasattr(self, '_crs_label'):
                self._crs_label.setText(
                    f"📍 <b>{new_crs.authid()}</b> — {new_crs.description()}"
                )
            QMessageBox.information(
                dlg, "✅ CRS Updated",
                f"Project CRS changed to:\n{new_crs.authid()} — {new_crs.description()}")

        btn_apply.clicked.connect(_apply_crs)
        set_row.addWidget(cmb_crs)
        set_row.addWidget(btn_apply)
        lay.addLayout(set_row)

        # ── Language toggle ──
        btn_row = QHBoxLayout()
        btn_en = QPushButton("🇬🇧 English")
        btn_vn = QPushButton("🇻🇳 Tiếng Việt")
        for b in [btn_en, btn_vn]:
            b.setCheckable(True)
            b.setStyleSheet(
                "QPushButton{padding:6px 16px;border-radius:4px;"
                "font-weight:bold;border:2px solid #ccc}"
                "QPushButton:checked{background:#1565c0;color:#fff;border-color:#1565c0}")
        btn_vn.setChecked(True)
        btn_row.addStretch()
        btn_row.addWidget(btn_en)
        btn_row.addWidget(btn_vn)
        btn_row.addStretch()
        lay.addLayout(btn_row)

        # ── Tabbed CRS content ──
        tabs = QTabWidget()

        # --- Tab 1: CRS Library (34 provinces) ---
        scroll1 = QScrollArea()
        scroll1.setWidgetResizable(True)
        content = QLabel()
        content.setWordWrap(True)
        content.setTextFormat(QtCompat.RichText)
        content.setOpenExternalLinks(True)
        content.setMargin(12)
        scroll1.setWidget(content)
        tabs.addTab(scroll1, "🌐 CRS 34 Tỉnh Mới")

        # --- Tab 2: Legacy 63 provinces ---
        scroll2 = QScrollArea()
        scroll2.setWidgetResizable(True)
        legacy = QLabel()
        legacy.setWordWrap(True)
        legacy.setTextFormat(QtCompat.RichText)
        legacy.setOpenExternalLinks(True)
        legacy.setMargin(12)
        legacy.setText(self._crs_legacy_63())
        scroll2.setWidget(legacy)
        tabs.addTab(scroll2, "📋 63 Tỉnh Cũ (Tham khảo)")

        # --- Tab 3: Nội bộ (3 tham số) ---
        scroll3 = QScrollArea()
        scroll3.setWidgetResizable(True)
        nb_label = QLabel()
        nb_label.setWordWrap(True)
        nb_label.setTextFormat(QtCompat.RichText)
        nb_label.setMargin(12)
        nb_label.setText(self._crs_noibotext())
        scroll3.setWidget(nb_label)
        tabs.addTab(scroll3, "🔒 Nội bộ (3 tham số)")

        # --- Tab 4: HN-72 ---
        scroll4 = QScrollArea()
        scroll4.setWidgetResizable(True)
        hn72_label = QLabel()
        hn72_label.setWordWrap(True)
        hn72_label.setTextFormat(QtCompat.RichText)
        hn72_label.setMargin(12)
        hn72_label.setText(self._crs_hn72_text())
        scroll4.setWidget(hn72_label)
        tabs.addTab(scroll4, "📐 HN-72 (Gauss-Kruger)")

        # --- Tab 5: Indian 1975 & Quốc tế ---
        scroll5 = QScrollArea()
        scroll5.setWidgetResizable(True)
        indian_label = QLabel()
        indian_label.setWordWrap(True)
        indian_label.setTextFormat(QtCompat.RichText)
        indian_label.setMargin(12)
        indian_label.setText(self._crs_indian_intl_text())
        scroll5.setWidget(indian_label)
        tabs.addTab(scroll5, "🌏 Indian 1975 & Quốc tế")

        lay.addWidget(tabs)

        crs_en = self._crs_text_en()
        crs_vn = self._crs_text_vn()
        content.setText(crs_vn)

        def switch_en():
            btn_en.setChecked(True)
            btn_vn.setChecked(False)
            content.setText(crs_en)

        def switch_vn():
            btn_vn.setChecked(True)
            btn_en.setChecked(False)
            content.setText(crs_vn)

        btn_en.clicked.connect(switch_en)
        btn_vn.clicked.connect(switch_vn)

        btn_close = QPushButton("OK")
        btn_close.clicked.connect(dlg.accept)
        lay.addWidget(btn_close)
        dlg.exec()

    def _help_text_en(self):
        return """
<h2>🗺️ LVT Map Layout v2.0 — User Guide</h2>

<h3>🔷 LAYOUT MODES</h3>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;margin-bottom:8px">
<tr style="background:#e3f2fd"><td><b>🖼️ Slide</b></td>
<td>Simple, borderless layout — ideal for presentations, reports, quick exports.</td></tr>
<tr style="background:#e8f5e9"><td><b>🖨️ Print</b></td>
<td>Full cartographic layout — multi-line frame, coordinate grids, scalebars,
title block, references. Professional map output (A5→A0).</td></tr>
</table>

<h3>📌 Step 1 — General Settings</h3>
<ul>
<li>🔘 <b>Layout Mode:</b> Choose <b>Slide</b> or <b>Print</b>.</li>
<li>🌐 <b>Language:</b> EN (English) or VN (Tiếng Việt) — controls all template labels.</li>
</ul>
<p><b>🖨️ Print mode — additional fields:</b></p>
<ul>
<li>📝 <b>Map Title:</b> Main title displayed in the title block.</li>
<li>🏢 <b>Organization:</b> Company / organization name.</li>
<li>📍 <b>Study Area:</b> Geographic area name (province, district, etc.).</li>
<li>📚 <b>References:</b> Technical standards, e.g. <i>TCVN 8211:2009</i>.</li>
</ul>
<p><b>👤 Common fields (both modes):</b></p>
<ul>
<li>✍️ <b>Author:</b> Map creator name.</li>
<li>📅 <b>Date:</b> Creation date (auto-filled today).</li>
</ul>

<h3>📌 Step 2 — Map Settings</h3>
<p><b>📐 Option A — Standard Paper:</b></p>
<ul>
<li>📄 Select paper size: <b>A5, A4, A3, A2, A1, A0</b>.</li>
<li>🔄 Choose orientation: <b>Landscape</b> (horizontal) or <b>Portrait</b> (vertical).</li>
<li>Map extent = current QGIS canvas view.</li>
</ul>
<p><b>🖊️ Option B — Draw Extent (Custom):</b></p>
<ol>
<li>Select <b>"Custom / Tự chọn"</b> in Paper Size dropdown.</li>
<li>Click <b>"🖊 Draw Extent"</b> button → draw a rectangle on the map canvas.</li>
<li>The drawn rectangle defines the exact map content area.</li>
<li>Plugin auto-calculates page dimensions and suggests standard scales.</li>
<li>💡 Scale suggestions appear below (e.g. 1:5,000 / 1:10,000 / 1:25,000).</li>
</ol>
<p><b>🔍 Scale options:</b></p>
<ul>
<li>☑ <b>Auto fit:</b> Scale adjusts automatically to fill the page.</li>
<li>☐ <b>Manual:</b> Uncheck "Auto fit" → enter a specific scale (e.g. 1:10,000).</li>
</ul>

<h3>📌 Step 2b — Signature Block ✍ (Print mode only)</h3>
<p>The Signature Block adds formal sign-off boxes at the bottom of your map — used for official forestry, cadastral, and environmental reports.</p>
<p><b>How to enable:</b></p>
<ol>
<li>Select <b>Print</b> mode in Step 1.</li>
<li>Check <b>✍ Add Signature Block</b> in the Map Settings tab.</li>
<li>Configure the expanded panel below.</li>
</ol>
<p><b>Parameters:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Parameter</th><th>Description</th><th>Default</th><th>Range</th></tr>
<tr><td>Count</td><td>Number of signature boxes</td><td>2</td><td>1–8</td></tr>
<tr><td>Height</td><td>Box height</td><td>50 mm</td><td>30–200 mm</td></tr>
<tr><td>Width</td><td>Box width</td><td>50 mm</td><td>30–200 mm</td></tr>
<tr><td>Left margin</td><td>Distance from left edge</td><td>3 mm</td><td>0–100 mm</td></tr>
<tr><td>Bottom margin</td><td>Distance from bottom edge</td><td>3 mm</td><td>0–100 mm</td></tr>
</table>
<p><b>Font styles</b> — each block has 3 text lines with independent font control:</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Component</th><th>Purpose</th><th>Default</th><th>Style</th></tr>
<tr><td>Title</td><td>Role/org label (e.g. "PARTY A")</td><td>12pt</td><td>Bold</td></tr>
<tr><td>Date</td><td>Signing date line</td><td>10pt</td><td>Italic</td></tr>
<tr><td>Name</td><td>Signatory full name</td><td>11pt</td><td>Bold</td></tr>
</table>
<p><b>Block content</b> — each box has 3 editable fields:</p>
<ul>
<li><b>Title:</b> Auto-fills "ĐẠI DIỆN BÊN A / B / C…"</li>
<li><b>Date:</b> Auto-fills today's date (Vietnamese format)</li>
<li><b>Name:</b> Type the signatory's full name</li>
</ul>
<p style="color:#1B5E20">💡 <i>2-party contract = Count 2. Multi-stakeholder (forestry + environment + commune) = Count 3–4.</i></p>

<h3>📌 Step 3 — Content &amp; Legend</h3>
<p><b>Basic content elements:</b></p>
<ul>
<li>📋 <b>Data Sources:</b> List your map data sources (satellite imagery, survey data, etc.).</li>
<li>🧭 <b>North Arrow:</b> Toggle on/off.</li>
<li>📏 <b>Scale Bar:</b> Toggle on/off (meter + degree/minute bars in Print mode).</li>
</ul>

<h4>📋 Legend Block (Print mode only)</h4>
<p>Generates a native QGIS legend showing map symbols, layer names, and classification categories.</p>
<p><b>How to enable:</b></p>
<ol>
<li>Go to the <b>Legend/Content</b> tab.</li>
<li>Check <b>📋 Add Legend Block</b>.</li>
<li>Configure title, columns, fonts, and select layers.</li>
</ol>
<p><b>Title &amp; layout:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Parameter</th><th>Description</th><th>Default</th></tr>
<tr><td>Title</td><td>Legend heading</td><td>CHÚ DẪN</td></tr>
<tr><td>Title size</td><td>Heading font size</td><td>14pt (8–36)</td></tr>
<tr><td>Columns</td><td>Number of columns</td><td>2 (1–6)</td></tr>
<tr><td>Split layers</td><td>Split items across columns</td><td>✅ On</td></tr>
<tr><td>Filter by Map</td><td>Only show visible layers</td><td>✅ On</td></tr>
<tr><td>Auto-update</td><td>Update when layers change</td><td>✅ On</td></tr>
</table>
<p><b>Symbol &amp; spacing:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Parameter</th><th>Description</th><th>Default</th><th>Range</th></tr>
<tr><td>Symbol W</td><td>Symbol width</td><td>23 mm</td><td>3–40 mm</td></tr>
<tr><td>Symbol H</td><td>Symbol height</td><td>8 mm</td><td>3–20 mm</td></tr>
<tr><td>Bottom margin</td><td>Space below legend</td><td>6 mm</td><td>0–50 mm</td></tr>
<tr><td>Max Width</td><td>Maximum legend width</td><td>120 mm</td><td>60–350 mm</td></tr>
<tr><td>Left margin</td><td>Distance from left edge</td><td>3 mm</td><td>0–50 mm</td></tr>
</table>
<p><b>Font:</b> Times New Roman / Arial, size 8pt (5–18), spacing 1.4 (0.8–3.0), Header Bold ✅</p>
<p><b>Layer selection:</b></p>
<ul>
<li>☑ <b>Checkbox:</b> Include/exclude each layer</li>
<li>✏️ <b>Display name:</b> Override layer name (blank = original)</li>
<li>🎨 <b>Symbol picker:</b> Choose different symbol from QGIS Style Manager</li>
<li>🔄 <b>Refresh:</b> Reload layers | ☑ <b>Select All</b> | ☐ <b>Deselect</b> | 🔍 <b>Search</b></li>
</ul>
<p style="color:#1B5E20">💡 <i>For thematic maps with many categories, increase Columns to 3–4 and reduce Symbol size.</i></p>

<h3>📌 Step 4 — Create &amp; Export</h3>
<ol>
<li>✅ Click <b>"Create Layout / Tạo khung"</b>.</li>
<li>🖥️ Layout opens in <b>QGIS Layout Designer</b>.</li>
<li>👀 Review all elements — adjust if needed.</li>
<li>💾 <b>File → Export as PDF</b> or <b>Export as Image</b> (PNG/TIFF).</li>
</ol>

<h3>📄 Paper Size Reference (ISO 216)</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Size</th><th>Dimensions (mm)</th><th>Best for</th></tr>
<tr style="background:#e3f2fd"><td colspan="3"><b>A-series</b></td></tr>
<tr><td>A5</td><td>210 × 148</td><td>Field pocket maps</td></tr>
<tr><td>A4</td><td>297 × 210</td><td>Reports, desk reference</td></tr>
<tr><td>A3</td><td>420 × 297</td><td>Field operations, presentations</td></tr>
<tr><td>A2</td><td>594 × 420</td><td>Wall display, planning</td></tr>
<tr><td>A1</td><td>841 × 594</td><td>Large wall maps</td></tr>
<tr><td>A0</td><td>1189 × 841</td><td>Exhibition, official submissions</td></tr>
<tr style="background:#e8f5e9"><td colspan="3"><b>B-series</b> (larger than A at same number)</td></tr>
<tr><td>B5</td><td>250 × 176</td><td>Books, envelopes</td></tr>
<tr><td>B4</td><td>353 × 250</td><td>Newspapers, atlases</td></tr>
<tr><td>B3</td><td>500 × 353</td><td>Posters, large charts</td></tr>
<tr><td>B2</td><td>707 × 500</td><td>Large posters, wall maps</td></tr>
<tr><td>B1</td><td>1000 × 707</td><td>Very large wall displays</td></tr>
<tr><td>B0</td><td>1414 × 1000</td><td>Exhibition, architectural plans</td></tr>
</table>

<p style="color:#888;font-size:10px;margin-top:12px"><i>LVT Map Layout v2.0 — Designed for forestry &amp; environmental mapping.</i></p>"""

    def _help_text_vn(self):
        return """
<h2>🗺️ LVT Map Layout v2.0 — Hướng dẫn sử dụng</h2>

<h3>🔷 CHẾ ĐỘ KHUNG BẢN ĐỒ</h3>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;margin-bottom:8px">
<tr style="background:#e3f2fd"><td><b>🖼️ Slide</b></td>
<td>Khung đơn giản, không viền — phù hợp trình chiếu, báo cáo, xuất nhanh.</td></tr>
<tr style="background:#e8f5e9"><td><b>🖨️ Print</b></td>
<td>Khung bản đồ chuyên nghiệp — khung viền nhiều lớp, lưới tọa độ,
thước tỷ lệ, khối tiêu đề, viện dẫn. Xuất bản đồ chuẩn (A5→A0).</td></tr>
</table>

<h3>📌 Bước 1 — Thiết lập chung (Tab General)</h3>
<ul>
<li>🔘 <b>Chế độ:</b> Chọn <b>Slide</b> hoặc <b>Print</b>.</li>
<li>🌐 <b>Ngôn ngữ:</b> EN (Tiếng Anh) hoặc VN (Tiếng Việt) — thay đổi toàn bộ nhãn trong khung.</li>
</ul>
<p><b>🖨️ Chế độ Print — các trường bổ sung:</b></p>
<ul>
<li>📝 <b>Tên bản đồ:</b> Tiêu đề chính hiển thị trong khối tiêu đề.</li>
<li>🏢 <b>Đơn vị:</b> Tên công ty / tổ chức xây dựng bản đồ.</li>
<li>📍 <b>Khu vực:</b> Tên khu vực nghiên cứu (tỉnh, huyện, xã, v.v.).</li>
<li>📚 <b>Viện dẫn:</b> Tiêu chuẩn kỹ thuật, vd: <i>TCVN 8211:2009</i>.</li>
</ul>
<p><b>👤 Trường chung (cả hai chế độ):</b></p>
<ul>
<li>✍️ <b>Người lập:</b> Tên người tạo bản đồ.</li>
<li>📅 <b>Ngày:</b> Ngày lập (tự điền ngày hôm nay).</li>
</ul>

<h3>📌 Bước 2 — Thiết lập bản đồ (Tab Map Settings)</h3>
<p><b>📐 Cách 1 — Khổ giấy chuẩn:</b></p>
<ul>
<li>📄 Chọn khổ giấy: <b>A5, A4, A3, A2, A1, A0</b>.</li>
<li>🔄 Chọn hướng: <b>Ngang</b> (Landscape) hoặc <b>Dọc</b> (Portrait).</li>
<li>Phạm vi bản đồ = toàn bộ canvas QGIS hiện tại.</li>
</ul>
<p><b>🖊️ Cách 2 — Vẽ phạm vi (Custom):</b></p>
<ol>
<li>Chọn <b>"Custom / Tự chọn"</b> ở dropdown Khổ giấy.</li>
<li>Bấm nút <b>"🖊 Draw Extent"</b> → vẽ hình chữ nhật trên bản đồ.</li>
<li>Hình chữ nhật vẽ = vùng nội dung bản đồ chính xác.</li>
<li>Plugin tự tính kích thước trang và đề xuất tỷ lệ chuẩn.</li>
<li>💡 Các tỷ lệ đề xuất hiện bên dưới (vd: 1:5.000 / 1:10.000 / 1:25.000).</li>
</ol>
<p><b>🔍 Tùy chọn tỷ lệ:</b></p>
<ul>
<li>☑ <b>Auto fit:</b> Tỷ lệ tự động điều chỉnh vừa trang.</li>
<li>☐ <b>Tự chọn:</b> Bỏ dấu "Auto fit" → nhập tỷ lệ cụ thể (vd: 1:10.000).</li>
</ul>

<h3>📌 Bước 2b — Ô ký / Khối chữ ký ✍ (chỉ chế độ Print)</h3>
<p>Ô ký thêm các khung chữ ký chính thức ở cuối bản đồ — dùng cho báo cáo lâm nghiệp, địa chính, môi trường.</p>
<p><b>Cách bật:</b></p>
<ol>
<li>Chọn chế độ <b>Print</b> ở Bước 1.</li>
<li>Tích chọn <b>✍ Thêm ô ký / Add Signature Block</b> trong tab Cài đặt bản đồ.</li>
<li>Cấu hình trong bảng mở rộng bên dưới.</li>
</ol>
<p><b>Thông số:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Thông số</th><th>Ý nghĩa</th><th>Mặc định</th><th>Phạm vi</th></tr>
<tr><td>Số lượng</td><td>Số ô ký cần tạo</td><td>2</td><td>1–8</td></tr>
<tr><td>Cao</td><td>Chiều cao mỗi ô</td><td>50 mm</td><td>30–200 mm</td></tr>
<tr><td>Rộng</td><td>Chiều rộng mỗi ô</td><td>50 mm</td><td>30–200 mm</td></tr>
<tr><td>Cách trái</td><td>Khoảng cách từ mép trái khung</td><td>3 mm</td><td>0–100 mm</td></tr>
<tr><td>Cách dưới</td><td>Khoảng cách từ mép dưới</td><td>3 mm</td><td>0–100 mm</td></tr>
</table>
<p><b>Kiểu chữ</b> — mỗi ô ký có 3 dòng văn bản với font độc lập:</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Thành phần</th><th>Chức năng</th><th>Mặc định</th><th>Kiểu</th></tr>
<tr><td>Tiêu đề</td><td>Chức danh/đơn vị (VD: "BÊN A")</td><td>12pt</td><td>Đậm</td></tr>
<tr><td>Ngày</td><td>Dòng ngày ký</td><td>10pt</td><td>Nghiêng</td></tr>
<tr><td>Họ tên</td><td>Tên đầy đủ người ký</td><td>11pt</td><td>Đậm</td></tr>
</table>
<p><b>Nội dung mỗi ô ký</b> — 3 trường nhập liệu:</p>
<ul>
<li><b>Tiêu đề:</b> Tự động điền "ĐẠI DIỆN BÊN A / B / C…"</li>
<li><b>Ngày:</b> Tự động điền ngày hiện tại (Ngày… tháng… năm…)</li>
<li><b>Họ tên:</b> Nhập tên đầy đủ người ký</li>
</ul>
<p style="color:#1B5E20">💡 <i>Hợp đồng 2 bên: Số lượng = 2. Biên bản đa bên (lâm nghiệp + môi trường + xã): Số lượng = 3–4.</i></p>

<h3>📌 Bước 3 — Nội dung &amp; Chú dẫn (Tab Content)</h3>
<p><b>Các thành phần cơ bản:</b></p>
<ul>
<li>📋 <b>Nguồn dữ liệu:</b> Liệt kê các nguồn dữ liệu bản đồ (ảnh vệ tinh, khảo sát, v.v.).</li>
<li>🧭 <b>Mũi tên Bắc:</b> Bật/tắt.</li>
<li>📏 <b>Thước tỷ lệ:</b> Bật/tắt (thước mét + thước độ/phút ở chế độ Print).</li>
</ul>

<h4>📋 Khối chú dẫn / Legend (chỉ chế độ Print)</h4>
<p>Tạo bảng chú dẫn QGIS gốc hiển thị ký hiệu, tên lớp và phân loại dữ liệu.</p>
<p><b>Cách bật:</b></p>
<ol>
<li>Chuyển sang tab <b>Legend/Content</b>.</li>
<li>Tích chọn <b>📋 Thêm chú dẫn / Add Legend Block</b>.</li>
<li>Cấu hình tiêu đề, số cột, font và chọn lớp.</li>
</ol>
<p><b>Tiêu đề &amp; bố cục:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Thông số</th><th>Ý nghĩa</th><th>Mặc định</th></tr>
<tr><td>Tiêu đề</td><td>Dòng tiêu đề chú dẫn</td><td>CHÚ DẪN</td></tr>
<tr><td>Cỡ tiêu đề</td><td>Cỡ chữ tiêu đề</td><td>14pt (8–36)</td></tr>
<tr><td>Số cột</td><td>Số cột hiển thị</td><td>2 (1–6)</td></tr>
<tr><td>Split layers</td><td>Cho phép chia mục sang cột khác</td><td>✅ Bật</td></tr>
<tr><td>Lọc theo Map</td><td>Chỉ hiện lớp trong phạm vi bản đồ</td><td>✅ Bật</td></tr>
<tr><td>Tự cập nhật</td><td>Chú dẫn cập nhật khi lớp thay đổi</td><td>✅ Bật</td></tr>
</table>
<p><b>Ký hiệu &amp; khoảng cách:</b></p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Thông số</th><th>Ý nghĩa</th><th>Mặc định</th><th>Phạm vi</th></tr>
<tr><td>Symbol W</td><td>Chiều rộng ký hiệu</td><td>23 mm</td><td>3–40 mm</td></tr>
<tr><td>Symbol H</td><td>Chiều cao ký hiệu</td><td>8 mm</td><td>3–20 mm</td></tr>
<tr><td>Cách dưới</td><td>Khoảng cách phía dưới</td><td>6 mm</td><td>0–50 mm</td></tr>
<tr><td>Max W</td><td>Chiều rộng tối đa</td><td>120 mm</td><td>60–350 mm</td></tr>
<tr><td>Cách trái</td><td>Khoảng cách từ mép trái</td><td>3 mm</td><td>0–50 mm</td></tr>
</table>
<p><b>Font:</b> Times New Roman / Arial, cỡ 8pt (5–18), giãn dòng 1.4 (0.8–3.0), Header Bold ✅</p>
<p><b>Chọn lớp hiển thị:</b></p>
<ul>
<li>☑ <b>Ô tích:</b> Chọn/bỏ chọn từng lớp</li>
<li>✏️ <b>Tên hiển thị:</b> Đổi tên lớp trong chú dẫn (để trống = giữ tên gốc)</li>
<li>🎨 <b>Chọn ký hiệu:</b> Chọn ký hiệu từ QGIS Style Manager</li>
<li>🔄 <b>Làm mới</b> | ☑ <b>Chọn tất cả</b> | ☐ <b>Bỏ chọn</b> | 🔍 <b>Tìm kiếm</b></li>
</ul>
<p style="color:#1B5E20">💡 <i>Bản đồ chuyên đề nhiều hạng mục: tăng số cột lên 3–4 và giảm cỡ ký hiệu.</i></p>

<h3>📌 Bước 4 — Tạo &amp; Xuất bản đồ</h3>
<ol>
<li>✅ Bấm <b>"Create Layout / Tạo khung"</b>.</li>
<li>🖥️ Khung bản đồ mở trong <b>QGIS Layout Designer</b>.</li>
<li>👀 Kiểm tra các thành phần — chỉnh sửa nếu cần.</li>
<li>💾 <b>File → Export as PDF</b> hoặc <b>Export as Image</b> (PNG/TIFF).</li>
</ol>

<h3>📄 Bảng khổ giấy tham khảo (ISO 216)</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px">
<tr style="background:#f5f5f5"><th>Khổ</th><th>Kích thước (mm)</th><th>Phù hợp cho</th></tr>
<tr style="background:#e3f2fd"><td colspan="3"><b>Dòng A</b></td></tr>
<tr><td>A5</td><td>210 × 148</td><td>Bản đồ bỏ túi, thực địa</td></tr>
<tr><td>A4</td><td>297 × 210</td><td>Báo cáo, tài liệu tham khảo</td></tr>
<tr><td>A3</td><td>420 × 297</td><td>Thực địa, trình bày</td></tr>
<tr><td>A2</td><td>594 × 420</td><td>Treo tường, quy hoạch</td></tr>
<tr><td>A1</td><td>841 × 594</td><td>Bản đồ treo tường lớn</td></tr>
<tr><td>A0</td><td>1189 × 841</td><td>Triển lãm, nộp hồ sơ chính thức</td></tr>
<tr style="background:#e8f5e9"><td colspan="3"><b>Dòng B</b> (lớn hơn A cùng số)</td></tr>
<tr><td>B5</td><td>250 × 176</td><td>Sách, bìa thư</td></tr>
<tr><td>B4</td><td>353 × 250</td><td>Báo, tập bản đồ</td></tr>
<tr><td>B3</td><td>500 × 353</td><td>Poster, biểu đồ lớn</td></tr>
<tr><td>B2</td><td>707 × 500</td><td>Poster lớn, bản đồ treo tường</td></tr>
<tr><td>B1</td><td>1000 × 707</td><td>Trưng bày cỡ rất lớn</td></tr>
<tr><td>B0</td><td>1414 × 1000</td><td>Triển lãm, bản vẽ kiến trúc</td></tr>
</table>

<p style="color:#888;font-size:10px;margin-top:12px"><i>LVT Map Layout v2.0 — Thiết kế cho bản đồ lâm nghiệp &amp; môi trường.</i></p>"""

    # ── CRS Library Text ─────────────────────────────────────────

    def _crs_text_en(self):
        return """
<h2>🌐 CRS Library — Coordinate Reference Systems for Vietnam</h2>
<p>Quick reference for EPSG codes commonly used in forestry & environmental mapping in Vietnam.</p>

<h3>🔵 Global / WGS 84</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e3f2fd"><th>EPSG</th><th>Name</th><th>Type</th><th>Description</th></tr>
<tr><td><b>4326</b></td><td>WGS 84</td><td>Geographic</td><td>Global standard (GPS). Lat/Lon in degrees. Used by Google Earth, OpenStreetMap.</td></tr>
<tr><td><b>32648</b></td><td>WGS 84 / UTM zone 48N</td><td>Projected</td><td>Western Vietnam (west of 108°E). Meters. Good for area calculations.</td></tr>
<tr><td><b>32649</b></td><td>WGS 84 / UTM zone 49N</td><td>Projected</td><td>Eastern Vietnam (east of 108°E). Meters.</td></tr>
</table>

<h3>🟢 VN-2000 — National 6° Zones (Toàn quốc)</h3>
<p>Used for national-scale mapping. Based on WGS 84 ellipsoid, Transverse Mercator projection.</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e8f5e9"><th>EPSG</th><th>Name</th><th>Central Meridian</th><th>Coverage</th></tr>
<tr><td><b>3405</b></td><td>VN-2000 / UTM zone 48N</td><td>105°E</td><td>Western Vietnam (west of 108°E)</td></tr>
<tr><td><b>3406</b></td><td>VN-2000 / UTM zone 49N</td><td>111°E</td><td>Eastern Vietnam (east of 108°E)</td></tr>
</table>

<h3>🟡 VN-2000 — Provincial 3° Zones (Tỉnh)</h3>
<p>Used for large-scale cadastral & engineering mapping. Each province is assigned a specific central meridian.</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th>CM</th><th>Province (New Name)</th><th>Merged From</th></tr>
<tr><td><b>103°00'</b></td><td>Điện Biên</td><td>—</td></tr>
<tr><td><b>104°00'</b></td><td>Sơn La</td><td>—</td></tr>
<tr><td><b>104°30'</b></td><td>Cà Mau</td><td>Bạc Liêu + Cà Mau</td></tr>
<tr><td rowspan="5"><b>104°45'</b></td><td>An Giang</td><td>Kiên Giang + An Giang</td></tr>
<tr><td>Lai Châu</td><td>—</td></tr>
<tr><td>Lào Cai</td><td>Lào Cai + Yên Bái</td></tr>
<tr><td>Nghệ An</td><td>—</td></tr>
<tr><td>Phú Thọ</td><td>Hòa Bình + Vĩnh Phúc + Phú Thọ</td></tr>
<tr><td rowspan="5"><b>105°00'</b></td><td>TP Hà Nội</td><td>—</td></tr>
<tr><td>Thanh Hóa</td><td>—</td></tr>
<tr><td>Ninh Bình</td><td>Hà Nam + Nam Định + Ninh Bình</td></tr>
<tr><td>Đồng Tháp</td><td>Tiền Giang + Đồng Tháp</td></tr>
<tr><td>TP Cần Thơ</td><td>Sóc Trăng + Hậu Giang + TP Cần Thơ</td></tr>
<tr><td rowspan="3"><b>105°30'</b></td><td>Hà Tĩnh</td><td>—</td></tr>
<tr><td>Hưng Yên</td><td>Hưng Yên + Thái Bình</td></tr>
<tr><td>Vĩnh Long</td><td>Bến Tre + Vĩnh Long + Trà Vinh</td></tr>
<tr><td rowspan="4"><b>105°45'</b></td><td>Cao Bằng</td><td>—</td></tr>
<tr><td>Tây Ninh</td><td>Tây Ninh + Long An</td></tr>
<tr><td>TP Hải Phòng</td><td>TP Hải Phòng + Hải Dương</td></tr>
<tr><td>TP HCM</td><td>Bà Rịa-VT + Bình Dương + TP HCM</td></tr>
<tr><td rowspan="2"><b>106°00'</b></td><td>Quảng Trị</td><td>Quảng Bình + Quảng Trị</td></tr>
<tr><td>Tuyên Quang</td><td>Hà Giang + Tuyên Quang</td></tr>
<tr><td><b>106°30'</b></td><td>Thái Nguyên</td><td>Thái Nguyên + Bắc Kạn</td></tr>
<tr><td rowspan="2"><b>107°00'</b></td><td>Bắc Ninh</td><td>Bắc Ninh + Bắc Giang</td></tr>
<tr><td>TP Huế</td><td>—</td></tr>
<tr><td><b>107°15'</b></td><td>Lạng Sơn</td><td>—</td></tr>
<tr><td rowspan="4"><b>107°45'</b></td><td>Quảng Ninh</td><td>—</td></tr>
<tr><td>Đồng Nai</td><td>Bình Phước + Đồng Nai</td></tr>
<tr><td>Lâm Đồng</td><td>Đắk Nông + Bình Thuận + Lâm Đồng</td></tr>
<tr><td>TP Đà Nẵng</td><td>Quảng Nam + TP Đà Nẵng</td></tr>
<tr><td><b>108°00'</b></td><td>Quảng Ngãi</td><td>Kon Tum + Quảng Ngãi</td></tr>
<tr><td rowspan="2"><b>108°15'</b></td><td>Gia Lai</td><td>Gia Lai + Bình Định</td></tr>
<tr><td>Khánh Hòa</td><td>Ninh Thuận + Khánh Hòa</td></tr>
<tr><td><b>108°30'</b></td><td>Đắk Lắk</td><td>Phú Yên + Đắk Lắk</td></tr>
</table>

<h3>💡 How to Choose?</h3>
<ul>
<li>📍 <b>GPS / web mapping:</b> EPSG:4326 (WGS 84)</li>
<li>📐 <b>Area/distance calculations:</b> EPSG:32648 or 32649 (UTM)</li>
<li>🗺️ <b>National maps:</b> EPSG:3405 or 3406 (VN-2000 / 6°)</li>
<li>📏 <b>Cadastral / provincial:</b> Use the 3° zone matching your province</li>
</ul>
<p style="color:#888;font-size:10px"><i>Source: EPSG Registry (epsg.io) — MONRE Vietnam</i></p>"""

    def _crs_text_vn(self):
        return """
<h2>🌐 Thư viện Hệ tọa độ — CRS cho Việt Nam</h2>
<p>Tra cứu nhanh mã EPSG thường dùng trong lâm nghiệp & môi trường tại Việt Nam.</p>

<h3>🔵 Toàn cầu / WGS 84</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e3f2fd"><th>EPSG</th><th>Tên</th><th>Loại</th><th>Mô tả</th></tr>
<tr><td><b>4326</b></td><td>WGS 84</td><td>Địa lý</td><td>Tiêu chuẩn toàn cầu (GPS). Tọa độ Lat/Lon tính bằng độ. Google Earth, OpenStreetMap.</td></tr>
<tr><td><b>32648</b></td><td>WGS 84 / UTM zone 48N</td><td>Phép chiếu</td><td>Tây Việt Nam (phía tây 108°E). Đơn vị: mét.</td></tr>
<tr><td><b>32649</b></td><td>WGS 84 / UTM zone 49N</td><td>Phép chiếu</td><td>Đông Việt Nam (phía đông 108°E). Đơn vị: mét.</td></tr>
</table>

<h3>🟢 VN-2000 — Múi 6° (Toàn quốc)</h3>
<p>Dùng cho bản đồ quy mô quốc gia. Dựa trên ellipsoid WGS 84, phép chiếu Transverse Mercator.</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e8f5e9"><th>EPSG</th><th>Tên</th><th>Kinh tuyến trục</th><th>Phạm vi</th></tr>
<tr><td><b>3405</b></td><td>VN-2000 / UTM zone 48N</td><td>105°E</td><td>Tây Việt Nam (phía tây 108°E)</td></tr>
<tr><td><b>3406</b></td><td>VN-2000 / UTM zone 49N</td><td>111°E</td><td>Đông Việt Nam (phía đông 108°E)</td></tr>
</table>

<h3>🟡 VN-2000 — Múi 3° (Tỉnh)</h3>
<p>Dùng cho bản đồ địa chính & kỹ thuật tỷ lệ lớn. Mỗi tỉnh được gán một kinh tuyến trục riêng.</p>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th>KTT</th><th>Tỉnh (tên mới)</th><th>Sáp nhập từ</th></tr>
<tr><td><b>103°00'</b></td><td>Điện Biên</td><td>—</td></tr>
<tr><td><b>104°00'</b></td><td>Sơn La</td><td>—</td></tr>
<tr><td><b>104°30'</b></td><td>Cà Mau</td><td>Bạc Liêu + Cà Mau</td></tr>
<tr><td rowspan="5"><b>104°45'</b></td><td>An Giang</td><td>Kiên Giang + An Giang</td></tr>
<tr><td>Lai Châu</td><td>—</td></tr>
<tr><td>Lào Cai</td><td>Lào Cai + Yên Bái</td></tr>
<tr><td>Nghệ An</td><td>—</td></tr>
<tr><td>Phú Thọ</td><td>Hòa Bình + Vĩnh Phúc + Phú Thọ</td></tr>
<tr><td rowspan="5"><b>105°00'</b></td><td>TP Hà Nội</td><td>—</td></tr>
<tr><td>Thanh Hóa</td><td>—</td></tr>
<tr><td>Ninh Bình</td><td>Hà Nam + Nam Định + Ninh Bình</td></tr>
<tr><td>Đồng Tháp</td><td>Tiền Giang + Đồng Tháp</td></tr>
<tr><td>TP Cần Thơ</td><td>Sóc Trăng + Hậu Giang + TP Cần Thơ</td></tr>
<tr><td rowspan="3"><b>105°30'</b></td><td>Hà Tĩnh</td><td>—</td></tr>
<tr><td>Hưng Yên</td><td>Hưng Yên + Thái Bình</td></tr>
<tr><td>Vĩnh Long</td><td>Bến Tre + Vĩnh Long + Trà Vinh</td></tr>
<tr><td rowspan="4"><b>105°45'</b></td><td>Cao Bằng</td><td>—</td></tr>
<tr><td>Tây Ninh</td><td>Tây Ninh + Long An</td></tr>
<tr><td>TP Hải Phòng</td><td>TP Hải Phòng + Hải Dương</td></tr>
<tr><td>TP HCM</td><td>Bà Rịa-VT + Bình Dương + TP HCM</td></tr>
<tr><td rowspan="2"><b>106°00'</b></td><td>Quảng Trị</td><td>Quảng Bình + Quảng Trị</td></tr>
<tr><td>Tuyên Quang</td><td>Hà Giang + Tuyên Quang</td></tr>
<tr><td><b>106°30'</b></td><td>Thái Nguyên</td><td>Thái Nguyên + Bắc Kạn</td></tr>
<tr><td rowspan="2"><b>107°00'</b></td><td>Bắc Ninh</td><td>Bắc Ninh + Bắc Giang</td></tr>
<tr><td>TP Huế</td><td>—</td></tr>
<tr><td><b>107°15'</b></td><td>Lạng Sơn</td><td>—</td></tr>
<tr><td rowspan="4"><b>107°45'</b></td><td>Quảng Ninh</td><td>—</td></tr>
<tr><td>Đồng Nai</td><td>Bình Phước + Đồng Nai</td></tr>
<tr><td>Lâm Đồng</td><td>Đắk Nông + Bình Thuận + Lâm Đồng</td></tr>
<tr><td>TP Đà Nẵng</td><td>Quảng Nam + TP Đà Nẵng</td></tr>
<tr><td><b>108°00'</b></td><td>Quảng Ngãi</td><td>Kon Tum + Quảng Ngãi</td></tr>
<tr><td rowspan="2"><b>108°15'</b></td><td>Gia Lai</td><td>Gia Lai + Bình Định</td></tr>
<tr><td>Khánh Hòa</td><td>Ninh Thuận + Khánh Hòa</td></tr>
<tr><td><b>108°30'</b></td><td>Đắk Lắk</td><td>Phú Yên + Đắk Lắk</td></tr>
</table>

<h3>💡 Chọn hệ tọa độ nào?</h3>
<ul>
<li>📍 <b>GPS / bản đồ web:</b> EPSG:4326 (WGS 84)</li>
<li>📐 <b>Tính diện tích / khoảng cách:</b> EPSG:32648 hoặc 32649 (UTM)</li>
<li>🗺️ <b>Bản đồ toàn quốc:</b> EPSG:3405 hoặc 3406 (VN-2000 / 6°)</li>
<li>📏 <b>Địa chính / cấp tỉnh:</b> Tra múi 3° theo tỉnh ở bảng trên</li>
</ul>
<p style="color:#888;font-size:10px"><i>Nguồn: EPSG Registry (epsg.io) — Bộ TN&MT Việt Nam</i></p>"""

    def _crs_legacy_63(self):
        """HTML table of legacy 63 provinces with EPSG Authority IDs."""
        return """
<h2>📋 Danh sách 63 Tỉnh Thành Cũ — Kinh tuyến trục & Authority ID</h2>
<p style="color:#555"><i>Dữ liệu tham khảo (trước sáp nhập). Nguồn: Bộ TN&MT.</i></p>

<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#f5f5f5"><th>STT</th><th>Mã</th><th>Tỉnh/TP</th><th>KTT</th><th>Authority ID</th></tr>
<tr><td>1</td><td>DC01</td><td>An Giang</td><td>104°45'</td><td>EPSG:9208</td></tr>
<tr><td>2</td><td>DC02</td><td>Bà Rịa-Vũng Tàu</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>3</td><td>DC03</td><td>Bạc Liêu</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>4</td><td>DC04</td><td>Bắc Giang</td><td>107°00'</td><td>EPSG:9214</td></tr>
<tr><td>5</td><td>DC05</td><td>Bắc Kạn</td><td>106°30'</td><td>EPSG:9213</td></tr>
<tr><td>6</td><td>DC06</td><td>Bắc Ninh</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>7</td><td>DC07</td><td>Bến Tre</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>8</td><td>DC08</td><td>Bình Dương</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>9</td><td>DC09</td><td>Bình Định</td><td>108°15'</td><td>EPSG:9217</td></tr>
<tr><td>10</td><td>DC10</td><td>Bình Phước</td><td>106°15'</td><td>EPSG:9212</td></tr>
<tr><td>11</td><td>DC11</td><td>Bình Thuận</td><td>108°30'</td><td>EPSG:9218</td></tr>
<tr><td>12</td><td>DC12</td><td>Cà Mau</td><td>104°30'</td><td>EPSG:9207</td></tr>
<tr><td>13</td><td>DC13</td><td>Cao Bằng</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>14</td><td>DC14</td><td>Cần Thơ</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>15</td><td>DC15</td><td>Đà Nẵng</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>16</td><td>DC16</td><td>Đắk Lắk</td><td>108°30'</td><td>EPSG:9218</td></tr>
<tr><td>17</td><td>DC17</td><td>Đắk Nông</td><td>108°30'</td><td>EPSG:9218</td></tr>
<tr><td>18</td><td>DC18</td><td>Điện Biên</td><td>103°00'</td><td>EPSG:9205</td></tr>
<tr><td>19</td><td>DC19</td><td>Đồng Nai</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>20</td><td>DC20</td><td>Đồng Tháp</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>21</td><td>DC21</td><td>Gia Lai</td><td>108°30'</td><td>EPSG:9218</td></tr>
<tr><td>22</td><td>DC22</td><td>Hà Giang</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>23</td><td>DC23</td><td>Hà Nam</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>24</td><td>DC24</td><td>Hà Nội</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>25</td><td>DC25</td><td>Hà Tĩnh</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>26</td><td>DC26</td><td>Hải Dương</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>27</td><td>DC27</td><td>Hải Phòng</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>28</td><td>DC28</td><td>Hậu Giang</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>29</td><td>DC29</td><td>Hòa Bình</td><td>106°00'</td><td>EPSG:9211</td></tr>
<tr><td>30</td><td>DC30</td><td>TP HCM</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>31</td><td>DC31</td><td>Hưng Yên</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>32</td><td>DC32</td><td>Khánh Hòa</td><td>108°15'</td><td>EPSG:9217</td></tr>
<tr><td>33</td><td>DC33</td><td>Kiên Giang</td><td>104°30'</td><td>EPSG:9207</td></tr>
<tr><td>34</td><td>DC34</td><td>Kon Tum</td><td>107°30'</td><td>EPSG:9216</td></tr>
<tr><td>35</td><td>DC35</td><td>Lai Châu</td><td>103°00'</td><td>EPSG:9205</td></tr>
<tr><td>36</td><td>DC36</td><td>Lạng Sơn</td><td>107°15'</td><td>EPSG:9215</td></tr>
<tr><td>37</td><td>DC37</td><td>Lào Cai</td><td>104°45'</td><td>EPSG:9208</td></tr>
<tr><td>38</td><td>DC38</td><td>Lâm Đồng</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>39</td><td>DC39</td><td>Long An</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>40</td><td>DC40</td><td>Nam Định</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>41</td><td>DC41</td><td>Nghệ An</td><td>104°45'</td><td>EPSG:9208</td></tr>
<tr><td>42</td><td>DC42</td><td>Ninh Bình</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>43</td><td>DC43</td><td>Ninh Thuận</td><td>108°15'</td><td>EPSG:9217</td></tr>
<tr><td>44</td><td>DC44</td><td>Phú Thọ</td><td>104°45'</td><td>EPSG:9208</td></tr>
<tr><td>45</td><td>DC45</td><td>Phú Yên</td><td>108°30'</td><td>EPSG:9218</td></tr>
<tr><td>46</td><td>DC46</td><td>Quảng Bình</td><td>106°00'</td><td>EPSG:9211</td></tr>
<tr><td>47</td><td>DC47</td><td>Quảng Nam</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>48</td><td>DC48</td><td>Quảng Ngãi</td><td>108°00'</td><td>EPSG:9216</td></tr>
<tr><td>49</td><td>DC49</td><td>Quảng Ninh</td><td>107°45'</td><td>EPSG:5899</td></tr>
<tr><td>50</td><td>DC50</td><td>Quảng Trị</td><td>106°15'</td><td>EPSG:9212</td></tr>
<tr><td>51</td><td>DC51</td><td>Sóc Trăng</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>52</td><td>DC52</td><td>Sơn La</td><td>104°00'</td><td>EPSG:9206</td></tr>
<tr><td>53</td><td>DC53</td><td>Tây Ninh</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>54</td><td>DC54</td><td>Thái Bình</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>55</td><td>DC55</td><td>Thái Nguyên</td><td>106°30'</td><td>EPSG:9213</td></tr>
<tr><td>56</td><td>DC56</td><td>Thanh Hóa</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>57</td><td>DC57</td><td>Thừa Thiên Huế</td><td>107°00'</td><td>EPSG:9214</td></tr>
<tr><td>58</td><td>DC58</td><td>Tiền Giang</td><td>105°45'</td><td>EPSG:9210</td></tr>
<tr><td>59</td><td>DC59</td><td>Trà Vinh</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>60</td><td>DC60</td><td>Tuyên Quang</td><td>106°00'</td><td>EPSG:9211</td></tr>
<tr><td>61</td><td>DC61</td><td>Vĩnh Long</td><td>105°30'</td><td>EPSG:9209</td></tr>
<tr><td>62</td><td>DC62</td><td>Vĩnh Phúc</td><td>105°00'</td><td>EPSG:5896</td></tr>
<tr><td>63</td><td>DC63</td><td>Yên Bái</td><td>104°45'</td><td>EPSG:9208</td></tr>
</table>

<p style="color:#888;font-size:10px"><i>⚠️ Dữ liệu tham khảo — Trước sáp nhập hành chính 2026. Nguồn: Bộ TN&MT.</i></p>
"""

    # ── Nội bộ (3 tham số) ────────────────────────────────────────

    def _crs_noibotext(self):
        return """
<h2>🔒 VN-2000 Nội bộ — 3 tham số (datum WGS84)</h2>
<p style="color:#555"><i>Hệ tọa độ nội bộ phổ biến trong dữ liệu MapInfo bản đồ Việt Nam.<br>
Khác hệ 7 tham số: <b>KHÔNG có phép chuyển đổi towgs84</b>. Proj4 dùng <code>+datum=WGS84</code>.<br>
⚠️ Không có EPSG code chính thức — sử dụng Proj4 string tùy chỉnh.</i></p>

<h3>🏛️ 63 Tỉnh Cũ — Nội bộ 3 tham số</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e8eaf6"><th>STT</th><th>Tỉnh/TP</th><th>KTT</th><th>Ghi chú</th></tr>
<tr><td>1</td><td>NB An Giang</td><td><b>104°45'</b></td><td>+proj=tmerc +lon_0=104.75 +datum=WGS84</td></tr>
<tr><td>2</td><td>NB Bà Rịa-VT</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>3</td><td>NB Bắc Giang</td><td><b>107°00'</b></td><td>+proj=tmerc +lon_0=107 +datum=WGS84</td></tr>
<tr><td>4</td><td>NB Bắc Kạn</td><td><b>106°30'</b></td><td>+proj=tmerc +lon_0=106.5 +datum=WGS84</td></tr>
<tr><td>5</td><td>NB Bạc Liêu</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>6</td><td>NB Bắc Ninh</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>7</td><td>NB Bến Tre</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>8</td><td>NB Bình Định</td><td><b>108°15'</b></td><td>+proj=tmerc +lon_0=108.25 +datum=WGS84</td></tr>
<tr><td>9</td><td>NB Bình Dương</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>10</td><td>NB Bình Phước</td><td><b>106°15'</b></td><td>+proj=tmerc +lon_0=106.25 +datum=WGS84</td></tr>
<tr><td>11</td><td>NB Bình Thuận</td><td><b>108°30'</b></td><td>+proj=tmerc +lon_0=108.5 +datum=WGS84</td></tr>
<tr><td>12</td><td>NB Cà Mau</td><td><b>104°30'</b></td><td>+proj=tmerc +lon_0=104.5 +datum=WGS84</td></tr>
<tr><td>13</td><td>NB Cần Thơ</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>14</td><td>NB Cao Bằng</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>15</td><td>NB Đà Nẵng</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>16</td><td>NB Đắk Lắk</td><td><b>108°30'</b></td><td>+proj=tmerc +lon_0=108.5 +datum=WGS84</td></tr>
<tr><td>17</td><td>NB Đắk Nông</td><td><b>108°30'</b></td><td>+proj=tmerc +lon_0=108.5 +datum=WGS84</td></tr>
<tr><td>18</td><td>NB Điện Biên</td><td><b>103°00'</b></td><td>+proj=tmerc +lon_0=103 +datum=WGS84</td></tr>
<tr><td>19</td><td>NB Đồng Nai</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>20</td><td>NB Đồng Tháp</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>21</td><td>NB Gia Lai</td><td><b>108°15'</b></td><td>+proj=tmerc +lon_0=108.25 +datum=WGS84</td></tr>
<tr><td>22</td><td>NB Hà Giang</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>23</td><td>NB Hải Dương</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>24</td><td>NB Hải Phòng</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>25</td><td>NB Hà Nam</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>26</td><td>NB Hà Nội</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>27</td><td>NB Hà Tĩnh</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>28</td><td>NB Hậu Giang</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>29</td><td>NB Hòa Bình</td><td><b>106°00'</b></td><td>+proj=tmerc +lon_0=106 +datum=WGS84</td></tr>
<tr><td>30</td><td>NB Hưng Yên</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>31</td><td>NB Khánh Hòa</td><td><b>108°15'</b></td><td>+proj=tmerc +lon_0=108.25 +datum=WGS84</td></tr>
<tr><td>32</td><td>NB Kiên Giang</td><td><b>104°30'</b></td><td>+proj=tmerc +lon_0=104.5 +datum=WGS84</td></tr>
<tr><td>33</td><td>NB Kon Tum</td><td><b>107°30'</b></td><td>+proj=tmerc +lon_0=107.5 +datum=WGS84</td></tr>
<tr><td>34</td><td>NB Lai Châu</td><td><b>103°00'</b></td><td>+proj=tmerc +lon_0=103 +datum=WGS84</td></tr>
<tr><td>35</td><td>NB Lâm Đồng</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>36</td><td>NB Lạng Sơn</td><td><b>107°15'</b></td><td>+proj=tmerc +lon_0=107.25 +datum=WGS84</td></tr>
<tr><td>37</td><td>NB Lào Cai</td><td><b>104°45'</b></td><td>+proj=tmerc +lon_0=104.75 +datum=WGS84</td></tr>
<tr><td>38</td><td>NB Long An</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>39</td><td>NB Nam Định</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>40</td><td>NB Nghệ An</td><td><b>104°45'</b></td><td>+proj=tmerc +lon_0=104.75 +datum=WGS84</td></tr>
<tr><td>41</td><td>NB Ninh Bình</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>42</td><td>NB Ninh Thuận</td><td><b>108°15'</b></td><td>+proj=tmerc +lon_0=108.25 +datum=WGS84</td></tr>
<tr><td>43</td><td>NB Phú Thọ</td><td><b>104°45'</b></td><td>+proj=tmerc +lon_0=104.75 +datum=WGS84</td></tr>
<tr><td>44</td><td>NB Phú Yên</td><td><b>108°30'</b></td><td>+proj=tmerc +lon_0=108.5 +datum=WGS84</td></tr>
<tr><td>45</td><td>NB Quảng Bình</td><td><b>106°00'</b></td><td>+proj=tmerc +lon_0=106 +datum=WGS84</td></tr>
<tr><td>46</td><td>NB Quảng Nam</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>47</td><td>NB Quảng Ngãi</td><td><b>108°00'</b></td><td>+proj=tmerc +lon_0=108 +datum=WGS84</td></tr>
<tr><td>48</td><td>NB Quảng Ninh</td><td><b>107°45'</b></td><td>+proj=tmerc +lon_0=107.75 +datum=WGS84</td></tr>
<tr><td>49</td><td>NB Quảng Trị</td><td><b>106°15'</b></td><td>+proj=tmerc +lon_0=106.25 +datum=WGS84</td></tr>
<tr><td>50</td><td>NB Sóc Trăng</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>51</td><td>NB Sơn La</td><td><b>104°00'</b></td><td>+proj=tmerc +lon_0=104 +datum=WGS84</td></tr>
<tr><td>52</td><td>NB Tây Ninh</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>53</td><td>NB Thái Bình</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>54</td><td>NB Thái Nguyên</td><td><b>106°30'</b></td><td>+proj=tmerc +lon_0=106.5 +datum=WGS84</td></tr>
<tr><td>55</td><td>NB Thanh Hóa</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>56</td><td>NB Thừa Thiên Huế</td><td><b>107°00'</b></td><td>+proj=tmerc +lon_0=107 +datum=WGS84</td></tr>
<tr><td>57</td><td>NB Tiền Giang</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>58</td><td>NB TP HCM</td><td><b>105°45'</b></td><td>+proj=tmerc +lon_0=105.75 +datum=WGS84</td></tr>
<tr><td>59</td><td>NB Trà Vinh</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>60</td><td>NB Tuyên Quang</td><td><b>106°00'</b></td><td>+proj=tmerc +lon_0=106 +datum=WGS84</td></tr>
<tr><td>61</td><td>NB Vĩnh Long</td><td><b>105°30'</b></td><td>+proj=tmerc +lon_0=105.5 +datum=WGS84</td></tr>
<tr><td>62</td><td>NB Vĩnh Phúc</td><td><b>105°00'</b></td><td>+proj=tmerc +lon_0=105 +datum=WGS84</td></tr>
<tr><td>63</td><td>NB Yên Bái</td><td><b>104°45'</b></td><td>+proj=tmerc +lon_0=104.75 +datum=WGS84</td></tr>
</table>

<h3 style="margin-top:16px">🆕 34 Tỉnh Mới — Nội bộ 3 tham số (sau sáp nhập 2025)</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e8eaf6"><th>STT</th><th>Tỉnh/TP (mới)</th><th>KTT</th><th>Sáp nhập từ</th></tr>
<tr><td>1</td><td>NB TP Hà Nội</td><td><b>105°00'</b></td><td>—</td></tr>
<tr><td>2</td><td>NB TP Hải Phòng</td><td><b>105°45'</b></td><td>Hải Dương</td></tr>
<tr><td>3</td><td>NB TP Đà Nẵng</td><td><b>107°45'</b></td><td>Quảng Nam</td></tr>
<tr><td>4</td><td>NB TP HCM</td><td><b>105°45'</b></td><td>—</td></tr>
<tr><td>5</td><td>NB TP Huế</td><td><b>107°00'</b></td><td>—</td></tr>
<tr><td>6</td><td>NB TP Cần Thơ</td><td><b>105°00'</b></td><td>Hậu Giang, Sóc Trăng</td></tr>
<tr><td>7</td><td>NB An Giang</td><td><b>104°45'</b></td><td>Đồng Tháp</td></tr>
<tr><td>8</td><td>NB Bắc Ninh</td><td><b>107°00'</b></td><td>Bắc Giang</td></tr>
<tr><td>9</td><td>NB Cà Mau</td><td><b>104°30'</b></td><td>Bạc Liêu, Kiên Giang</td></tr>
<tr><td>10</td><td>NB Cao Bằng</td><td><b>105°45'</b></td><td>—</td></tr>
<tr><td>11</td><td>NB Đắk Lắk</td><td><b>108°30'</b></td><td>Phú Yên</td></tr>
<tr><td>12</td><td>NB Điện Biên</td><td><b>103°00'</b></td><td>—</td></tr>
<tr><td>13</td><td>NB Đồng Nai</td><td><b>107°45'</b></td><td>BRVT, Bình Dương</td></tr>
<tr><td>14</td><td>NB Gia Lai</td><td><b>108°15'</b></td><td>Bình Định</td></tr>
<tr><td>15</td><td>NB Hà Tĩnh</td><td><b>105°30'</b></td><td>—</td></tr>
<tr><td>16</td><td>NB Hưng Yên</td><td><b>105°30'</b></td><td>Thái Bình</td></tr>
<tr><td>17</td><td>NB Khánh Hòa</td><td><b>108°15'</b></td><td>Ninh Thuận</td></tr>
<tr><td>18</td><td>NB Lai Châu</td><td><b>103°00'</b></td><td>—</td></tr>
<tr><td>19</td><td>NB Lâm Đồng</td><td><b>107°45'</b></td><td>Đắk Nông, Bình Thuận</td></tr>
<tr><td>20</td><td>NB Lạng Sơn</td><td><b>107°15'</b></td><td>—</td></tr>
<tr><td>21</td><td>NB Lào Cai</td><td><b>104°45'</b></td><td>Yên Bái</td></tr>
<tr><td>22</td><td>NB Long An</td><td><b>105°45'</b></td><td>Bến Tre, Tiền Giang</td></tr>
<tr><td>23</td><td>NB Nghệ An</td><td><b>104°45'</b></td><td>—</td></tr>
<tr><td>24</td><td>NB Ninh Bình</td><td><b>105°00'</b></td><td>Hà Nam, Nam Định</td></tr>
<tr><td>25</td><td>NB Phú Thọ</td><td><b>104°45'</b></td><td>Hòa Bình, Vĩnh Phúc</td></tr>
<tr><td>26</td><td>NB Quảng Ngãi</td><td><b>108°00'</b></td><td>Kon Tum</td></tr>
<tr><td>27</td><td>NB Quảng Ninh</td><td><b>107°45'</b></td><td>—</td></tr>
<tr><td>28</td><td>NB Quảng Trị</td><td><b>106°15'</b></td><td>Quảng Bình</td></tr>
<tr><td>29</td><td>NB Sơn La</td><td><b>104°00'</b></td><td>—</td></tr>
<tr><td>30</td><td>NB Tây Ninh</td><td><b>105°45'</b></td><td>Bình Phước, Trà Vinh</td></tr>
<tr><td>31</td><td>NB Thái Nguyên</td><td><b>106°30'</b></td><td>Bắc Kạn</td></tr>
<tr><td>32</td><td>NB Thanh Hóa</td><td><b>105°00'</b></td><td>—</td></tr>
<tr><td>33</td><td>NB Tuyên Quang</td><td><b>106°00'</b></td><td>Hà Giang</td></tr>
<tr><td>34</td><td>NB Vĩnh Long</td><td><b>105°30'</b></td><td>Trà Vinh</td></tr>
</table>
<p style="color:#888;font-size:10px"><i>⚠️ Nội bộ 3 tham số KHÔNG có EPSG code chính thức. Plugin tự tạo CRS từ Proj4 string.</i></p>"""

    # ── HN-72 (Hà Nội 1972) ─────────────────────────────────────

    def _crs_hn72_text(self):
        return """
<h2>📐 HN-72 — Hà Nội 1972 (Gauss-Kruger)</h2>
<p style="color:#555"><i>Hệ tọa độ Việt Nam trước VN-2000. Ellipsoid: <b>Krassowsky 1940</b>.<br>
Phép chiếu: <b>Gauss-Kruger</b> (tương tự UTM nhưng k=1).<br>
Tham số chuyển đổi: <code>towgs84 = -17.51, -108.32, -62.39, 0, 0, 0, 0</code><br>
⚠️ Không có EPSG code chính thức — sử dụng Proj4 string tùy chỉnh.</i></p>

<h3>🟤 HN-72 Gauss-Kruger — Múi 6°</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#efebe9"><th>Tên</th><th>CM</th><th>Phạm vi</th><th>Proj4</th></tr>
<tr><td><b>HN-72 / GK zone 18</b></td><td>105°E</td><td>Tây Việt Nam</td><td><code>+lon_0=105 +k=1 +ellps=krass</code></td></tr>
<tr><td><b>HN-72 / GK zone 19</b></td><td>111°E</td><td>Đông Việt Nam</td><td><code>+lon_0=111 +k=1 +ellps=krass</code></td></tr>
</table>

<h3>🟤 HN-72 — Múi 3° (theo kinh tuyến trục)</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#efebe9"><th>STT</th><th>Kinh tuyến trục</th><th>Proj4 lon_0</th></tr>
<tr><td>1</td><td><b>103°00'</b></td><td>+lon_0=103</td></tr>
<tr><td>2</td><td><b>104°00'</b></td><td>+lon_0=104</td></tr>
<tr><td>3</td><td><b>104°30'</b></td><td>+lon_0=104.5</td></tr>
<tr><td>4</td><td><b>104°45'</b></td><td>+lon_0=104.75</td></tr>
<tr><td>5</td><td><b>105°00'</b></td><td>+lon_0=105</td></tr>
<tr><td>6</td><td><b>105°30'</b></td><td>+lon_0=105.5</td></tr>
<tr><td>7</td><td><b>105°45'</b></td><td>+lon_0=105.75</td></tr>
<tr><td>8</td><td><b>106°00'</b></td><td>+lon_0=106</td></tr>
<tr><td>9</td><td><b>106°15'</b></td><td>+lon_0=106.25</td></tr>
<tr><td>10</td><td><b>106°30'</b></td><td>+lon_0=106.5</td></tr>
<tr><td>11</td><td><b>107°00'</b></td><td>+lon_0=107</td></tr>
<tr><td>12</td><td><b>107°15'</b></td><td>+lon_0=107.25</td></tr>
<tr><td>13</td><td><b>107°30'</b></td><td>+lon_0=107.5</td></tr>
<tr><td>14</td><td><b>107°45'</b></td><td>+lon_0=107.75</td></tr>
<tr><td>15</td><td><b>108°00'</b></td><td>+lon_0=108</td></tr>
<tr><td>16</td><td><b>108°15'</b></td><td>+lon_0=108.25</td></tr>
<tr><td>17</td><td><b>108°30'</b></td><td>+lon_0=108.5</td></tr>
</table>

<h3>📝 Proj4 template đầy đủ</h3>
<p style="background:#f5f5f5;padding:8px;border-radius:4px;font-family:monospace;font-size:10px">
+proj=tmerc +lat_0=0 +lon_0=<b>{CM}</b> +k=1 +x_0=500000 +y_0=0<br>
+ellps=krass +towgs84=-17.51,-108.32,-62.39,0,0,0,0 +units=m +no_defs
</p>

<p style="color:#888;font-size:10px"><i>Nguồn: Cục Đo đạc và Bản đồ Việt Nam. HN-72 dần bị thay thế bởi VN-2000 từ năm 2000.</i></p>"""

    # ── Indian 1975 & Quốc tế ───────────────────────────────────

    def _crs_indian_intl_text(self):
        return """
<h2>🌏 Indian 1975 / Thailand & CRS Quốc tế</h2>

<h3>🔶 Indian 1975 / Thailand — Everest 1830</h3>
<p style="color:#555"><i>Dùng trong dự án lâm nghiệp khu vực Đông Nam Á (Lào, Campuchia, Thái Lan).<br>
Ellipsoid: <b>Everest 1830</b>. Tham số: <code>towgs84 = 210.0, 814.0, 289.0</code></i></p>

<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff3e0"><th>EPSG</th><th>Tên</th><th>CM</th><th>Phạm vi</th></tr>
<tr><td><b>EPSG:24047</b></td><td>Indian 1975 / UTM zone 47N</td><td>99°E</td><td>Thái Lan (Tây), Myanmar</td></tr>
<tr><td><b>EPSG:24048</b></td><td>Indian 1975 / UTM zone 48N</td><td>105°E</td><td>Thái Lan (Đông), Lào, Campuchia</td></tr>
<tr><td colspan="4" style="background:#fff8e1"><b>Tùy chỉnh (Proj4):</b></td></tr>
<tr><td>—</td><td>Indian 1975 / TM CM 99°</td><td>99°E</td><td>+lon_0=99 +ellps=evrst30</td></tr>
<tr><td>—</td><td>Indian 1975 / TM CM 102°</td><td>102°E</td><td>+lon_0=102 +ellps=evrst30</td></tr>
<tr><td>—</td><td>Indian 1975 / TM CM 105°</td><td>105°E</td><td>+lon_0=105 +ellps=evrst30</td></tr>
</table>

<h3>📝 Proj4 template Indian 1975</h3>
<p style="background:#f5f5f5;padding:8px;border-radius:4px;font-family:monospace;font-size:10px">
+proj=tmerc +lat_0=0 +lon_0=<b>{CM}</b> +k=0.9996 +x_0=500000 +y_0=0<br>
+ellps=evrst30 +towgs84=210.0,814.0,289.0,0,0,0,0 +units=m +no_defs
</p>

<h3 style="margin-top:20px">🌐 CRS Quốc tế phổ biến</h3>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e3f2fd"><th>EPSG</th><th>Tên</th><th>Loại</th><th>Khu vực / Mô tả</th></tr>
<tr><td><b>EPSG:4326</b></td><td>WGS 84</td><td>Geographic</td><td>Toàn cầu — GPS, Google Earth, OpenStreetMap</td></tr>
<tr><td><b>EPSG:3857</b></td><td>WGS 84 / Pseudo-Mercator</td><td>Projected</td><td>Google Maps, Bing Maps, bản đồ web tile</td></tr>
<tr><td><b>EPSG:4756</b></td><td>VN-2000</td><td>Geographic</td><td>Việt Nam — hệ tọa độ quốc gia</td></tr>
<tr><td><b>EPSG:32647</b></td><td>WGS 84 / UTM zone 47N</td><td>Projected</td><td>Tây Đông Nam Á (96°E–102°E)</td></tr>
<tr><td><b>EPSG:32648</b></td><td>WGS 84 / UTM zone 48N</td><td>Projected</td><td>Tây Việt Nam (102°E–108°E)</td></tr>
<tr><td><b>EPSG:32649</b></td><td>WGS 84 / UTM zone 49N</td><td>Projected</td><td>Đông Việt Nam (108°E–114°E)</td></tr>
<tr><td><b>EPSG:4269</b></td><td>NAD83</td><td>Geographic</td><td>Bắc Mỹ (USA, Canada, Mexico)</td></tr>
<tr><td><b>EPSG:4230</b></td><td>ED50</td><td>Geographic</td><td>Châu Âu (European Datum 1950)</td></tr>
<tr><td><b>EPSG:4612</b></td><td>JGD2000</td><td>Geographic</td><td>Nhật Bản (Japanese Geodetic Datum 2000)</td></tr>
<tr><td><b>EPSG:4490</b></td><td>CGCS2000</td><td>Geographic</td><td>Trung Quốc (China Geodetic Coordinate System)</td></tr>
</table>

<p style="color:#888;font-size:10px;margin-top:12px"><i>Nguồn: EPSG Registry (epsg.io) — OGP Geomatics Committee</i></p>"""

    # ── CRS Guide Text ───────────────────────────────────────────

    def _crs_guide_en(self):
        return """
<h2>🌐 CRS Control & Conversion Guide</h2>
<p style="color:#555"><i>Training guide for GPS, QGIS, MapInfo, ArcGIS, Google Earth & Smartphone</i></p>

<h3 style="background:#e3f2fd;padding:6px;border-radius:4px">
📘 Part 1 — Understanding CRS Basics</h3>

<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e3f2fd"><th width="30%">System</th><th>Key Info</th></tr>
<tr>
<td>🌍 <b>WGS 84</b><br>EPSG: <b>4326</b></td>
<td>
• <b>Unit:</b> Degrees (or D°M'S")<br>
• <b>Type:</b> Global geographic CRS<br>
• 📱 <b>Required for:</b> Google Earth, Smartphone apps, GPS devices<br>
• ⚠️ Cannot measure area/distance accurately (not projected)
</td>
</tr>
<tr>
<td>🇻🇳 <b>VN-2000</b><br>(Projected, meters)</td>
<td>
• <b>Unit:</b> Meters — ideal for area/distance<br>
• Each province has its own <b>Central Meridian</b> and <b>EPSG code</b><br>
• Example: Thanh Hóa = 5897, Quảng Nam = 5899, Quảng Trị = 9213
</td>
</tr>
</table>

<h4>⚡ Integration vs Internal System (Critical for GPS users!)</h4>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th width="35%">Type</th><th>When to use</th></tr>
<tr>
<td>✅ <b>Integration</b><br>(Hệ hội nhập)</td>
<td>Has full 7 parameters (e.g. -192, -39, -111).<br>
→ Map <b>aligns perfectly</b> with satellite basemap on screen.</td>
</tr>
<tr>
<td>⚠️ <b>Internal</b><br>(Hệ nội bộ)</td>
<td>No shift parameters. Use when GPS device is <b>not configured</b> with 7 params.<br>
→ Printed map coords match GPS readout exactly (even if offset from satellite).</td>
</tr>
</table>

<hr>
<h3 style="background:#e8f5e9;padding:6px;border-radius:4px">
🔧 Part 2 — Fixing Misaligned Maps</h3>
<p>When your map is offset from the basemap or "floating in the ocean":</p>

<table border="0" cellpadding="4" style="font-size:11px;width:100%">
<tr><td style="background:#e3f2fd;border-radius:4px;padding:8px">
<b>🔍 Step 1 — Identify the original CRS</b><br>
• Open Basemap (Google Satellite) to compare rivers, roads<br>
• Check coordinates: if values like <code>107.75</code> → <code>0.75 × 60 = 45'</code> → Central meridian = <b>107°45'</b><br>
• Look up the province in the CRS table
</td></tr>
<tr><td style="background:#fff9c4;border-radius:4px;padding:8px">
<b>⚙️ Step 2 — Define / Set CRS (NOT Export!)</b><br>
• ❌ Do <b>NOT</b> use Export/Save As yet!<br>
• ✅ Use <b>"Set Layer CRS"</b> to assign the correct EPSG (e.g. 5899)<br>
• Map will <b>snap to correct position</b> immediately
</td></tr>
<tr><td style="background:#e8f5e9;border-radius:4px;padding:8px">
<b>📤 Step 3 — Reproject (Export)</b><br>
• Only after the map is in the right place<br>
• Export → Save As → choose target CRS (e.g. WGS84 / 4326)
</td></tr>
</table>

<hr>
<h3 style="background:#fce4ec;padding:6px;border-radius:4px">
📱 Part 3 — Export to Smartphone & Google Earth</h3>

<table border="0" cellpadding="4" style="font-size:11px;width:100%">
<tr><td style="background:#f3e5f5;border-radius:4px;padding:8px">
<b>1️⃣ Fix fonts:</b> Convert TCVN3 → Unicode in Attribute Table
</td></tr>
<tr><td style="background:#e8eaf6;border-radius:4px;padding:8px">
<b>2️⃣ Install Plugin:</b> "KML Tools" in QGIS
</td></tr>
<tr><td style="background:#e3f2fd;border-radius:4px;padding:8px">
<b>3️⃣ Export KMZ:</b> Select only essential fields (Owner, Location, Land type, Plot)<br>
❌ Don't export all columns → file too heavy
</td></tr>
<tr><td style="background:#e8f5e9;border-radius:4px;padding:8px">
<b>4️⃣ Share:</b> Send .kmz via Zalo → Open on phone with Google Earth
</td></tr>
</table>

<hr>
<h3 style="background:#fff3e0;padding:6px;border-radius:4px">
🔢 Part 4 — DMS ↔ Decimal Conversion</h3>

<h4>📐 Manual Formula</h4>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-size:12px;width:100%;background:#fffde7">
<tr><td>
<b>DMS → Decimal:</b>&nbsp;&nbsp; <code style="font-size:14px;background:#fff9c4;padding:4px 8px;border-radius:3px">Decimal = Degrees + Minutes/60 + Seconds/3600</code><br><br>
<b>Example:</b> 105°27'35"<br>
→ 105 + 27/60 + 35/3600 = 105 + 0.45 + 0.00972 = <b style="color:#d32f2f;font-size:13px">105.45972</b>
</td></tr>
<tr><td>
<b>Decimal → DMS:</b><br>
• Degrees = integer part: <b>105</b><br>
• Minutes = (0.45972 × 60) = 27.583 → <b>27'</b><br>
• Seconds = (0.583 × 60) = 34.99 → <b>35"</b><br>
→ Result: <b style="color:#1565c0;font-size:13px">105°27'35"</b>
</td></tr>
</table>

<h4>🖥️ QGIS Formulas (Field Calculator)</h4>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th>Direction</th><th>QGIS Formula</th><th>Output Type</th></tr>
<tr>
<td>DMS → Decimal</td>
<td><code>dms_to_degree("column_name")</code></td>
<td>Decimal / Real</td>
</tr>
<tr>
<td>Decimal → DMS</td>
<td><code>to_dms(X)</code> or<br><code>to_dms(transform($geometry, 'EPSG:3405', 'EPSG:4326'))</code></td>
<td>Text / String</td>
</tr>
</table>
<p>💡 Use <b>Field Calculator</b> in Attribute Table to batch-convert entire columns.</p>
"""

    def _crs_guide_vn(self):
        return """
<h2>🌐 Hướng dẫn Kiểm soát & Chuyển đổi Hệ tọa độ</h2>
<p style="color:#555"><i>Tài liệu đào tạo cho GPS, QGIS, MapInfo, ArcGIS, Google Earth & Smartphone</i></p>

<h3 style="background:#e3f2fd;padding:6px;border-radius:4px">
📘 Phần 1 — Hiểu đúng về CRS cơ bản</h3>

<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#e3f2fd"><th width="30%">Hệ tọa độ</th><th>Thông tin chính</th></tr>
<tr>
<td>🌍 <b>WGS 84</b><br>EPSG: <b>4326</b></td>
<td>
• <b>Đơn vị:</b> Độ (hoặc Độ Phút Giây)<br>
• <b>Loại:</b> Hệ tọa độ địa lý toàn cầu<br>
• 📱 <b>Bắt buộc cho:</b> Google Earth, App điện thoại, máy GPS<br>
• ⚠️ Không đo diện tích/khoảng cách chính xác (chưa chiếu phẳng)
</td>
</tr>
<tr>
<td>🇻🇳 <b>VN-2000</b><br>(Hệ phẳng, mét)</td>
<td>
• <b>Đơn vị:</b> Mét — lý tưởng để đo diện tích/khoảng cách<br>
• Mỗi tỉnh có <b>Kinh tuyến trục</b> và <b>mã EPSG</b> riêng<br>
• Ví dụ: Thanh Hóa = 5897, Quảng Nam = 5899, Quảng Trị = 9213
</td>
</tr>
</table>

<h4>⚡ Hệ Hội nhập vs Hệ Nội bộ (Rất quan trọng khi đo GPS!)</h4>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th width="35%">Loại</th><th>Khi nào dùng</th></tr>
<tr>
<td>✅ <b>Hệ hội nhập</b></td>
<td>Có đủ 7 tham số (ví dụ: -192, -39, -111).<br>
→ Bản đồ <b>khớp hoàn hảo</b> với ảnh vệ tinh (Basemap) trên máy tính.</td>
</tr>
<tr>
<td>⚠️ <b>Hệ nội bộ</b></td>
<td>Không có tham số dịch chuyển. Dùng khi GPS cầm tay <b>chưa cài 7 tham số</b>.<br>
→ Tọa độ in ra <b>khớp với số đọc trên GPS</b> (dù lệch ảnh vệ tinh).</td>
</tr>
</table>

<hr>
<h3 style="background:#e8f5e9;padding:6px;border-radius:4px">
🔧 Phần 2 — Xử lý Bản đồ bị lệch</h3>
<p>Khi mở file thấy bản đồ lệch ảnh vệ tinh hoặc nằm giữa biển:</p>

<table border="0" cellpadding="4" style="font-size:11px;width:100%">
<tr><td style="background:#e3f2fd;border-radius:4px;padding:8px">
<b>🔍 Bước 1 — Xác định hệ tọa độ gốc</b><br>
• Mở Basemap (Google Satellite) để đối chiếu sông, đường<br>
• Kiểm tra tọa độ: nếu thấy <code>107.75</code> → <code>0.75 × 60 = 45'</code> → Kinh tuyến trục = <b>107°45'</b><br>
• Tra bảng CRS để biết thuộc tỉnh nào
</td></tr>
<tr><td style="background:#fff9c4;border-radius:4px;padding:8px">
<b>⚙️ Bước 2 — Định nghĩa lại CRS (KHÔNG Export!)</b><br>
• ❌ <b>TUYỆT ĐỐI</b> không dùng Export/Save As vội!<br>
• ✅ Dùng <b>"Set Layer CRS"</b> để ép đúng mã EPSG (ví dụ: 5899)<br>
• Bản đồ sẽ <b>nhảy về đúng vị trí</b> ngay lập tức
</td></tr>
<tr><td style="background:#e8f5e9;border-radius:4px;padding:8px">
<b>📤 Bước 3 — Chuyển đổi (Export)</b><br>
• Chỉ khi bản đồ đã nằm đúng vị trí<br>
• Export → Save As → chọn hệ tọa độ đích (ví dụ: WGS84 / 4326)
</td></tr>
</table>

<hr>
<h3 style="background:#fce4ec;padding:6px;border-radius:4px">
📱 Phần 3 — Xuất bản đồ lên Điện thoại & Google Earth</h3>

<table border="0" cellpadding="4" style="font-size:11px;width:100%">
<tr><td style="background:#f3e5f5;border-radius:4px;padding:8px">
<b>1️⃣ Sửa font:</b> Chuyển TCVN3 → Unicode trong Bảng thuộc tính
</td></tr>
<tr><td style="background:#e8eaf6;border-radius:4px;padding:8px">
<b>2️⃣ Cài Plugin:</b> "KML Tools" trong QGIS
</td></tr>
<tr><td style="background:#e3f2fd;border-radius:4px;padding:8px">
<b>3️⃣ Xuất KMZ:</b> Chỉ chọn các trường cần thiết (Chủ rừng, Địa danh, Loại đất, Lô, Khoảnh)<br>
❌ Không chọn hết tất cả → file quá nặng
</td></tr>
<tr><td style="background:#e8f5e9;border-radius:4px;padding:8px">
<b>4️⃣ Chia sẻ:</b> Gửi .kmz qua Zalo → Mở trên điện thoại bằng Google Earth
</td></tr>
</table>

<hr>
<h3 style="background:#fff3e0;padding:6px;border-radius:4px">
🔢 Phần 4 — Chuyển đổi Độ Phút Giây ↔ Số thập phân</h3>

<h4>📐 Công thức thủ công</h4>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-size:12px;width:100%;background:#fffde7">
<tr><td>
<b>Độ Phút Giây → Thập phân:</b>&nbsp;&nbsp; <code style="font-size:14px;background:#fff9c4;padding:4px 8px;border-radius:3px">Thập phân = Độ + Phút/60 + Giây/3600</code><br><br>
<b>Ví dụ:</b> 105°27'35"<br>
→ 105 + 27/60 + 35/3600 = 105 + 0.45 + 0.00972 = <b style="color:#d32f2f;font-size:13px">105.45972</b>
</td></tr>
<tr><td>
<b>Thập phân → Độ Phút Giây:</b><br>
• Độ = phần nguyên: <b>105</b><br>
• Phút = (0.45972 × 60) = 27.583 → <b>27'</b><br>
• Giây = (0.583 × 60) = 34.99 → <b>35"</b><br>
→ Kết quả: <b style="color:#1565c0;font-size:13px">105°27'35"</b>
</td></tr>
</table>

<h4>🖥️ Công thức QGIS (Field Calculator)</h4>
<table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:11px;width:100%">
<tr style="background:#fff9c4"><th>Chiều</th><th>Công thức QGIS</th><th>Kiểu cột</th></tr>
<tr>
<td>Độ Phút Giây → Thập phân</td>
<td><code>dms_to_degree("tên_cột")</code></td>
<td>Decimal / Real</td>
</tr>
<tr>
<td>Thập phân → Độ Phút Giây</td>
<td><code>to_dms(X)</code> hoặc<br><code>to_dms(transform($geometry, 'EPSG:3405', 'EPSG:4326'))</code></td>
<td>Text / Chuỗi</td>
</tr>
</table>
<p>💡 Dùng <b>Field Calculator</b> trong Bảng thuộc tính để chuyển đổi hàng loạt.</p>
"""

    # ── Presets (Save / Load / Delete) ───────────────────────────

    def _preset_dir(self):
        """Return presets directory, creating if needed."""
        import os
        d = os.path.join(os.path.dirname(os.path.dirname(__file__)),
                         "presets")
        os.makedirs(d, exist_ok=True)
        return d

    def _load_preset_list(self):
        """Populate preset combobox from JSON files."""
        import os
        import glob
        self.cmb_presets.clear()
        d = self._preset_dir()
        for f in sorted(glob.glob(os.path.join(d, "*.json"))):
            name = os.path.splitext(os.path.basename(f))[0]
            self.cmb_presets.addItem(name, f)
        if self.cmb_presets.count() == 0:
            self.cmb_presets.addItem("(no presets)")

    def _save_preset(self):
        """Save current dialog settings to a JSON preset file."""
        import json
        import os
        from qgis.PyQt.QtWidgets import QInputDialog
        name, ok = QInputDialog.getText(
            self, "Save Preset", "Preset name:",
            text=self.cmb_presets.currentText().replace("(no presets)", ""))
        if not ok or not name.strip():
            return
        name = name.strip()

        data = {
            # General tab
            "mode": "print" if self.rad_print.isChecked() else "slide",
            "paper": self.cmb_paper.currentText(),
            "orientation": 0 if self.rad_landscape.isChecked() else 1,
            "title": self.txt_title.text(),
            "subtitle": self.txt_subtitle.text(),
            "org": self.txt_org.text(),
            "language": self.cmb_lang.currentIndex(),
            # Map Settings tab
            "signature": self.chk_signature.isChecked(),
            "sig_h": self.spn_sig_h.value(),
            "sig_w": self.spn_sig_w.value(),
            "sig_left": self.spn_sig_left.value(),
            "sig_bottom": self.spn_sig_bottom.value(),
            # Legend/Content tab
            "legend": self.chk_legend.isChecked(),
            "leg_title": self.txt_leg_title.text(),
            "leg_title_size": self.spn_leg_title_size.value(),
            "leg_cols": self.spn_leg_cols.value(),
            "leg_split": self.chk_leg_split.isChecked(),
            "leg_filter_map": self.chk_leg_filter_map.isChecked(),
            "leg_auto_update": self.chk_leg_auto_update.isChecked(),
            "leg_sym_w": self.spn_leg_sym_w.value(),
            "leg_sym_h": self.spn_leg_sym_h.value(),
            "leg_max_w": self.spn_leg_max_w.value(),
            "leg_mb": self.spn_leg_mb.value(),
            "leg_ml": self.spn_leg_ml.value(),
            "leg_margin_bottom": self.spn_leg_margin_bottom.value(),
            "leg_font": self.cmb_leg_font.currentIndex(),
            "leg_font_size": self.spn_leg_font_size.value(),
            "leg_spacing": self.spn_leg_spacing.value(),
            "leg_hdr_bold": self.chk_leg_hdr_bold.isChecked(),
            "leg_hide_groups": self.chk_leg_hide_groups.isChecked(),
            "data_sources": self.txt_sources.toPlainText(),
        }

        fpath = os.path.join(self._preset_dir(), f"{name}.json")
        with open(fpath, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        self._load_preset_list()
        # Select the just-saved preset
        idx = self.cmb_presets.findText(name)
        if idx >= 0:
            self.cmb_presets.setCurrentIndex(idx)

    def _load_preset(self):
        """Load settings from selected JSON preset."""
        import json
        import os
        fpath = self.cmb_presets.currentData()
        if not fpath or not os.path.isfile(fpath):
            return
        try:
            with open(fpath, "r", encoding="utf-8") as f:
                data = json.load(f)
        except Exception:
            return

        # General tab
        if data.get("mode") == "print":
            self.rad_print.setChecked(True)
        else:
            self.rad_slide.setChecked(True)
        if "paper" in data:
            idx = self.cmb_paper.findText(data["paper"])
            if idx >= 0:
                self.cmb_paper.setCurrentIndex(idx)
        if "orientation" in data:
            if data["orientation"] == 0:
                self.rad_landscape.setChecked(True)
            else:
                self.rad_portrait.setChecked(True)
        if "title" in data:
            self.txt_title.setText(data["title"])
        if "subtitle" in data:
            self.txt_subtitle.setText(data["subtitle"])
        if "org" in data:
            self.txt_org.setText(data["org"])
        if "language" in data:
            self.cmb_lang.setCurrentIndex(data["language"])

        # Map Settings tab
        if "signature" in data:
            self.chk_signature.setChecked(data["signature"])
        if "sig_h" in data:
            self.spn_sig_h.setValue(data["sig_h"])
        if "sig_w" in data:
            self.spn_sig_w.setValue(data["sig_w"])
        if "sig_left" in data:
            self.spn_sig_left.setValue(data["sig_left"])
        if "sig_bottom" in data:
            self.spn_sig_bottom.setValue(data["sig_bottom"])

        # Legend/Content tab
        if "legend" in data:
            self.chk_legend.setChecked(data["legend"])
        if "leg_title" in data:
            self.txt_leg_title.setText(data["leg_title"])
        if "leg_title_size" in data:
            self.spn_leg_title_size.setValue(data["leg_title_size"])
        if "leg_cols" in data:
            self.spn_leg_cols.setValue(data["leg_cols"])
        if "leg_split" in data:
            self.chk_leg_split.setChecked(data["leg_split"])
        if "leg_filter_map" in data:
            self.chk_leg_filter_map.setChecked(data["leg_filter_map"])
        if "leg_auto_update" in data:
            self.chk_leg_auto_update.setChecked(data["leg_auto_update"])
        if "leg_sym_w" in data:
            self.spn_leg_sym_w.setValue(data["leg_sym_w"])
        if "leg_sym_h" in data:
            self.spn_leg_sym_h.setValue(data["leg_sym_h"])
        if "leg_max_w" in data:
            self.spn_leg_max_w.setValue(data["leg_max_w"])
        if "leg_mb" in data:
            self.spn_leg_mb.setValue(data["leg_mb"])
        if "leg_ml" in data:
            self.spn_leg_ml.setValue(data["leg_ml"])
        if "leg_margin_bottom" in data:
            self.spn_leg_margin_bottom.setValue(data["leg_margin_bottom"])
        if "leg_font" in data:
            self.cmb_leg_font.setCurrentIndex(data["leg_font"])
        if "leg_font_size" in data:
            self.spn_leg_font_size.setValue(data["leg_font_size"])
        if "leg_spacing" in data:
            self.spn_leg_spacing.setValue(data["leg_spacing"])
        if "leg_hdr_bold" in data:
            self.chk_leg_hdr_bold.setChecked(data["leg_hdr_bold"])
        if "leg_hide_groups" in data:
            self.chk_leg_hide_groups.setChecked(data["leg_hide_groups"])
        if "data_sources" in data:
            self.txt_sources.setPlainText(data["data_sources"])

    def _delete_preset(self):
        """Delete selected preset file."""
        import os
        from qgis.PyQt.QtWidgets import QMessageBox
        fpath = self.cmb_presets.currentData()
        if not fpath or not os.path.isfile(fpath):
            return
        name = self.cmb_presets.currentText()
        reply = QMessageBox.question(
            self, "Delete Preset",
            f"Delete preset \"{name}\"?",
            MsgButton.Yes | MsgButton.No)
        if reply == MsgButton.Yes:
            os.remove(fpath)
            self._load_preset_list()

    def _reset_defaults(self):
        """Reset all dialog settings to factory defaults."""
        # General tab
        self.rad_print.setChecked(True)
        idx = self.cmb_paper.findText("A3")
        if idx >= 0:
            self.cmb_paper.setCurrentIndex(idx)
        self.rad_landscape.setChecked(True)  # Landscape
        self.txt_title.clear()
        self.txt_subtitle.clear()
        self.txt_org.clear()
        self.cmb_lang.setCurrentIndex(0)  # EN

        # Map Settings tab
        self.chk_signature.setChecked(False)
        self._sig_container.setVisible(False)
        self.spn_sig_h.setValue(50)
        self.spn_sig_w.setValue(180)
        self.spn_sig_left.setValue(3)
        self.spn_sig_bottom.setValue(3)

        # Legend/Content tab
        self.chk_legend.setChecked(False)
        self._leg_container.setVisible(False)
        self.txt_leg_title.setText("CHÚ DẪN")
        self.spn_leg_title_size.setValue(14)
        self.spn_leg_cols.setValue(2)
        self.chk_leg_split.setChecked(True)
        self.chk_leg_filter_map.setChecked(True)
        self.chk_leg_auto_update.setChecked(True)
        self.spn_leg_sym_w.setValue(23)
        self.spn_leg_sym_h.setValue(8)
        self.spn_leg_max_w.setValue(120)
        self.spn_leg_mb.setValue(6)
        self.spn_leg_ml.setValue(3)
        self.spn_leg_margin_bottom.setValue(6)
        self.cmb_leg_font.setCurrentIndex(0)  # Times New Roman
        self.spn_leg_font_size.setValue(8)
        self.spn_leg_spacing.setValue(1.4)
        self.chk_leg_hdr_bold.setChecked(True)
        self.chk_leg_hide_groups.setChecked(False)
        self.txt_sources.clear()

        # Reset layer checkboxes to all checked
        for chk in self._leg_layer_checks.values():
            chk.setChecked(True)
        # Clear rename fields
        for le in self._leg_layer_renames.values():
            le.clear()
        # Reset symbol combos to default (index 0)
        for cmb in self._leg_symbol_combos.values():
            cmb.setCurrentIndex(0)
        # Clear search
        if hasattr(self, '_leg_search'):
            self._leg_search.clear()

        self.refresh_layers()

    # ── Collect Parameters ───────────────────────────────────────

    def _collect_params(self):
        """Gather all UI values into a dict for the engine."""
        mode = MODE_PRINT if self.rad_print.isChecked() else MODE_SLIDE
        lang = "EN" if self.cmb_lang.currentIndex() == 0 else "VN"

        paper_key = self.cmb_paper.currentText()
        is_custom = paper_key == "Custom / Tự chọn"
        drawn_extent = getattr(self, "drawn_extent", None)

        if is_custom:
            # Spinboxes hold Map Frame size; add margins for page
            map_w = self.spn_custom_w.value()
            map_h = self.spn_custom_h.value()
            ml, mt, mr, mb = self._get_margins()
            pw = int(round(map_w + ml + mr))
            ph = int(round(map_h + mt + mb))
        else:
            pw, ph = PAPER_SIZES[paper_key]
            # Only apply orientation swap for standard sizes
            if self.rad_portrait.isChecked():
                pw, ph = ph, pw

        return {
            "mode": mode,
            "lang": lang,
            "title": self.txt_title.text().strip(),
            "org_name": self.txt_org.text().strip(),
            "study_area": self.txt_study.text().strip(),
            "references": "",  # fixed in template
            "author": self.txt_author.text().strip(),
            "date": self.txt_date.text().strip(),
            "page_width": pw,
            "page_height": ph,
            "auto_scale": self.chk_auto_scale.isChecked(),
            "scale": self.spn_scale.value(),
            "extent_mode": "drawn" if (is_custom and drawn_extent) else "canvas",
            "drawn_extent": drawn_extent,
            "data_sources": self.txt_sources.toPlainText().strip(),
            "show_legend": self.chk_legend.isChecked(),
            "show_north": self.chk_north.isChecked(),
            "north_arrow_svg": self.cmb_arrow.currentData() or "",
            "north_arrow_left": self.spn_na_left.value(),
            "north_arrow_top": self.spn_na_top.value(),
            "north_arrow_w": self.spn_na_w.value(),
            "north_arrow_h": self.spn_na_h.value(),
            "show_scalebar": self.chk_scalebar.isChecked(),
            "signature_block": self.chk_signature.isChecked() and self.rad_print.isChecked(),
            "sig_height": self.spn_sig_h.value(),
            "sig_width": self.spn_sig_w.value(),
            "sig_margin_left": self.spn_sig_left.value(),
            "sig_margin_bottom": self.spn_sig_bottom.value(),
            "sig_font_title": (self._sf1.currentText(),
                               self._ss1.value(),
                               self._sb1.isChecked(),
                               self._si1.isChecked()),
            "sig_font_date": (self._sf2.currentText(),
                              self._ss2.value(),
                              self._sb2.isChecked(),
                              self._si2.isChecked()),
            "sig_font_name": (self._sf3.currentText(),
                              self._ss3.value(),
                              self._sb3.isChecked(),
                              self._si3.isChecked()),
            "sig_blocks": [
                {"title": b["title"].text().strip(),
                 "date": b["date"].text().strip(),
                 "name": b["name"].text().strip()}
                for b in self._sig_block_inputs
            ],
            "legend": {
                "enabled": self.chk_legend.isChecked(),
                "title": self.txt_leg_title.text().strip(),
                "title_size": self.spn_leg_title_size.value(),
                "columns": self.spn_leg_cols.value(),
                "split_layer": self.chk_leg_split.isChecked(),
                "filter_by_map": self.chk_leg_filter_map.isChecked(),
                "auto_update": self.chk_leg_auto_update.isChecked(),
                "symbol_width": self.spn_leg_sym_w.value(),
                "symbol_height": self.spn_leg_sym_h.value(),
                "max_width": self.spn_leg_max_w.value(),
                "margin_left": self.spn_leg_ml.value(),
                "margin_bottom": self.spn_leg_margin_bottom.value(),
                "font": self.cmb_leg_font.currentText(),
                "font_size": self.spn_leg_font_size.value(),
                "line_spacing": self.spn_leg_spacing.value(),
                "header_bold": self.chk_leg_hdr_bold.isChecked(),
                "hide_groups": self.chk_leg_hide_groups.isChecked(),
                "layer_ids": [lid for lid, chk
                              in self._leg_layer_checks.items()
                              if chk.isChecked()],
                "layer_renames": {
                    lid: le.text().strip()
                    for lid, le in self._leg_layer_renames.items()
                    if le.text().strip()
                },
                "symbol_overrides": {
                    lid: cmb.currentData()
                    for lid, cmb in self._leg_symbol_combos.items()
                    if cmb.currentData()
                },
            },
        }

    # ── Actions ──────────────────────────────────────────────────

    def _on_create(self):
        """Generate layout and open in QGIS layout designer."""
        params = self._collect_params()

        # Validation
        if params["extent_mode"] == "drawn" and not params["drawn_extent"]:
            QMessageBox.warning(
                self, "LVT",
                "Please draw an extent first using the 'Draw Extent' button."
            )
            return

        # Disable button + show wait cursor (no modal dialog to avoid freeze)
        self.btn_create.setEnabled(False)
        from ..shared.i18n import current_language
        self.btn_create.setText(
            "⏳ Đang tạo..." if current_language() == 'vi' else "⏳ Creating...")
        from qgis.PyQt.QtWidgets import QApplication
        QApplication.setOverrideCursor(QtCompat.WaitCursor)
        QApplication.processEvents()

        try:
            layout = self.engine.create_layout(params)
            designer = self.iface.openLayoutDesigner(layout)
            try:
                designer.view().zoomFull()
                # Trigger a deferred refresh so map tiles render sharply
                # after the designer window is fully visible (800ms delay).
                from qgis.PyQt.QtCore import QTimer as _QTimer
                def _deferred_refresh():
                    try:
                        layout.refresh()
                        designer.view().zoomFull()
                    except Exception:  # nosec B110
                        pass  # nosec B110
                _QTimer.singleShot(800, _deferred_refresh)
            except Exception:  # nosec B110
                pass  # nosec B110
            self.iface.messageBar().pushSuccess(
                "LVT",
                "✅ Đã tạo khung!" if current_language() == 'vi' else "✅ Layout created!"
            )
        except Exception as e:
            QMessageBox.critical(self, "Error", str(e))
        finally:
            QApplication.restoreOverrideCursor()
            self.btn_create.setEnabled(True)
            self.btn_create.setText(
                "Tạo khung" if current_language() == 'vi' else "Create Layout")
