資料處理/地圖繪製#

3D 掃描建置地景模型(Luma AI)#

  • 於手機中下載 Luma 3D Capture

  • 依照軟體介面提示,環繞拍攝欲建置的物件

  • 上傳至 Luma 雲端 (上傳時間約 1 小時,視拍攝物件大小)

  • 於電腦 Luma AI - Interactive Scenes 網頁登入拍攝時所使用的帳號,檢視個人先前上傳的影像
    https://lumalabs.ai/interactive-scenes

  • 點選分享功能,內有可複製的 iframe code ,可嵌入於 jupyter book 中完成 3D 模型呈現

日治時期 Kinaji/Marikowan代表聚落位置圖#

  • 準備點位資料 (QGIS)

    建立新的 gpkg ,根據整理文獻所得,點出當時聚落位置。屬性表欄位包括聚落名稱、所屬社群

logo
點位示意圖 圖片來源:筆者照片
logo
屬性表示意圖 圖片來源:筆者照片
  • 匯入套件

from pathlib import Path
import re
import geopandas as gpd
import folium
import pandas as pd
  • 輸入/輸出路徑

DATA = Path("日治原社.gpkg")  
OUT  = Path("docs/_static/maps/kinaji_marikowan_points.html")
  • 讀取全部圖層,設定座標為 EPSG:4326

layers = gpd.io.file.fiona.listlayers(str(DATA))
gdfs = []
for lyr in layers:
    g = gpd.read_file(DATA, layer=lyr)
    if g.crs and g.crs.to_epsg() != 4326:
        g = g.to_crs(4326)
    g["__layer__"] = lyr
    gdfs.append(g)
gdf = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True), crs="EPSG:4326")
  • 給定欄位、中文族語對照資料

col_comm = "社群"
col_zh = "中文名"
col_at = "族語"
name_override = {
    "司馬庫斯":"Smangus", "斯馬庫斯":"Smangus",
    "宇老":"Uraw", "抬耀":"Tayax", "泰崗":"Thyakan",
    "田埔":"Tbahu", "石磊":"Quri", "錦路":"Kin lwan",
    "養老":"Yuluw", "馬美":"Mami", "馬里光":"Llyung",
    "拉號":"Rahaw", "鳥嘴":"Cyocuy", "鎮西堡":"Cinsbu",
}
  • 組合標籤顯示名稱

def to_label(row):
    zh = str(row[col_zh]).strip() if col_zh else ""
    at = str(row[col_at]).strip() if (col_at and pd.notna(row[col_at])) else ""
    if not at and zh in name_override:
        at = name_override[zh]
    if not at:
        at = zh
    return f"{at}{zh})" if zh else at

gdf["label_fmt"] = gdf.apply(to_label, axis=1)
  • 根據 gpkg 的點位屬性資料做社群分類

def grp(c):
    s = str(c).upper()
    if s.startswith("K"): return "Kinaji(基那吉)"
    if s.startswith("M"): return "Marikowan(馬里光)"
    return "未分類"

gdf["group_name"] = gdf[col_comm].apply(grp) if col_comm else "未分類"
  • 繪製地圖、設定底圖

center = [gdf.geometry.y.mean(), gdf.geometry.x.mean()]
m = folium.Map(location=center, zoom_start=10, control_scale=True, tiles=None)

folium.TileLayer("OpenStreetMap", name="OSM 現代地圖", overlay=False, control=True, show=True).add_to(m)
folium.TileLayer(
    tiles="https://gis.sinica.edu.tw/tileserver/file-exists.php?img=JM300K_1924-png-{z}-{x}-{y}",
    name="1924 日治臺灣全圖(XYZ)",
    attr="GIS Center, RCHSS, Academia Sinica — non-commercial use",
    overlay=False, control=True, show=False
).add_to(m)
  • 標點樣式設定

COLOR_K = "#1f77b4"  # 藍:Kinaji
COLOR_M = "#d62728"  # 紅:Marikowan
COLOR_U = "#7f7f7f"

bounds = []
for _, r in gdf.iterrows():
    if r.geometry is None:
        continue
    lat, lon = float(r.geometry.y), float(r.geometry.x)
    grp_name = r["group_name"]
    if grp_name.startswith("Kinaji"):
        fill = COLOR_K
    elif grp_name.startswith("Marikowan"):
        fill = COLOR_M
    else:
        fill = COLOR_U
    folium.CircleMarker(
        location=[lat, lon],
        radius=7,
        color="white",
        weight=1.5,
        fill=True, fill_color=fill, fill_opacity=0.95,
        tooltip=f"{r['label_fmt']} · {grp_name}",
        popup=f"<b>{r['label_fmt']}</b><br/>{grp_name}"
    ).add_to(m)
    bounds.append([lat, lon])
  • 圖例樣式設定,並且加入LayerControl功能,讓使用者可以切換底圖

<div style="...">
  <div style="font-weight:600;margin-bottom:6px;">代表社群</div>
  <div style="display:flex;align-items:center;margin:3px 0;">
    <span style="...background:#1f77b4..."></span>
    <span style="font-size:13px;">Kinaji(基那吉)</span>
  </div>
  <div style="display:flex;align-items:center;margin:3px 0;">
    <span style="...background:#d62728..."></span>
    <span style="font-size:13px;">Marikowan(馬里光)</span>
  </div>
</div>
"""
m.get_root().html.add_child(folium.Element(legend_html))
folium.LayerControl(position="topright", collapsed=False).add_to(m)
  • 調整地圖縮放比例、輸出

if bounds:
    m.fit_bounds(bounds)
    OUT.parent.mkdir(parents=True, exist_ok=True)
m.save(OUT)
print("Saved:", OUT)

司馬庫斯遷徙地圖#

  • 準備點位資料 (QGIS)

    建立新的 gpkg ,根據整理文獻所得,加入當時聚落的大致位置與名稱

  • 匯入套件

from pathlib import Path
import re
import geopandas as gpd
import folium
  • 輸入/輸出路徑

DATA = Path("tribe.gpkg")  # 點位 gpkg
OUT  = Path("docs/_static/maps/smangus_immigration.html")
  • 讀取圖層,設定座標為 EPSG:4326

all_layers = gpd.io.file.fiona.listlayers(str(DATA))
gdfs = []
for lyr in all_layers:
    g = gpd.read_file(DATA, layer=lyr)
    if g.crs and g.crs.to_epsg() != 4326:
        g = g.to_crs(4326)
    g["__layer__"] = lyr
    gdfs.append(g)
gdf = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True), crs="EPSG:4326")
  • 幫每一筆資料的所有文字欄位串在一起,方便用關鍵字篩選

def row_text(r):
    txts = []
    for c in gdf.columns:
        if c == gdf.geometry.name: 
            continue
        v = r.get(c, None)
        if isinstance(v, str):
            txts.append(v)
    return " ".join(txts)

gdf["__text__"] = gdf.apply(row_text, axis=1)
  • 建立一個空列表,用來存每個遷徙階段的資訊。並且標籤固定用「泰雅族語(中文)」的呈現格式

stages = []
def add(idx, num, sub=None):
    if idx is None: 
        return
    pt = gdf.iloc[idx].geometry
    stages.append({
        "stage": num, 
        "part": sub, 
        "lat": float(pt.y), 
        "lon": float(pt.x)
    })

add(idx_1, 1); add(idx_2, 2); add(idx_3, 3)
add(idx_4, 4); add(idx_5a, 5, 1); add(idx_5b, 5, 2); add(idx_6, 6)

def label_of(stage, part=None):
    if stage == 1: return "Pinsbkan(瑞岩)"
    if stage == 2: return "Quri Sqabu(思源埡口)"
  • 建立地圖、設定底圖

center = [gdf.geometry.y.mean(), gdf.geometry.x.mean()]
m = folium.Map(location=center, zoom_start=10, control_scale=True, tiles=None)

folium.TileLayer("OpenStreetMap", name="OSM 現代地圖", overlay=False, control=True, show=True).add_to(m)
folium.TileLayer(
    tiles="https://gis.sinica.edu.tw/tileserver/file-exists.php?img=JM300K_1924-png-{z}-{x}-{y}",
    name="1924 日治臺灣全圖(XYZ)",
    attr="GIS Center, RCHSS, Academia Sinica — non-commercial use",
    overlay=False, control=True, show=False
).add_to(m)
  • 設定點位標記樣式

palette = ["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"]

bounds = []
for r in stages:
    g = r["stage"]; p = r["part"]
    label_num = f"{g}-{p}" if p else f"{g}"
    color = palette[(g-1) % len(palette)]
    text = label_of(g, p)
    bounds.append([r["lat"], r["lon"]])
    html = f"""
    <div style="display:inline-block; padding:2px 8px; min-width:32px;
        background:{color}; border:2px solid #ffffff; color:#ffffff;
        border-radius:16px; font-weight:700; font-size:12px; line-height:20px;
        text-align:center;">{label_num}</div>"""
    folium.Marker(
        location=[r["lat"], r["lon"]],
        icon=folium.DivIcon(html=html),
        tooltip=f"{label_num}. {text}",
        popup=f"<b>{label_num}. {text}</b>"
    ).add_to(m)
  • 整理圖例中每個階段的名稱

items_by_stage = {}
for r in stages:
    key = r["stage"]                       
    items_by_stage.setdefault(key, [])
    disp = label_of(r["stage"], r["part"]) 
    num  = f"{r['stage']}-{r['part']}" if r["part"] else f"{r['stage']}"  
    s = f"{disp}{num})" if r["part"] else f"{disp}"
    if s not in items_by_stage[key]:
        items_by_stage[key].append(s)
  • 圖例樣式設定

legend_rows = []
for s in sorted(items_by_stage):
    color = palette[(s-1) % len(palette)]          
    names = "、".join(items_by_stage[s])           
    legend_rows.append(
        f'<div style="display:flex;align-items:center;margin:3px 0;">'
        f'<span style="display:inline-block;width:18px;height:18px;border-radius:9px;background:{color};'
        f'border:1px solid #fff;margin-right:6px;"></span>'
        f'<span style="font-size:13px;">{s}. {names}</span></div>'
    )
  • 將圖例插到地圖,並且加入LayerControl功能,讓使用者可以切換底圖

legend_html = f"""
<div style="position: fixed; bottom: 16px; right: 16px; z-index: 9999;
 background: white; padding: 10px 12px; border: 1px solid #ccc;
 border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,.3);
 font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Noto Sans TC', sans-serif;">
  <div style="font-weight:600;margin-bottom:6px;">遷徙階段</div>
  {''.join(legend_rows)}
</div>
"""
m.get_root().html.add_child(folium.Element(legend_html))
folium.LayerControl(position="topright", collapsed=False).add_to(m)
  • 調整地圖縮放比例、輸出

if bounds: 
    m.fit_bounds(bounds)             
OUT.parent.mkdir(parents=True, exist_ok=True)
m.save(OUT)                        
print("Saved:", OUT)