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
334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""
|
|
Mobile API Adapter — Optimizes IOM data for quiXzoom mobile app
|
|
Mobile-first: iPhone-first, always. One hand, one thumb, three seconds.
|
|
"""
|
|
|
|
from typing import Dict, List, Optional
|
|
from datetime import datetime
|
|
|
|
|
|
class MobileAdapter:
|
|
"""Adapts IOM data for mobile consumption"""
|
|
|
|
def __init__(self):
|
|
self.max_payload_size = 50 * 1024 # 50KB max per response
|
|
|
|
def adapt_observation(self, observation: Dict) -> Dict:
|
|
"""
|
|
Adapt observation for mobile app
|
|
Strip unnecessary fields, optimize images
|
|
"""
|
|
return {
|
|
"id": observation.get("id"),
|
|
"goid": observation.get("goid"),
|
|
"type": self._get_mobile_type(observation.get("goid", "")),
|
|
"condition": observation.get("overall_condition", 3),
|
|
"condition_label": self._get_condition_label(observation.get("overall_condition", 3)),
|
|
"location": {
|
|
"lat": observation.get("latitude"),
|
|
"lng": observation.get("longitude"),
|
|
"address": observation.get("address", "Unknown")
|
|
},
|
|
"thumbnail": observation.get("thumbnail_url"),
|
|
"findings_count": len(observation.get("findings", [])),
|
|
"timestamp": observation.get("timestamp"),
|
|
"synced": True
|
|
}
|
|
|
|
def adapt_rgi_for_mobile(self, rgi_result: Dict) -> Dict:
|
|
"""
|
|
Adapt RGI for mobile display
|
|
Simplified, visual, actionable
|
|
"""
|
|
return {
|
|
"location": rgi_result.get("location"),
|
|
"rgi_score": round(rgi_result.get("overall_rgi", 0)),
|
|
"rgi_color": self._get_rgi_color(rgi_result.get("overall_rgi", 0)),
|
|
"level": rgi_result.get("contradiction_level"),
|
|
"alert": rgi_result.get("overall_rgi", 0) > 50,
|
|
|
|
# Simplified subscores for mobile
|
|
"dimensions": [
|
|
{
|
|
"name": "Physical",
|
|
"score": round(rgi_result.get("subscores", {}).get("physical_reality_gap", {}).get("score", 0)),
|
|
"icon": "building"
|
|
},
|
|
{
|
|
"name": "Safety",
|
|
"score": round(rgi_result.get("subscores", {}).get("safety_reality_gap", {}).get("score", 0)),
|
|
"icon": "shield"
|
|
},
|
|
{
|
|
"name": "Economic",
|
|
"score": round(rgi_result.get("subscores", {}).get("economic_reality_gap", {}).get("score", 0)),
|
|
"icon": "dollar"
|
|
}
|
|
],
|
|
|
|
# Top contradiction for mobile alert
|
|
"top_contradiction": self._get_top_contradiction(rgi_result),
|
|
|
|
# Actionable insight
|
|
"insight": self._generate_insight(rgi_result)
|
|
}
|
|
|
|
def adapt_rcs_for_mobile(self, rcs_result: Dict) -> Dict:
|
|
"""
|
|
Adapt RCS for mobile display
|
|
Alert-style, immediate
|
|
"""
|
|
return {
|
|
"location": rcs_result.get("location"),
|
|
"rcs_score": round(rcs_result.get("rcs_score", 0)),
|
|
"alert_level": self._get_alert_level(rcs_result.get("rcs_score", 0)),
|
|
"alert_color": self._get_alert_color(rcs_result.get("rcs_score", 0)),
|
|
|
|
# Strongest contradiction
|
|
"strongest": self._get_strongest_contradiction(rcs_result),
|
|
|
|
# Count summary
|
|
"summary": {
|
|
"total": rcs_result.get("contradiction_count", 0),
|
|
"critical": rcs_result.get("critical_count", 0),
|
|
"major": rcs_result.get("major_count", 0)
|
|
}
|
|
}
|
|
|
|
def adapt_uli_for_mobile(self, uli_result: Dict) -> Dict:
|
|
"""
|
|
Adapt ULI for mobile
|
|
Visual layer representation
|
|
"""
|
|
layers = uli_result.get("layers", {})
|
|
|
|
return {
|
|
"location": uli_result.get("location"),
|
|
"uli_score": round(uli_result.get("uli", 0)),
|
|
"reality_count": uli_result.get("reality_count", 0),
|
|
|
|
# Visual layer bars
|
|
"layers": [
|
|
{
|
|
"name": "Formal",
|
|
"score": round(layers.get("formal", {}).get("score", 0)),
|
|
"color": "#4A90E2",
|
|
"dominant": layers.get("formal", {}).get("dominant", False)
|
|
},
|
|
{
|
|
"name": "Functional",
|
|
"score": round(layers.get("functional", {}).get("score", 0)),
|
|
"color": "#F5A623",
|
|
"dominant": layers.get("functional", {}).get("dominant", False)
|
|
},
|
|
{
|
|
"name": "Informal",
|
|
"score": round(layers.get("informal", {}).get("score", 0)),
|
|
"color": "#D0021B",
|
|
"dominant": layers.get("informal", {}).get("dominant", False)
|
|
}
|
|
],
|
|
|
|
# Dominant layer
|
|
"dominant_layer": uli_result.get("dominant_layer", "unknown"),
|
|
|
|
# Interpretation
|
|
"interpretation": uli_result.get("interpretation", "")
|
|
}
|
|
|
|
def adapt_dashboard_summary(self, data: Dict) -> Dict:
|
|
"""
|
|
Adapt dashboard data for mobile summary view
|
|
Cards-style, swipeable
|
|
"""
|
|
return {
|
|
"cards": [
|
|
{
|
|
"type": "rgi",
|
|
"title": "Reality Gap",
|
|
"score": round(data.get("rgi", 0)),
|
|
"trend": data.get("rgi_trend", "stable"),
|
|
"color": self._get_rgi_color(data.get("rgi", 0))
|
|
},
|
|
{
|
|
"type": "rcs",
|
|
"title": "Contradictions",
|
|
"score": round(data.get("rcs", 0)),
|
|
"count": data.get("contradiction_count", 0),
|
|
"color": self._get_alert_color(data.get("rcs", 0))
|
|
},
|
|
{
|
|
"type": "uli",
|
|
"title": "Urban Layers",
|
|
"score": round(data.get("uli", 0)),
|
|
"realities": data.get("reality_count", 0),
|
|
"color": "#9013FE"
|
|
}
|
|
],
|
|
"last_updated": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
def _get_mobile_type(self, goid: str) -> str:
|
|
"""Get mobile-friendly type from GOID"""
|
|
parts = goid.split("-")
|
|
if len(parts) >= 4:
|
|
type_map = {
|
|
"WIN": "Window",
|
|
"ROD": "Road",
|
|
"SGN": "Sign",
|
|
"LIG": "Light",
|
|
"CHA": "Charger",
|
|
"PAV": "Pavement",
|
|
"FEN": "Fence",
|
|
"PIP": "Pipe",
|
|
"CAB": "Cabinet",
|
|
"ANT": "Antenna"
|
|
}
|
|
return type_map.get(parts[3], parts[3])
|
|
return "Unknown"
|
|
|
|
def _get_condition_label(self, condition: int) -> str:
|
|
"""Get human-readable condition label"""
|
|
labels = {
|
|
1: "Excellent",
|
|
2: "Good",
|
|
3: "Fair",
|
|
4: "Poor",
|
|
5: "Critical"
|
|
}
|
|
return labels.get(condition, "Unknown")
|
|
|
|
def _get_rgi_color(self, score: float) -> str:
|
|
"""Get color for RGI score"""
|
|
if score < 20:
|
|
return "#4CAF50" # Green
|
|
elif score < 40:
|
|
return "#8BC34A" # Light green
|
|
elif score < 60:
|
|
return "#FFC107" # Yellow
|
|
elif score < 80:
|
|
return "#FF9800" # Orange
|
|
else:
|
|
return "#F44336" # Red
|
|
|
|
def _get_alert_level(self, score: float) -> str:
|
|
"""Get alert level"""
|
|
if score < 10:
|
|
return "low"
|
|
elif score < 30:
|
|
return "medium"
|
|
elif score < 50:
|
|
return "high"
|
|
else:
|
|
return "critical"
|
|
|
|
def _get_alert_color(self, score: float) -> str:
|
|
"""Get alert color"""
|
|
if score < 10:
|
|
return "#4CAF50"
|
|
elif score < 30:
|
|
return "#FFC107"
|
|
elif score < 50:
|
|
return "#FF9800"
|
|
else:
|
|
return "#F44336"
|
|
|
|
def _get_top_contradiction(self, rgi_result: Dict) -> Optional[Dict]:
|
|
"""Get top contradiction for mobile"""
|
|
contradictions = rgi_result.get("contradictions", [])
|
|
if contradictions:
|
|
top = contradictions[0]
|
|
return {
|
|
"type": top.get("type"),
|
|
"severity": top.get("severity"),
|
|
"description": top.get("description", "")[:100] # Truncate for mobile
|
|
}
|
|
return None
|
|
|
|
def _get_strongest_contradiction(self, rcs_result: Dict) -> Optional[Dict]:
|
|
"""Get strongest contradiction for mobile"""
|
|
strongest = rcs_result.get("strongest_contradictions", [])
|
|
if strongest:
|
|
top = strongest[0]
|
|
return {
|
|
"type": top.get("type"),
|
|
"severity": top.get("severity"),
|
|
"description": top.get("description", "")[:80]
|
|
}
|
|
return None
|
|
|
|
def _generate_insight(self, rgi_result: Dict) -> str:
|
|
"""Generate actionable insight"""
|
|
score = rgi_result.get("overall_rgi", 0)
|
|
|
|
if score < 20:
|
|
return "Low gap. Systems functioning as intended."
|
|
elif score < 40:
|
|
return "Moderate gap. Some systems need attention."
|
|
elif score < 60:
|
|
return "Significant gap. Multiple systems underperforming."
|
|
elif score < 80:
|
|
return "Major gap. Critical intervention needed."
|
|
else:
|
|
return "Critical gap. System failure likely."
|
|
|
|
|
|
# Example usage
|
|
def example_mobile_adaptation():
|
|
"""Example: Adapt data for mobile"""
|
|
adapter = MobileAdapter()
|
|
|
|
# Example RGI result
|
|
rgi = {
|
|
"location": "Bangkok Silom",
|
|
"overall_rgi": 38.77,
|
|
"contradiction_level": "medium",
|
|
"subscores": {
|
|
"physical_reality_gap": {"score": 63.33},
|
|
"operational_reality_gap": {"score": 16.67},
|
|
"safety_reality_gap": {"score": 0.0},
|
|
"institutional_reality_gap": {"score": 70.0},
|
|
"economic_reality_gap": {"score": 40.0},
|
|
"maintenance_reality_gap": {"score": 25.0},
|
|
"human_experience_gap": {"score": 20.0}
|
|
},
|
|
"contradictions": [
|
|
{"type": "planned_vs_constructed", "severity": "high", "description": "Planned 3 buildings but observed 5"}
|
|
]
|
|
}
|
|
|
|
mobile_rgi = adapter.adapt_rgi_for_mobile(rgi)
|
|
|
|
print("=== Mobile RGI ===")
|
|
print(f"Score: {mobile_rgi['rgi_score']}")
|
|
print(f"Color: {mobile_rgi['rgi_color']}")
|
|
print(f"Alert: {mobile_rgi['alert']}")
|
|
print(f"Insight: {mobile_rgi['insight']}")
|
|
|
|
# Example ULI
|
|
uli = {
|
|
"location": "Bangkok Silom",
|
|
"uli": 41.16,
|
|
"reality_count": 3,
|
|
"dominant_layer": "functional",
|
|
"interpretation": "3 realities coexist",
|
|
"layers": {
|
|
"formal": {"score": 75.0, "dominant": False},
|
|
"functional": {"score": 82.5, "dominant": True},
|
|
"informal": {"score": 45.0, "dominant": False}
|
|
}
|
|
}
|
|
|
|
mobile_uli = adapter.adapt_uli_for_mobile(uli)
|
|
|
|
print("\n=== Mobile ULI ===")
|
|
print(f"Score: {mobile_uli['uli_score']}")
|
|
print(f"Realities: {mobile_uli['reality_count']}")
|
|
print(f"Dominant: {mobile_uli['dominant_layer']}")
|
|
|
|
return mobile_rgi, mobile_uli
|
|
|
|
|
|
if __name__ == '__main__':
|
|
example_mobile_adaptation()
|