bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
317 lines
9.9 KiB
Python
317 lines
9.9 KiB
Python
"""
|
|
Landvex QGIS Plugin
|
|
Integrates IOM data into QGIS desktop application
|
|
"""
|
|
|
|
from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication
|
|
from qgis.PyQt.QtGui import QIcon
|
|
from qgis.PyQt.QtWidgets import QAction, QMessageBox
|
|
from qgis.core import (
|
|
QgsProject, QgsVectorLayer, QgsFeature, QgsGeometry,
|
|
QgsPointXY, QgsField, QgsSymbol, QgsRendererCategory,
|
|
QgsCategorizedSymbolRenderer, QgsMarkerSymbol
|
|
)
|
|
from PyQt5.QtCore import QVariant
|
|
import requests
|
|
import json
|
|
|
|
|
|
class LandvexQGISPlugin:
|
|
"""QGIS Plugin for Landvex Urban Intelligence"""
|
|
|
|
def __init__(self, iface):
|
|
self.iface = iface
|
|
self.plugin_dir = os.path.dirname(__file__)
|
|
self.actions = []
|
|
self.menu = "Landvex"
|
|
self.toolbar = self.iface.addToolBar("Landvex")
|
|
self.toolbar.setObjectName("LandvexToolbar")
|
|
|
|
# API Configuration
|
|
self.api_url = "https://api.landvex.com/v1"
|
|
self.api_key = None
|
|
|
|
def add_action(
|
|
self,
|
|
icon_path,
|
|
text,
|
|
callback,
|
|
enabled_flag=True,
|
|
add_to_menu=True,
|
|
add_to_toolbar=True,
|
|
status_tip=None,
|
|
whats_this=None,
|
|
parent=None
|
|
):
|
|
"""Add action to toolbar and menu"""
|
|
icon = QIcon(icon_path)
|
|
action = QAction(icon, text, parent)
|
|
action.triggered.connect(callback)
|
|
action.setEnabled(enabled_flag)
|
|
|
|
if status_tip is not None:
|
|
action.setStatusTip(status_tip)
|
|
|
|
if whats_this is not None:
|
|
action.setWhatsThis(whats_this)
|
|
|
|
if add_to_toolbar:
|
|
self.toolbar.addAction(action)
|
|
|
|
if add_to_menu:
|
|
self.iface.addPluginToMenu(self.menu, action)
|
|
|
|
self.actions.append(action)
|
|
return action
|
|
|
|
def initGui(self):
|
|
"""Initialize plugin GUI"""
|
|
# Load Observations action
|
|
self.add_action(
|
|
"icon.png",
|
|
text="Load Observations",
|
|
callback=self.load_observations,
|
|
parent=self.iface.mainWindow()
|
|
)
|
|
|
|
# Load RGI Layer action
|
|
self.add_action(
|
|
"icon.png",
|
|
text="Load RGI Layer",
|
|
callback=self.load_rgi_layer,
|
|
parent=self.iface.mainWindow()
|
|
)
|
|
|
|
# Load UMI Layer action
|
|
self.add_action(
|
|
"icon.png",
|
|
text="Load UMI Layer",
|
|
callback=self.load_umi_layer,
|
|
parent=self.iface.mainWindow()
|
|
)
|
|
|
|
# Settings action
|
|
self.add_action(
|
|
"icon.png",
|
|
text="Settings",
|
|
callback=self.show_settings,
|
|
parent=self.iface.mainWindow()
|
|
)
|
|
|
|
def unload(self):
|
|
"""Unload plugin"""
|
|
for action in self.actions:
|
|
self.iface.removePluginMenu(self.menu, action)
|
|
self.iface.removeToolBarIcon(action)
|
|
del self.toolbar
|
|
|
|
def load_observations(self):
|
|
"""Load observations from Landvex API"""
|
|
try:
|
|
# Fetch observations
|
|
response = requests.get(
|
|
f"{self.api_url}/observations",
|
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
params={"limit": 1000}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# Create vector layer
|
|
layer = QgsVectorLayer(
|
|
"Point?crs=EPSG:4326",
|
|
"Landvex Observations",
|
|
"memory"
|
|
)
|
|
|
|
# Add fields
|
|
provider = layer.dataProvider()
|
|
provider.addAttributes([
|
|
QgsField("id", QVariant.String),
|
|
QgsField("type", QVariant.String),
|
|
QgsField("condition", QVariant.Int),
|
|
QgsField("zoomer", QVariant.String),
|
|
QgsField("status", QVariant.String),
|
|
QgsField("date", QVariant.String),
|
|
QgsField("rgi", QVariant.Double),
|
|
])
|
|
layer.updateFields()
|
|
|
|
# Add features
|
|
features = []
|
|
for obs in data.get("observations", []):
|
|
feature = QgsFeature()
|
|
feature.setGeometry(QgsGeometry.fromPointXY(
|
|
QgsPointXY(obs["lng"], obs["lat"])
|
|
))
|
|
feature.setAttributes([
|
|
obs["id"],
|
|
obs["type"],
|
|
obs["condition"],
|
|
obs["zoomer"],
|
|
obs["status"],
|
|
obs["date"],
|
|
obs.get("rgi", 0),
|
|
])
|
|
features.append(feature)
|
|
|
|
provider.addFeatures(features)
|
|
layer.updateExtents()
|
|
|
|
# Style by condition
|
|
self.style_by_condition(layer)
|
|
|
|
# Add to project
|
|
QgsProject.instance().addMapLayer(layer)
|
|
|
|
QMessageBox.information(
|
|
self.iface.mainWindow(),
|
|
"Success",
|
|
f"Loaded {len(features)} observations"
|
|
)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(
|
|
self.iface.mainWindow(),
|
|
"Error",
|
|
f"Failed to load observations: {str(e)}"
|
|
)
|
|
|
|
def style_by_condition(self, layer):
|
|
"""Style layer by condition (1-5)"""
|
|
categories = []
|
|
|
|
# Condition 1: Excellent (Green)
|
|
symbol = QgsMarkerSymbol.createSimple({
|
|
"name": "circle",
|
|
"color": "#34C759",
|
|
"size": "6"
|
|
})
|
|
categories.append(QgsRendererCategory("1", symbol, "Excellent"))
|
|
|
|
# Condition 2: Good (Light Green)
|
|
symbol = QgsMarkerSymbol.createSimple({
|
|
"name": "circle",
|
|
"color": "#99D98C",
|
|
"size": "6"
|
|
})
|
|
categories.append(QgsRendererCategory("2", symbol, "Good"))
|
|
|
|
# Condition 3: Fair (Yellow)
|
|
symbol = QgsMarkerSymbol.createSimple({
|
|
"name": "circle",
|
|
"color": "#F9C74F",
|
|
"size": "6"
|
|
})
|
|
categories.append(QgsRendererCategory("3", symbol, "Fair"))
|
|
|
|
# Condition 4: Poor (Orange)
|
|
symbol = QgsMarkerSymbol.createSimple({
|
|
"name": "circle",
|
|
"color": "#F8961E",
|
|
"size": "6"
|
|
})
|
|
categories.append(QgsRendererCategory("4", symbol, "Poor"))
|
|
|
|
# Condition 5: Critical (Red)
|
|
symbol = QgsMarkerSymbol.createSimple({
|
|
"name": "circle",
|
|
"color": "#FF3B30",
|
|
"size": "8"
|
|
})
|
|
categories.append(QgsRendererCategory("5", symbol, "Critical"))
|
|
|
|
renderer = QgsCategorizedSymbolRenderer("condition", categories)
|
|
layer.setRenderer(renderer)
|
|
|
|
def load_rgi_layer(self):
|
|
"""Load RGI (Reality Gap Index) heatmap layer"""
|
|
try:
|
|
# Fetch RGI data for grid
|
|
response = requests.get(
|
|
f"{self.api_url}/indexes/rgi/grid",
|
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
|
params={"bounds": "17.5,59.0,19.0,60.0", "resolution": 100}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# Create grid layer
|
|
layer = QgsVectorLayer(
|
|
"Polygon?crs=EPSG:4326",
|
|
"RGI Heatmap",
|
|
"memory"
|
|
)
|
|
|
|
provider = layer.dataProvider()
|
|
provider.addAttributes([
|
|
QgsField("rgi", QVariant.Double),
|
|
QgsField("category", QVariant.String),
|
|
])
|
|
layer.updateFields()
|
|
|
|
# Add grid cells
|
|
features = []
|
|
for cell in data.get("grid", []):
|
|
feature = QgsFeature()
|
|
# Create polygon from bounds
|
|
points = [
|
|
QgsPointXY(cell["west"], cell["south"]),
|
|
QgsPointXY(cell["east"], cell["south"]),
|
|
QgsPointXY(cell["east"], cell["north"]),
|
|
QgsPointXY(cell["west"], cell["north"]),
|
|
QgsPointXY(cell["west"], cell["south"]),
|
|
]
|
|
feature.setGeometry(QgsGeometry.fromPolygonXY([points]))
|
|
feature.setAttributes([
|
|
cell["rgi"],
|
|
cell["category"],
|
|
])
|
|
features.append(feature)
|
|
|
|
provider.addFeatures(features)
|
|
layer.updateExtents()
|
|
|
|
# Style by RGI value
|
|
self.style_rgi_heatmap(layer)
|
|
|
|
QgsProject.instance().addMapLayer(layer)
|
|
|
|
QMessageBox.information(
|
|
self.iface.mainWindow(),
|
|
"Success",
|
|
f"Loaded RGI grid with {len(features)} cells"
|
|
)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(
|
|
self.iface.mainWindow(),
|
|
"Error",
|
|
f"Failed to load RGI: {str(e)}"
|
|
)
|
|
|
|
def style_rgi_heatmap(self, layer):
|
|
"""Style RGI heatmap with graduated colors"""
|
|
# Use graduated renderer for RGI values 0-10
|
|
# Low (green) → High (red)
|
|
pass # Implementation depends on QGIS version
|
|
|
|
def load_umi_layer(self):
|
|
"""Load UMI (Urban Morphology Index) layer"""
|
|
QMessageBox.information(
|
|
self.iface.mainWindow(),
|
|
"Info",
|
|
"UMI layer loading - implementation in progress"
|
|
)
|
|
|
|
def show_settings(self):
|
|
"""Show plugin settings dialog"""
|
|
from .settings_dialog import SettingsDialog
|
|
dialog = SettingsDialog(self.iface.mainWindow())
|
|
dialog.exec_()
|
|
|
|
|
|
def classFactory(iface):
|
|
"""Load plugin"""
|
|
return LandvexQGISPlugin(iface)
|