البرمجة الجيومكانية

مرجع عملي منظّم بالمستويات: من تهيئة البيئة والأدوات، إلى أتمتة التحليل ونشر الخرائط والخدمات. جميع الأقسام تشرح الفكرة ثم خطوات التنفيذ ثم أمثلة كود قصيرة وواضحة.

المستوى 1 — بايثون داخل بيئة نظم المعلومات

الهدف: تجهيز بايثون، قراءة بيانات شبكية/خطية، تنفيذ عمليات شائعة، وتصدير النتائج.

تهيئةإنشاء بيئة عمل وتثبيت الحزم
يوصى باستخدام Conda/Miniconda. الفكرة: بيئة منفصلة لكل مشروع لتجنّب تعارض الإصدارات.
# إنشاء بيئة
conda create -n gis python=3.11 -y
conda activate gis

# حزم شائعة
conda install -c conda-forge geopandas pyogrio rtree shapely fiona -y
conda install -c conda-forge rasterio rioxarray xarray -y
pip install matplotlib contextily rich tqdm

ملاحظة: على Windows، تأكد من تثبيت Microsoft C++ Build Tools عند الحاجة لبعض الحزم.

قراءة/كتابةGeoPandas: قراءة ملفات شبكية/خطية

قراءة ملفات GeoPackage/GeoJSON/Shapefile، إصلاح CRS، حفظ نسخة نظيفة.

import geopandas as gpd

gdf = gpd.read_file("data/roads.gpkg", layer="primary_roads")
print(gdf.crs)   # افحص نظام الإحداثيات

# تعريف CRS إذا كان مفقودًا
if gdf.crs is None:
    gdf.set_crs(epsg=4326, inplace=True)

# تحويل مسقط إلى متري (UTM مثال)
gdf_m = gdf.to_crs(epsg=32638)

# حفظ
gdf_m.to_file("out/roads_clean.gpkg", layer="primary_roads", driver="GPKG")

قص طبقة على مضلع حدود، ثم تبسيط شكل هندسي لتخفيف الحجم.

study = gpd.read_file("data/study_boundary.geojson").to_crs(gdf_m.crs)
clipped = gpd.clip(gdf_m, study)

# تبسيط الشكل (يلزم CRS متري)
simple = clipped.copy()
simple["geometry"] = simple.geometry.simplify(tolerance=5)

simple.to_file("out/roads_clipped_simple.gpkg",
               layer="roads", driver="GPKG")
راسترRasterio/RioXarray: قراءة راستر شبكي وحساب مشتقات

فتح GeoTIFF شبكي وحساب إحصاءات سريعة.

import rasterio
from rasterio.plot import reshape_as_raster
import numpy as np

with rasterio.open("data/dem.tif") as src:
    dem = src.read(1)
    profile = src.profile
    print(profile["crs"], profile["transform"])
    print(np.nanmin(dem), np.nanmax(dem))

باستخدام rioxarray لاشتقاق انحدار تقريبي.

import rioxarray as rxr
import numpy as np

dem = rxr.open_rasterio("data/dem.tif").squeeze()
# تدرجات تقريبية (متر/بكسل)
dx = dem.differentiate("x")
dy = dem.differentiate("y")
slope = np.degrees(np.arctan(np.hypot(dx, dy)))

slope_da = dem.copy(data=slope)
slope_da.rio.to_raster("out/slope_deg.tif")

إذا أردت مؤشرات طبيعية مثل NDVI لاحقًا، ستجد نموذجًا جاهزًا في قسم المؤشرات أدناه ضمن بايثون.

ArcPyتشغيل أدوات ArcGIS Pro عبر بايثون

ArcPy يعمل من بيئة ArcGIS Pro Python. مثال: قص طبقة، ثم Buffer، ثم حساب طول الطريق ضمن منطقة الدراسة.

import arcpy
arcpy.env.overwriteOutput = True

study = r"C:\gis\study.gdb\boundary"
roads = r"C:\gis\data.gdb\roads"
out_gdb = r"C:\gis\out.gdb"

clip_out = arcpy.analysis.Clip(roads, study, f"{out_gdb}\\roads_clip")
buf_out  = arcpy.analysis.Buffer(clip_out, f"{out_gdb}\\roads_buf_50m", "50 Meters")

arcpy.management.AddField(clip_out, "len_m", "DOUBLE")
arcpy.management.CalculateGeometryAttributes(clip_out,
    [["len_m","LENGTH_GEODESIC"]], length_unit="METERS")
print("done.")

نصيحة: احفظ التسلسل كـ ModelBuilder ثم صدّره ككود بايثون لتوثيق خطوات التحليل.

المستوى 2 — جافاسكربت لخرائط الويب (Leaflet و ArcGIS JS)

الهدف: بناء خريطة ويب قابلة للتضمين، إضافة طبقات أساسية، وإظهار بيانات GeoJSON، وتحكمات أساسية.

Leafletخريطة أساس + طبقة GeoJSON

HTML بسيط مع Leaflet CDN. احفظ كملف مستقل لتجربة الفكرة.

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<div id="map" style="height:400px"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
  const map = L.map('map').setView([24.7,46.7], 6);
  const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
    {attribution:'OSM'}).addTo(map);
  fetch('data/points.geojson').then(r=>r.json()).then(g=>{
    L.geoJSON(g,{
      pointToLayer:(f,latlng)=>L.circleMarker(latlng,{radius:6,fillColor:"#6b4dff",color:"#89aaff",weight:1,fillOpacity:.9}),
      onEachFeature:(f,layer)=>layer.bindPopup(f.properties.name||"")
    }).addTo(map);
  });
</script>

تبديل الطبقات الأساسية والتحكم في العرض.

const imagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
  {attribution:'Esri'});
const topo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
  {attribution:'OpenTopo'});

const base = {"OSM":osm, "Imagery":imagery, "Topo":topo};
L.control.layers(base, null, {position:'topright'}).addTo(map);
ArcGIS JSخدمة FeatureLayer وإظهارها

مقتطف موجز لاستخدام ArcGIS Maps SDK for JS (نسخة 4.x).

<link rel="stylesheet" href="https://js.arcgis.com/4.29/esri/themes/light/main.css">
<div id="view" style="height:400px"></div>
<script src="https://js.arcgis.com/4.29/"></script>
<script>
require(["esri/Map","esri/views/MapView","esri/layers/FeatureLayer"],(Map,MapView,FeatureLayer)=>{
  const map = new Map({basemap:"streets-navigation-vector"});
  const view = new MapView({container:"view", map, center:[46.7,24.7], zoom:6});
  const fl = new FeatureLayer({url:"YOUR/FeatureServer/0"});
  map.add(fl);
});
</script>

يمكن ضبط الرموز والـ popups والفلترة من خصائص FeatureLayer.

المستوى 3 — أتمتة التقارير والخرائط

الهدف: تنفيذ سلسلة مهام تلقائيًا، توليد خرائط وصور وملفات نهائية في مجلد واحد.

وظائفبايثون: مهام دورية وتوليد صور

نموذج سكربت: يقرأ طبقة، يحسب طول الطرق لكل حي، يصدّر CSV وصورة بسيطة.

import geopandas as gpd
import matplotlib.pyplot as plt

districts = gpd.read_file("data/districts.gpkg").to_crs(epsg=32638)
roads     = gpd.read_file("data/roads.gpkg").to_crs(districts.crs)

length = roads.length.groupby(roads.sindex.query_bulk(districts.geometry, predicate="intersects")[1]).sum()
districts["roads_km"] = (length.reindex(range(len(districts))).fillna(0) / 1000).values

districts[["name","roads_km"]].to_csv("out/roads_by_district.csv", index=False)

ax = districts.plot(column="roads_km", cmap="viridis", legend=True, figsize=(6,6))
plt.tight_layout(); plt.savefig("out/roads_map.png", dpi=180)

شغّليه يدويًا، أو عبر جدولة النظام (Task Scheduler/cron) لتنفيذ يومي/أسبوعي.

المستوى 4 — PostGIS (قواعد البيانات المكانية)

الهدف: إنشاء قاعدة مكانية، استيراد بيانات، فهارس مكانية، استعلامات شائعة وأداء جيد.

تهيئةإنشاء قاعدة وتفعيل PostGIS

أوامر psql أولية:

CREATE DATABASE gisdb;
\c gisdb;
CREATE EXTENSION postgis;
-- تحقق
SELECT postgis_full_version();

استيراد Shapefile/GeoJSON عبر ogr2ogr:

ogr2ogr -f "PostgreSQL" PG:"dbname=gisdb user=postgres password=*" \
  roads.shp -nln public.roads -nlt PROMOTE_TO_MULTI -lco GEOMETRY_NAME=geom -lco FID=gid -overwrite

يفضل التخزين بـ SRID مناسب (UTM لمنطقتك) لسهولة القياسات بالأمتار.

أداءفهارس مكانية واستعلامات

إنشاء فهرس GiST:

CREATE INDEX idx_roads_geom ON public.roads USING GIST(geom);

تقطيع المسار حسب حدود الدراسة:

CREATE TABLE roads_clip AS
SELECT r.*
FROM public.roads r
JOIN public.boundary b
ON ST_Intersects(r.geom, b.geom);

أطوال الطرق داخل كل حي:

SELECT d.id, d.name,
       SUM(ST_Length(ST_Intersection(r.geom, d.geom)))/1000 AS km_len
FROM districts d
JOIN roads r
  ON ST_Intersects(r.geom, d.geom)
GROUP BY d.id, d.name
ORDER BY km_len DESC;
تحليلأمثلة سريعة إضافية
المهمةالاستعلام
أقرب طريق لنقطة
SELECT r.*
FROM roads r
ORDER BY r.geom <-> ST_SetSRID(ST_MakePoint(46.7,24.7),4326)
LIMIT 1;
مضلعات التغطية لمسافات 5/10 دقائق (بافتراض سرعة)
-- يحتاج شبكة طرق وحقول سرعة/زمن. عادة يتم عبر pgRouting أو أدوات خارجية.

المستوى 5 — GeoServer ونشر WMS/WFS

الهدف: نشر طبقاتك كخدمات خرائط قياسية وربطها بتطبيقات الويب.

نشرخطوات أساسية
  1. تثبيت GeoServer (يفضل الإصدار المتوافق مع JRE حديث).
  2. إضافة Store من نوع PostGIS وربطه بقاعدة gisdb.
  3. إنشاء Layer لكل جدول مكاني وتعيين SRS صحيح.
  4. اختبار WMS/WFS من تبويب Preview.

عند الاستهلاك من JavaScript، استخدمي عناوين WMS/WFS/WMTS الرسمية وقيود CORS حسب المضيف.

أسلوبنمط بسيط SLD لخطوط
<sld:StyledLayerDescriptor version="1.0.0"
 xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc">
  <sld:NamedLayer>
    <sld:Name>roads</sld:Name>
    <sld:UserStyle>
      <sld:FeatureTypeStyle>
        <sld:Rule>
          <sld:LineSymbolizer>
            <sld:Stroke>
              <sld:CssParameter name="stroke">#6b4dff</sld:CssParameter>
              <sld:CssParameter name="stroke-width">1.5</sld:CssParameter>
            </sld:Stroke>
          </sld:LineSymbolizer>
        </sld:Rule>
      </sld:FeatureTypeStyle>
    </sld:UserStyle>
  </sld:NamedLayer>
</sld:StyledLayerDescriptor>

المستوى 6 — التعلم العميق مع الصور الفضائية (مقدمة عملية)

الهدف: فهم سلسلة العمل: إعداد البيانات، تجزئة الصور إلى مربعات، تقسيم تدريب/تحقق، تدريب نموذج بسيط للتقسيم الدلالي.

إعدادتقطيع بيانات شبكية إلى مربعات
import rasterio
from rasterio.windows import Window
from pathlib import Path
import numpy as np

src_path = "data/imagery.tif"
out_dir  = Path("out/tiles"); out_dir.mkdir(parents=True, exist_ok=True)

tile = 512
with rasterio.open(src_path) as src:
    for r in range(0, src.height, tile):
        for c in range(0, src.width, tile):
            w = Window(c, r, min(tile, src.width-c), min(tile, src.height-r))
            arr = src.read(window=w)
            if arr.size == 0: continue
            profile = src.profile
            profile.update({"height":arr.shape[1], "width":arr.shape[2],
                            "transform": rasterio.windows.transform(w, src.transform)})
            out = out_dir / f"tile_{r}_{c}.tif"
            with rasterio.open(out, "w", **profile) as dst:
                dst.write(arr)

بعدها استخدم أداة تعليم (Labeling) لرسم الأقنعة، ثم قسم إلى train/val.

تدريبهيكل مختصر للتدريب بـ PyTorch
# ملاحظة: هذا مخطط هيكلي مبسط (ليس جاهزًا للإنتاج)
import torch, torch.nn as nn, torch.optim as optim

class MiniUNet(nn.Module):
    def _init(self): super().init_()
    # طبقات مختصرة جداً للعرض فقط

# dataset/dataloader لقراءة المربعات (X: صور، y: أقنعة)
# loop: train for N epochs, save best model

# الحفظ:
# torch.save(model.state_dict(), "out/model.pt")

في المشاريع الفعلية يُفضّل استخدام مكتبات جاهزة لـ Remote Sensing مثل torchgeo أو segmentation-models-pytorch مع ضبط Augmentations ومقاييس التقييم.

ملحق — وصفات جاهزة مختصرة

PythonNDVI من مشاهد شبكية متعددة
import rasterio
import numpy as np

with rasterio.open("data/S2_B08.tif") as nir, rasterio.open("data/S2_B04.tif") as red:
    N = nir.read(1).astype("float32")
    R = red.read(1).astype("float32")
    ndvi = (N - R) / (N + R + 1e-6)
    profile = nir.profile; profile.update(dtype="float32")
    with rasterio.open("out/ndvi.tif","w",**profile) as dst:
        dst.write(ndvi, 1)
Leafletتحميل WMS من GeoServer
const wms = L.tileLayer.wms("https://your-geoserver/geoserver/wms",{
  layers:"workspace:roads",
  format:"image/png",
  transparent:true
}).addTo(map);
PostGISمراكز ثقل المضلعات
SELECT id, ST_PointOnSurface(geom) AS label_pt
INTO labels
FROM parcels;