土石流区域設定支援システムと同等な横断測線、流下方向(縦断測線)をQGISで再現

はじめに

土石流区域設定支援システムのデータベース(dem.mdb)から、縦断情報と横断情報を読み取り、QGIS上に測線を作成するPythonスクリプトを作成しました。

当初はMDBを直接開く部分でうまくいかなかったため、次の手順で処理していました。

これまでの方法
MDBをAccessで開く → Excelの各シートへコピー → CSV出力 → QGISでCSVを選択 → Pythonを実行

今回、Microsoft AccessのODBC接続方法を見直したことで、QGISからMDBを直接読み込み、1回の操作で2つのレイヤを一括作成できるようになりました。

新しい方法
QGISでdem.mdbを選択 → 「流下方向」と「横断測線」を一括作成

MDB内の独自形式である「カンマ区切り座標」と「mm単位」を自動変換し、以下の2レイヤを生成します。

  1. 流下方向(縦断):区間勾配を計算し、勾配に応じて色分けしたライン
  2. 横断測線:各横断グループの始点と終点を結んだライン
QGISで作成した縦断・横断測線

1. 更新版:MDBを直接読み込んで一括作成

この更新版では、MDB内の次の2テーブルを直接読み込みます。

  • 縦断情報GISKeyIDILData
  • 横断情報GISKeyIDILData

ILDataには、標高・X座標・Y座標がカンマ区切りかつmm単位で記録されています。スクリプトはこれをm単位へ変換してQGISのラインを作成します。

一括処理の内容

  • MDBファイルを1回選択するだけで、縦断・横断をまとめて処理
  • GISKeyごとにデータを分離し、異なる箇所同士の誤接続を防止
  • 縦断はID順に接続し、距離・標高差・勾配角度を計算
  • 横断はIDの10000単位のグループごとに、最小IDと最大IDを接続
  • 作成した2レイヤをMDB名のレイヤグループに格納
  • 処理件数とスキップ件数を完了画面に表示

作成される属性

レイヤ 主な属性
流下方向 GISKey、始点ID、終点ID、勾配角、絶対勾配角、水平距離、標高差、始点・終点座標
横断測線 連番、GISKey、グループID、始点ID、終点ID、点数、始点・終点座標

実行環境

  • Windows版QGIS
  • QGISと同じビット数のMicrosoft Access ODBCドライバー
  • MDB内に「縦断情報」「横断情報」テーブルが存在すること

通常の64bit版QGISを使用している場合は、64bit版のMicrosoft Access Database Engineが必要です。

QGISでの実行方法

  1. 下記のコードをUTF-8形式のPythonファイルとして保存します。
  2. QGISで「プラグイン」→「Pythonコンソール」を開きます。
  3. 「エディタを表示」を押して、保存したPythonファイルを開きます。
  4. 実行ボタンを押し、対象のdem.mdbを選択します。
  5. レイヤパネルに「流下方向」と「横断測線」が追加されます。

Pythonコンソールから直接実行する場合は、次のように入力します。

exec(open(r"C:\保存先\QGIS_MDB_batch_v2.py", encoding="utf-8").read())

MDB接続でつまずいた原因

MDBを直接開く処理では、データ解析より先にODBC接続でつまずきやすい点に注意が必要です。今回も当初は「MDBを開けませんでした」と表示されましたが、Accessドライバーそのものは認識されていました。

原因は、接続文字列のMDBパスを波括弧で囲んでいたことでした。

うまくいかなかった例
DBQ={C:\フォルダ\dem.mdb}
正しい例
DBQ=C:\フォルダ\dem.mdb

Accessドライバーによっては、波括弧までファイル名として解釈され、「ファイル名が正しくありません」となる場合があります。また、日本語や空白を含むOneDrive上のパスに備え、更新版ではWindowsの短縮パスでも再試行するようにしています。

更新版スクリプトコード

# -*- coding: utf-8 -*-
"""
QGIS Pythonコンソール用
Microsoft Access MDBから「縦断情報」「横断情報」を直接読み込み、
  1) 流下方向(区間勾配で色分け)
  2) 横断測線
を一括作成します。

前提:
- Windows版QGIS
- QGISと同じビット数のMicrosoft Access ODBCドライバーがインストール済み
- MDB内に [縦断情報] / [横断情報] テーブルがあり、
  GISKey, ID, ILData の各列が存在すること
"""

import math
import os
import uuid
from collections import defaultdict

from qgis.core import (
    QgsFeature,
    QgsField,
    QgsGeometry,
    QgsGraduatedSymbolRenderer,
    QgsPointXY,
    QgsProject,
    QgsRendererRange,
    QgsSymbol,
    QgsVectorLayer,
)
from qgis.PyQt.QtCore import QVariant
from qgis.PyQt.QtGui import QColor
from qgis.PyQt.QtSql import QSqlDatabase, QSqlQuery
from qgis.PyQt.QtWidgets import QFileDialog, QMessageBox
from qgis.utils import iface


# 岐阜県(平面直角座標系第7系)の既定値。
# QGISプロジェクトが投影座標系なら、プロジェクトCRSを優先します。
DEFAULT_CRS = "EPSG:6675"
VERTICAL_TABLE = "縦断情報"
TRANSVERSE_TABLE = "横断情報"


def _output_crs_authid():
    """メートル座標として使える出力CRSを返す。"""
    project_crs = QgsProject.instance().crs()
    if project_crs.isValid() and not project_crs.isGeographic():
        authid = project_crs.authid()
        if authid:
            return authid
    return DEFAULT_CRS


def _installed_access_driver_names():
    """Windowsレジストリと一般的な名称からAccess ODBCドライバー候補を返す。"""
    names = []

    try:
        import winreg

        registry_paths = [
            r"SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers",
            r"SOFTWARE\WOW6432Node\ODBC\ODBCINST.INI\ODBC Drivers",
        ]
        for root in (winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER):
            for reg_path in registry_paths:
                try:
                    with winreg.OpenKey(root, reg_path) as key:
                        index = 0
                        while True:
                            try:
                                driver_name, value, _ = winreg.EnumValue(key, index)
                                index += 1
                                text = driver_name.lower()
                                if (
                                    "access" in text
                                    and ".mdb" in text
                                    and str(value).lower() == "installed"
                                ):
                                    names.append(driver_name)
                            except OSError:
                                break
                except OSError:
                    pass
    except ImportError:
        pass

    # 通常の日本語Windowsでも、ドライバー名はこの英語名で登録されます。
    names.extend([
        "Microsoft Access Driver (*.mdb, *.accdb)",
        "Microsoft Access Driver (*.mdb)",
    ])

    # 重複除去(順序保持)
    return list(dict.fromkeys(names))


def _open_mdb(mdb_path):
    """QODBCでMDBを開き、(database, connection_name) を返す。"""
    if "QODBC" not in QSqlDatabase.drivers():
        raise RuntimeError(
            "QGISのQt SQLにQODBCドライバーがありません。\n"
            "Windows版QGISのインストール構成を確認してください。"
        )

    errors = []

    # Access ODBC の DBQ はファイルパスを波括弧で囲まない。
    # DBQ={C:\...\file.mdb} とすると、Access ドライバーが波括弧まで
    # ファイル名として解釈し「ファイル名が正しくありません」になることがある。
    native_path = os.path.normpath(os.path.abspath(mdb_path))
    if not os.path.isfile(native_path):
        raise RuntimeError("MDBファイルが見つかりません。\n{}".format(native_path))

    # 通常パスに加え、取得できる場合はWindowsの短いパスも試す。
    # 日本語・空白を含むOneDrive上のパスで古いAccessドライバーが失敗する場合の保険。
    path_candidates = [native_path]
    try:
        import ctypes

        buffer_size = 32768
        buffer = ctypes.create_unicode_buffer(buffer_size)
        result = ctypes.windll.kernel32.GetShortPathNameW(
            native_path, buffer, buffer_size
        )
        if result and buffer.value and buffer.value not in path_candidates:
            path_candidates.append(buffer.value)
    except Exception:
        pass

    for driver_name in _installed_access_driver_names():
        for dbq_path in path_candidates:
            connection_name = "mdb_" + uuid.uuid4().hex
            db = QSqlDatabase.addDatabase("QODBC", connection_name)
            connection_string = "DRIVER={{{}}};DBQ={};READONLY=TRUE;".format(
                driver_name, dbq_path
            )
            db.setDatabaseName(connection_string)

            if db.open():
                return db, connection_name

            errors.append(
                "{} / {}: {}".format(
                    driver_name, dbq_path, db.lastError().text()
                )
            )
            db.close()
            del db
            QSqlDatabase.removeDatabase(connection_name)

    detail = "\n".join(errors)
    raise RuntimeError(
        "MDBを開けませんでした。\n"
        "QGISと同じビット数のMicrosoft Access ODBCドライバーが必要です。\n\n"
        + detail
    )


def _read_access_table(db, table_name):
    """GISKey, ID, ILDataをMDBテーブルから読み込む。"""
    available = db.tables()
    if table_name not in available:
        raise RuntimeError(
            "MDB内にテーブル「{}」がありません。\n\n存在するテーブル:\n{}".format(
                table_name, "\n".join(available)
            )
        )

    sql = (
        "SELECT [GISKey], [ID], [ILData] "
        "FROM [{}] ORDER BY [GISKey], [ID]".format(table_name)
    )
    query = QSqlQuery(db)
    exec_method = getattr(query, "exec", None) or query.exec_
    if not exec_method(sql):
        raise RuntimeError(
            "テーブル「{}」の読み込みに失敗しました。\n{}".format(
                table_name, query.lastError().text()
            )
        )

    rows = []
    skipped = 0
    while query.next():
        try:
            gis_key = int(query.value(0))
            point_id = int(query.value(1))
            il_data = str(query.value(2)).strip()
            if not il_data:
                skipped += 1
                continue
            rows.append((gis_key, point_id, il_data))
        except (TypeError, ValueError):
            skipped += 1

    return rows, skipped


def _parse_ildata(il_data):
    """ILDataから Z, X, Y(mm→m)を取り出す。"""
    try:
        parts = [part.strip() for part in str(il_data).split(",")]
        if len(parts) < 4:
            return None
        z = float(parts[1]) / 1000.0
        x = float(parts[2]) / 1000.0
        y = float(parts[3]) / 1000.0
        return z, x, y
    except (TypeError, ValueError):
        return None


def _create_transverse_layer(rows, crs_authid):
    """横断情報から、各横断グループの最小ID~最大IDを結ぶ測線を作成。"""
    groups = {}
    invalid = 0

    # GISKeyもキーに含めるため、複数箇所入りMDBでも混線しない。
    for gis_key, point_id, il_data in rows:
        group_no = point_id // 10000
        if group_no <= 0:
            invalid += 1
            continue

        parsed = _parse_ildata(il_data)
        if parsed is None:
            invalid += 1
            continue

        _, x, y = parsed
        key = (gis_key, group_no)
        item = (point_id, x, y)

        if key not in groups:
            groups[key] = {"min": item, "max": item, "count": 1}
        else:
            groups[key]["count"] += 1
            if point_id < groups[key]["min"][0]:
                groups[key]["min"] = item
            if point_id > groups[key]["max"][0]:
                groups[key]["max"] = item

    layer = QgsVectorLayer(
        "LineString?crs={}".format(crs_authid), "横断測線", "memory"
    )
    provider = layer.dataProvider()
    provider.addAttributes([
        QgsField("Seq_No", QVariant.Int),
        QgsField("GISKey", QVariant.LongLong),
        QgsField("Group_ID", QVariant.Int),
        QgsField("Start_ID", QVariant.Int),
        QgsField("End_ID", QVariant.Int),
        QgsField("Pt_Count", QVariant.Int),
        QgsField("Start_X", QVariant.Double),
        QgsField("Start_Y", QVariant.Double),
        QgsField("End_X", QVariant.Double),
        QgsField("End_Y", QVariant.Double),
    ])
    layer.updateFields()

    features = []
    for seq_no, key in enumerate(sorted(groups.keys())):
        gis_key, group_no = key
        group = groups[key]
        start_id, start_x, start_y = group["min"]
        end_id, end_x, end_y = group["max"]

        feature = QgsFeature(layer.fields())
        feature.setGeometry(
            QgsGeometry.fromPolylineXY([
                QgsPointXY(start_x, start_y),
                QgsPointXY(end_x, end_y),
            ])
        )
        feature.setAttributes([
            seq_no,
            gis_key,
            group_no * 10000,
            start_id,
            end_id,
            group["count"],
            start_x,
            start_y,
            end_x,
            end_y,
        ])
        features.append(feature)

    provider.addFeatures(features)
    layer.updateExtents()
    layer.setCustomProperty("source_table", TRANSVERSE_TABLE)
    return layer, len(features), invalid


def _apply_slope_renderer(line_layer):
    """絶対勾配角による既存の色区分を適用。"""
    categories = [
        (0.0, 2.0, QColor(0, 255, 255), "0度 ~ 2度未満"),
        (2.0, 3.0, QColor(0, 0, 255), "2度 ~ 3度未満"),
        (3.0, 7.0, QColor(0, 255, 0), "3度 ~ 7度未満"),
        (7.0, 10.0, QColor(139, 0, 0), "7度 ~ 10度未満"),
        (10.0, 15.0, QColor(255, 0, 255), "10度 ~ 15度未満"),
        (15.0, 20.0, QColor(205, 133, 63), "15度 ~ 20度未満"),
        (20.0, 30.0, QColor(255, 165, 0), "20度 ~ 30度未満"),
        (30.0, 999.0, QColor(255, 0, 0), "30度以上"),
    ]

    ranges = []
    for lower, upper, color, label in categories:
        symbol = QgsSymbol.defaultSymbol(line_layer.geometryType())
        symbol.setColor(color)
        symbol.setWidth(0.8)
        ranges.append(QgsRendererRange(lower, upper, symbol, label))

    renderer = QgsGraduatedSymbolRenderer("slope_abs", ranges)
    renderer.setMode(QgsGraduatedSymbolRenderer.Custom)
    line_layer.setRenderer(renderer)


def _create_vertical_layer(rows, crs_authid):
    """縦断情報をGISKey別にID順で結び、区間勾配を計算。"""
    point_groups = defaultdict(list)
    invalid = 0

    for gis_key, point_id, il_data in rows:
        parsed = _parse_ildata(il_data)
        if parsed is None:
            invalid += 1
            continue
        z, x, y = parsed
        point_groups[gis_key].append((point_id, x, y, z))

    layer = QgsVectorLayer(
        "LineString?crs={}".format(crs_authid), "流下方向", "memory"
    )
    provider = layer.dataProvider()
    provider.addAttributes([
        QgsField("GISKey", QVariant.LongLong),
        QgsField("Start_ID", QVariant.Int),
        QgsField("End_ID", QVariant.Int),
        QgsField("slope_angle", QVariant.Double),
        QgsField("slope_abs", QVariant.Double),
        QgsField("distance_m", QVariant.Double),
        QgsField("dz_m", QVariant.Double),
        QgsField("Start_X", QVariant.Double),
        QgsField("Start_Y", QVariant.Double),
        QgsField("End_X", QVariant.Double),
        QgsField("End_Y", QVariant.Double),
    ])
    layer.updateFields()

    features = []
    for gis_key in sorted(point_groups.keys()):
        points = sorted(point_groups[gis_key], key=lambda value: value[0])
        for index in range(len(points) - 1):
            start_id, x1, y1, z1 = points[index]
            end_id, x2, y2, z2 = points[index + 1]

            dx = x2 - x1
            dy = y2 - y1
            dz = z2 - z1
            distance = math.hypot(dx, dy)

            if distance > 0:
                angle = math.degrees(math.atan2(dz, distance))
            elif dz > 0:
                angle = 90.0
            elif dz < 0:
                angle = -90.0
            else:
                angle = 0.0

            feature = QgsFeature(layer.fields())
            feature.setGeometry(
                QgsGeometry.fromPolylineXY([
                    QgsPointXY(x1, y1),
                    QgsPointXY(x2, y2),
                ])
            )
            feature.setAttributes([
                gis_key,
                start_id,
                end_id,
                angle,
                abs(angle),
                distance,
                dz,
                x1,
                y1,
                x2,
                y2,
            ])
            features.append(feature)

    provider.addFeatures(features)
    layer.updateExtents()
    layer.setCustomProperty("source_table", VERTICAL_TABLE)
    _apply_slope_renderer(layer)
    return layer, len(features), invalid


def create_layers_from_mdb():
    mdb_path, _ = QFileDialog.getOpenFileName(
        None,
        "MDBファイルを選択してください",
        "",
        "Microsoft Access Database (*.mdb *.accdb);;All Files (*)",
    )
    if not mdb_path:
        return

    db = None
    connection_name = None

    try:
        db, connection_name = _open_mdb(mdb_path)

        vertical_rows, vertical_read_skipped = _read_access_table(
            db, VERTICAL_TABLE
        )
        transverse_rows, transverse_read_skipped = _read_access_table(
            db, TRANSVERSE_TABLE
        )

        if not vertical_rows:
            raise RuntimeError("テーブル「縦断情報」に有効なデータがありません。")
        if not transverse_rows:
            raise RuntimeError("テーブル「横断情報」に有効なデータがありません。")

        crs_authid = _output_crs_authid()
        vertical_layer, vertical_count, vertical_invalid = _create_vertical_layer(
            vertical_rows, crs_authid
        )
        transverse_layer, transverse_count, transverse_invalid = (
            _create_transverse_layer(transverse_rows, crs_authid)
        )

        # レイヤパネル内でMDBごとにまとめる。
        project = QgsProject.instance()
        group_name = "MDB解析_{}".format(os.path.splitext(os.path.basename(mdb_path))[0])
        root = project.layerTreeRoot()
        group = root.addGroup(group_name)

        project.addMapLayer(vertical_layer, False)
        group.addLayer(vertical_layer)
        project.addMapLayer(transverse_layer, False)
        group.addLayer(transverse_layer)

        iface.mapCanvas().refresh()

        QMessageBox.information(
            None,
            "完了",
            "MDBを直接読み込み、2レイヤを作成しました。\n\n"
            "・流下方向: {:,} 区間\n"
            "・横断測線: {:,} 本\n"
            "・出力CRS: {}\n\n"
            "読み込み時スキップ: 縦断 {:,} / 横断 {:,}\n"
            "ILData解析スキップ: 縦断 {:,} / 横断 {:,}".format(
                vertical_count,
                transverse_count,
                crs_authid,
                vertical_read_skipped,
                transverse_read_skipped,
                vertical_invalid,
                transverse_invalid,
            ),
        )

    except Exception as error:
        QMessageBox.critical(None, "エラー", str(error))

    finally:
        if db is not None:
            db.close()
            del db
        if connection_name:
            QSqlDatabase.removeDatabase(connection_name)


create_layers_from_mdb()
注意:作成されるレイヤはメモリレイヤです。QGISを閉じる前に、GeoPackageやShapefileなどへエクスポートして保存してください。

2. 旧方式:CSVから流下方向(縦断)を作成

以下は、MDBを直接開けない環境で使用していたCSV経由の旧方式です。ODBCドライバーを導入できないパソコンでは、現在も代替手段として利用できます。

縦断データのCSVを読み込み、以下の処理を行います。

  • mm単位の座標をm単位に変換してラインを作成
  • 勾配(角度)を計算し、自動で色分け(0~2度、2~3度...30度以上)
  • 属性テーブルに「勾配角度」と「始点・終点座標(X,Y)」を出力

スクリプトコード

import csv
import math
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsFeature,
    QgsGeometry,
    QgsField,
    QgsPointXY,
    QgsGraduatedSymbolRenderer,
    QgsRendererRange,
    QgsSymbol,
    QgsCoordinateReferenceSystem
)
from qgis.utils import iface
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QVariant
from PyQt5.QtGui import QColor

def main_process():
    # 1. ファイル選択
    file_path, _ = QFileDialog.getOpenFileName(
        None, "CSVファイルを選択してください", "", "CSV Files (*.csv);;All Files (*)"
    )
    if not file_path: return

    # 2. データを読み込み
    points = []
    encodings = ['cp932', 'utf-8']
    data_rows = []
    
    for enc in encodings:
        try:
            with open(file_path, 'r', encoding=enc, newline='') as f:
                reader = csv.reader(f)
                data_rows = list(reader)
            break
        except UnicodeDecodeError: continue
            
    if not data_rows: return

    # 座標データ列探索
    target_col_index = -1
    start_row = 1 if len(data_rows) > 1 else 0
    for i in range(start_row, min(len(data_rows), 5)):
        row = data_rows[i]
        for idx, val in enumerate(row):
            if val.count(',') >= 3: 
                target_col_index = idx
                break
        if target_col_index != -1: break
    
    if target_col_index == -1: return

    # データ解析
    for i in range(start_row, len(data_rows)):
        row = data_rows[i]
        if len(row) <= target_col_index: continue
        val_str = row[target_col_index]
        try:
            parts = val_str.split(',')
            if len(parts) < 4: continue
            # [1]=Z, [2]=X, [3]=Y (mm->m)
            z = float(parts[1]) / 1000.0
            x = float(parts[2]) / 1000.0
            y = float(parts[3]) / 1000.0
            points.append({'x': x, 'y': y, 'z': z})
        except: continue

    if len(points) < 2: return

    # 3. レイヤ作成(流下方向)
    crs = QgsProject.instance().crs().authid()
    if not crs: crs = "EPSG:6668"
    line_layer = QgsVectorLayer(f"LineString?crs={crs}", "流下方向", "memory")
    pr = line_layer.dataProvider()
    
    # 属性追加
    pr.addAttributes([
        QgsField("slope_angle", QVariant.Double),
        QgsField("Start_X", QVariant.Double),
        QgsField("Start_Y", QVariant.Double),
        QgsField("End_X", QVariant.Double),
        QgsField("End_Y", QVariant.Double)
    ])
    line_layer.updateFields()

    new_features = []
    
    # 4. ライン生成と勾配計算
    for i in range(len(points) - 1):
        p1 = points[i]
        p2 = points[i+1]
        dx = p2['x'] - p1['x']
        dy = p2['y'] - p1['y']
        dz = p2['z'] - p1['z']
        dist = math.sqrt(dx*dx + dy*dy)
        angle = 0.0
        if dist != 0:
            angle = math.degrees(math.atan2(dz, dist))
        elif dz != 0:
            angle = 90.0 if dz > 0 else -90.0
            
        geom = QgsGeometry.fromPolylineXY([QgsPointXY(p1['x'], p1['y']), QgsPointXY(p2['x'], p2['y'])])
        f = QgsFeature()
        f.setGeometry(geom)
        f.setAttributes([angle, p1['x'], p1['y'], p2['x'], p2['y']])
        new_features.append(f)

    pr.addFeatures(new_features)
    line_layer.updateExtents()
    
    # 5. 色分け適用
    target_field = 'abs("slope_angle")'
    categories = [
        (0.0,  2.0,  QColor(0, 255, 255), "0度 ~ 2度未満"),
        (2.0,  3.0,  QColor(0, 0, 255),   "2度 ~ 3度未満"),
        (3.0,  7.0,  QColor(0, 255, 0),   "3度 ~ 7度未満"),
        (7.0,  10.0, QColor(139, 0, 0),   "7度 ~ 10度未満"),
        (10.0, 15.0, QColor(255, 0, 255), "10度 ~ 15度未満"),
        (15.0, 20.0, QColor(205, 133, 63), "15度 ~ 20度未満"),
        (20.0, 30.0, QColor(255, 165, 0),  "20度 ~ 30度未満"),
        (30.0, 90.0, QColor(255, 0, 0),    "30度以上")
    ]
    ranges = []
    for lower, upper, color, label in categories:
        symbol = QgsSymbol.defaultSymbol(line_layer.geometryType())
        symbol.setColor(color)
        symbol.setWidth(0.8)
        ranges.append(QgsRendererRange(lower, upper, symbol, label))

    renderer = QgsGraduatedSymbolRenderer(target_field, ranges)
    renderer.setMode(QgsGraduatedSymbolRenderer.Custom)
    line_layer.setRenderer(renderer)

    QgsProject.instance().addMapLayer(line_layer)
    QMessageBox.information(None, "完了", "CSVから色分けラインを作成しました。\n属性テーブルに座標(m)が入っています。")

main_process()

勾配ごとに色分けされた流下方向ライン


3. 旧方式:CSVから横断測線を作成

横断測線のCSVデータから、各測線(IDグループ:10000, 20000...)ごとの始点と終点を抽出し、ラインを作成します。

  • IDごとにグループ化し、最初と最後を自動抽出
  • ラインを作成し、属性に連番(Seq_No)を付与
  • 始点・終点のX,Y座標(m単位)を属性テーブルに格納

スクリプトコード

import csv
from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsFeature,
    QgsGeometry,
    QgsField,
    QgsPointXY
)
from PyQt5.QtWidgets import QFileDialog, QMessageBox
from PyQt5.QtCore import QVariant

def create_transverse_lines_final():
    # 1. ファイル選択
    file_path, _ = QFileDialog.getOpenFileName(
        None, "CSVファイルを選択してください", "", "CSV Files (*.csv);;All Files (*)"
    )
    if not file_path: return

    # 2. データ読み込み
    groups = {}
    encodings = ['cp932', 'utf-8']
    rows = []
    
    loaded = False
    for enc in encodings:
        try:
            with open(file_path, 'r', encoding=enc, newline='') as f:
                reader = csv.DictReader(f)
                if not reader.fieldnames: continue
                field_map = {name.strip(): name for name in reader.fieldnames}
                id_col = next((k for k in field_map if 'ID' in k), None)
                il_col = next((k for k in field_map if 'ILData' in k), None)
                if not id_col or not il_col: continue
                for row in reader:
                    rows.append({'id_col': id_col, 'il_col': il_col, 'data': row})
                loaded = True
            break
        except UnicodeDecodeError: continue
            
    if not loaded: return

    # 3. グループごとの始点・終点抽出
    for item in rows:
        row = item['data']
        id_key = item['id_col']
        try:
            current_id_val = int(row[id_key])
            group_key = current_id_val // 10000
            if group_key == 0: continue 

            if group_key not in groups:
                groups[group_key] = {
                    'min_id': current_id_val, 'min_row': row,
                    'max_id': current_id_val, 'max_row': row
                }
            else:
                if current_id_val < groups[group_key]['min_id']:
                    groups[group_key]['min_id'] = current_id_val
                    groups[group_key]['min_row'] = row
                if current_id_val > groups[group_key]['max_id']:
                    groups[group_key]['max_id'] = current_id_val
                    groups[group_key]['max_row'] = row
        except ValueError: continue

    # 4. レイヤ作成(横断測線)
    crs = QgsProject.instance().crs().authid()
    if not crs: crs = "EPSG:6668"
    vl = QgsVectorLayer(f"LineString?crs={crs}", "横断測線", "memory")
    pr = vl.dataProvider()
    
    # 属性定義
    pr.addAttributes([
        QgsField("Seq_No", QVariant.Int),
        QgsField("Group_ID", QVariant.Int),
        QgsField("Start_ID", QVariant.Int),
        QgsField("End_ID", QVariant.Int),
        QgsField("Start_X", QVariant.Double),
        QgsField("Start_Y", QVariant.Double),
        QgsField("End_X", QVariant.Double),
        QgsField("End_Y", QVariant.Double)
    ])
    vl.updateFields()
    
    new_features = []
    il_col_name = rows[0]['il_col']
    sorted_group_keys = sorted(groups.keys())

    # 5. ライン作成
    for seq_index, g_key in enumerate(sorted_group_keys):
        g_val = groups[g_key]
        
        def get_xy(row_data):
            try:
                val_str = row_data[il_col_name]
                parts = val_str.split(',')
                if len(parts) < 4: return None
                # [2]=X, [3]=Y, mm->m
                x = float(parts[2]) / 1000.0
                y = float(parts[3]) / 1000.0
                return QgsPointXY(x, y)
            except: return None

        pt_start = get_xy(g_val['min_row'])
        pt_end = get_xy(g_val['max_row'])
        
        if pt_start and pt_end:
            line_geom = QgsGeometry.fromPolylineXY([pt_start, pt_end])
            feat = QgsFeature()
            feat.setGeometry(line_geom)
            feat.setAttributes([
                seq_index, g_key * 10000, g_val['min_id'], g_val['max_id'],
                pt_start.x(), pt_start.y(), pt_end.x(), pt_end.y()
            ])
            new_features.append(feat)

    pr.addFeatures(new_features)
    vl.updateExtents()
    QgsProject.instance().addMapLayer(vl)
    QMessageBox.information(None, "完了", "レイヤ「横断測線」を作成しました。\n属性テーブルを確認してください。")

create_transverse_lines_final()

自動作成された横断測線ライン

おわりに

当初はMDBを開くところでつまずき、ExcelとCSVを経由する方法で運用していました。この方法でも処理は可能でしたが、コピー、シート選択、CSV保存、ファイル選択といった作業が必要で、件数が多いほど手間や選択ミスが生じやすくなります。

今回の更新により、MDBを1回選択するだけで、縦断情報と横断情報を直接読み込み、2レイヤを一括作成できるようになりました。CSV作成が不要になったことで、作業時間の短縮だけでなく、古いCSVを使用するミスやコピー漏れの防止にもつながります。

なお、ODBCドライバーが利用できない環境に備え、従来のCSV版スクリプトも代替手段として残しています。

コメント

このブログの人気の投稿

IJCAD(AutoCAD互換CAD) コマンド早見表

設計定数を求めるための代表N値について

WEB地図(国土地理院など)のズームレベルについて