Skip to content

Latest commit

 

History

History
493 lines (406 loc) · 19.3 KB

File metadata and controls

493 lines (406 loc) · 19.3 KB

MapMetrics Flutter - Work Log

Session: 2025-01-17 - queryLayers Implementation & Bug Fixes

Objective

Implement queryLayers functionality to extract full POI properties from vector tiles on both iOS and Android platforms.

Changes Made

1. Pigeon API Definition Change

File: pigeons/pigeon.dart Line: 253 Change: Updated return type from nullable to non-nullable strings

// Before:
@async
List<Map<String, String?>> queryLayers(double x, double y);

// After:
@async
List<Map<String, String>> queryLayers(double x, double y);

Reason: Swift compiler cannot properly parse deeply nested generic types with optionals like Result<[[String: String?]], Error>. Simplified to non-nullable strings.

2. Regenerated Pigeon Code

Command: dart run pigeon --input pigeons/pigeon.dart Files Generated:

  • lib/src/platform/pigeon.g.dart (Dart)
  • ios/mapmetrics/Sources/maplibre_ios/Pigeon.g.swift (iOS Swift)
  • android/src/main/kotlin/com/github/mapmetrics/maplibre/Pigeon.g.kt (Android Kotlin)
  • linux/pigeon.g.cc (Linux C++)
  • linux/pigeon.g.h (Linux header)
  • windows/runner/pigeon.g.cpp (Windows C++)
  • windows/runner/pigeon.g.h (Windows header)

3. Fixed Android Implementation

File: android/src/main/kotlin/com/github/mapmetrics/maplibre/MapLibreMapController.kt Lines: 730-776 Changes:

  • Updated function signature to return List<Map<String, String>> (non-nullable)
  • Fixed JsonObject iteration methods from keys()/opt() to keySet()/get()
  • Added layer metadata extraction (layerId, sourceId, sourceLayer)
  • Extract all feature properties from vector tiles
// Fixed JsonObject iteration:
for (key in featureProperties.keySet()) {
    val value = featureProperties.get(key)
    properties[key] = value?.toString() ?: ""
}

4. Fixed iOS Implementation

File: ios/mapmetrics/Sources/maplibre_ios/MapViewDelegate.swift Lines: 4-5, 1236-1276 Changes:

  • Added typealias for complex nested generic type: typealias LayerPropertiesArray = [[String: String]]
  • Fixed function signature typo: changed Error] to Error> on line 1239
  • Updated to return [[String: String]] (non-nullable)
  • Implemented feature property extraction for all MLN feature types:
    • MLNPointFeature
    • MLNPolylineFeature
    • MLNPolygonFeature
    • MLNShapeCollectionFeature

5. Updated Platform Wrappers

Files:

  • lib/src/platform/ios/map_state.dart (line 325)
  • lib/src/platform/android/map_state.dart (line 386)
  • lib/src/platform/web/map_state.dart (lines 429-455)

Change: Updated all return types to Future<List<Map<String, String>>>

6. Fixed Dart Type Casting (CRITICAL FIX)

File: lib/src/platform/pigeon.g.dart Lines: 1214-1220 Problem: Platform channels return Map<Object?, Object?> which shallow .cast<>() cannot convert Solution: Implemented deep conversion

// Before (shallow cast - FAILS):
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<Map<String, String>>();

// After (deep conversion - WORKS):
return (pigeonVar_replyList[0] as List<Object?>).map((e) {
  final map = e as Map<Object?, Object?>;
  return map.map((key, value) => MapEntry(key.toString(), value.toString()));
}).toList();

Errors Fixed

Error 1: Swift Compiler - Nested Generic with Optional

Swift Compiler Error (Xcode): Expected '>' to complete generic argument list
completion: @escaping (Result<[[String: String?]], Error>) -> Void

Root Cause: Swift parser cannot handle Result<[[String: String?]], Error> Fix: Changed API to non-nullable strings

Error 2: Swift Compiler - Typo

Swift Compiler Error (Xcode): Expected '>' to complete generic argument list
completion: @escaping (Result<[[String: String]], Error]) -> Void

Root Cause: Typo - closing bracket ] instead of > Fix: Changed Error] to Error>

Error 3: Android Kotlin - JsonObject Methods

Unresolved reference 'keys'
Unresolved reference 'opt'

Root Cause: Used wrong JsonObject method names Fix: Changed from keys()/opt() to keySet()/get()

Error 4: Dart Type Casting - Platform Channel Types

type '_Map<Object?, Object?>' is not a subtype of type 'Map<String, String>' in type cast

Root Cause: Shallow .cast<>() doesn't work for nested platform channel types Fix: Implemented deep type conversion with .map() and MapEntry()

Build Status

iOS: Builds successfully ✅ Android: Builds successfully ✅ Type Safety: All type conversions properly handled

iOS Layer Metadata Fix (Session 2)

Problem: iOS queryLayers was finding features but missing layer metadata (layerId, sourceId, sourceLayer), causing Flutter code to filter them out as "No POI found"

Solution: Updated iOS implementation in MapViewDelegate.swift (lines 1236-1326) to:

  • Query each style layer individually using visibleFeatures(at:styleLayerIdentifiers:)
  • Extract layer metadata for each feature (layerId, sourceId, sourceLayer)
  • Match Android implementation structure for consistent cross-platform behavior

Changes:

// Now queries each layer and adds metadata:
properties["layerId"] = layer.identifier
properties["sourceId"] = source.identifier (from MLNVectorStyleLayer or MLNSymbolStyleLayer)
properties["sourceLayer"] = vectorLayer.sourceLayerIdentifier ?? ""

UI Enhancement (Session 3)

Problem: Dialog only displayed a hardcoded subset of POI properties

Solution: Updated poi_demo_page.dart to display ALL POI properties dynamically:

  • Organized properties into categories: important info, contact, opening hours, cuisine, address
  • Added "Additional Properties" section to display all other feature properties
  • Improved UX with emojis, better formatting, and scrollable content
  • Changed function signature from Map<String, String?> to Map<String, String> (non-nullable)

Changes:

Future<void> _showPOIDialog(Position point, Map<String, String> poiData) async {
  // Separate metadata properties from POI properties
  final metadataKeys = {'layerId', 'sourceId', 'sourceLayer'};
  final specialKeys = {'name', 'amenity', 'phone', 'opening_hours', 'cuisine', 'website', 'addr:street', 'addr:housenumber', 'addr:city'};

  // Get all POI properties (excluding metadata)
  final allProperties = <String, String>{};
  final specialProperties = <String, String>{};

  for (final entry in poiData.entries) {
    if (!metadataKeys.contains(entry.key) && entry.value.isNotEmpty) {
      if (specialKeys.contains(entry.key)) {
        specialProperties[entry.key] = entry.value;
      } else {
        allProperties[entry.key] = entry.value;
      }
    }
  }

  // Display special properties prominently with icons
  // Display all other properties in "Additional Properties" section
  // Show layer metadata at bottom for debugging
}

Testing Status

Pending: Need to test POI long-press functionality on iOS device/simulator ⏳ Pending: Need to verify all properties (name, phone, opening_hours, cuisine, amenity) are extracted and displayed on both platforms

Next Steps

  1. Complete iOS build
  2. Hot reload Flutter apps on both platforms
  3. Test long-press on POI icons
  4. Verify all feature properties are displayed correctly including opening_hours

Files Modified Summary

  • pigeons/pigeon.dart (API definition)
  • All Pigeon-generated files (7 files)
  • MapLibreMapController.kt (Android implementation)
  • MapViewDelegate.swift (iOS implementation)
  • 3 platform map_state.dart wrappers
  • lib/src/platform/pigeon.g.dart (manual deep conversion fix)

Key Learnings

  1. Swift compiler has limitations with deeply nested generic types containing optionals
  2. Platform channel types require deep conversion, not shallow casting
  3. JsonObject (Gson) uses keySet()/get(), not keys()/opt()
  4. Always use empty strings instead of null for better type safety across platforms

Session: 2025-01-18 - POI Icon Mapping & Rendering Fix

Objective

Expand POI icon mappings from 5 basic categories to all ~400 POI types defined in poi-mapping.json, fixing icon rendering failures caused by non-existent fallback icons.

Problem Discovery

Issue: Icons weren't rendering at all - only red circles visible User Report: "not all mappings are done correctly i jsut saw the leasure garden and it didnt had an icon" Root Cause: Most category default icons (like amenity-m, leisure-m, sport-m) don't exist in sprite sheet. When MapLibre can't find fallback icons, the entire icon-image expression fails and no icons render.

Changes Made

1. Dynamic Icon Expression Builder

File: example/lib/poi_demo_page.dart Lines: 125-213

Created _buildIconImageExpression() method that:

  • Loads poi-mapping.json at runtime (~400 mappings)
  • Generates MapLibre "match" expressions for each category
  • Handles all specific value-to-icon mappings (e.g., "fast_food": "fastfood-m")
  • Handles nested mappings (e.g., place_of_worship with religion subcategories)

Critical Fix - Safe Fallback Icons (lines 163-184):

if (matchCases.isNotEmpty) {
  final categoryDefault = categoryMappings['default'];
  String defaultValue;
  if (categoryDefault is String) {
    defaultValue = categoryDefault;
  } else {
    // Only use category-m if it exists in sprite sheet
    if (category == 'shop' || category == 'tourism' || category == 'office') {
      defaultValue = '$category-m';
    } else {
      defaultValue = 'tourism-m';  // Safe fallback that always exists
    }
  }

  expression.add([
    'image',
    ['match', ['get', category], ...matchCases, defaultValue]
  ]);
}

Why This Works:

  • Verified that only 3 category defaults exist in symbols.sdf sprite sheet: shop-m, tourism-m, office-m
  • All other categories (amenity, leisure, sport, etc.) use tourism-m as safe fallback
  • Expression structure: ['coalesce', ...tryEachCategory..., 'tourism-m']

2. Updated POI Layer Setup

File: example/lib/poi_demo_page.dart Lines: 287-302

Updated _setupPoiLayer() to use dynamic expression:

// Build icon expression from poi-mapping.json (handles all ~400 mappings)
final iconImageExpression = await _buildIconImageExpression();

await _styleController.addLayer(
  SymbolStyleLayer(
    id: 'poi-symbols',
    sourceId: 'poi-source',
    layout: {
      'source-layer': 'pois',
      'icon-image': iconImageExpression,  // Dynamic expression
      'icon-size': iconSize,
      'icon-allow-overlap': true,
      'icon-ignore-placement': true,
    },
  ),
);

3. Fixed Toggle Layer Function (CRITICAL)

File: example/lib/poi_demo_page.dart Lines: 501-570

Problem: _togglePoiLayer() was using OLD hardcoded expression with only 5 categories. When users toggled POI layer off/on, they would lose comprehensive mappings and revert to simple expression.

Solution: Updated _togglePoiLayer() to also use _buildIconImageExpression():

// Then add symbols on top with comprehensive icon mappings
// Build icon expression from poi-mapping.json (handles all ~400 mappings)
final iconImageExpression = await _buildIconImageExpression();

await _styleController.addLayer(
  SymbolStyleLayer(
    id: 'poi-symbols',
    sourceId: 'poi-source',
    layout: {
      'source-layer': 'pois',
      // Icon expression built dynamically from poi-mapping.json
      'icon-image': iconImageExpression,
      'icon-size': iconSize,
      'icon-allow-overlap': true,
      'icon-ignore-placement': true,
    },
  ),
);

Why Critical: Without this fix, toggling the layer would break icon rendering again

Sprite Sheet Analysis

File: example/assets/poi/symbols.sdf

Verified Icon Existence:

  • garden-m exists at line 147
  • fastfood-m exists (specific user example)
  • tourism-m exists at line 929 (safe fallback)
  • shop-m exists at line 930
  • office-m exists at line 775
  • amenity-m does NOT exist
  • leisure-m does NOT exist
  • sport-m does NOT exist

Build & Test Status

Android: Successfully built with BUILD_TIMESTAMP_2025_01_17_v2

  • All POI icons loading successfully (garden-m, fastfood-m, etc.)
  • Safe fallback logic applied correctly
  • Icons should now render on map (pending visual verification)

iOS: Needs rebuild to apply fix

  • Currently running old code (only fastfood-m)
  • Needs hot reload or full rebuild

Testing Steps

  1. Zoom in on map to POI level (zoom ≥14)
  2. Verify icons appear for various POI types across all categories
  3. Test specific examples:
    • leisure=garden should show garden-m icon
    • amenity=fast_food should show fastfood-m icon
    • shop=supermarket should show grocery-m icon
  4. Long-press on POIs to verify properties are still extracted correctly

Next Steps

  1. ✅ Document fix in WORK_LOG.md
  2. ⏳ Rebuild iOS to apply updated code
  3. ⏳ Visual verification that icons render on both platforms
  4. ⏳ Test comprehensive POI icon coverage across different categories

Verification Complete (Session 2025-01-18 continued)

All Three Components Integration Verified:

  1. poi-mapping.json - All ~400 mappings being used

    • _buildIconImageExpression() loads and parses all 18 categories at lines 125-213
    • Generates MapLibre match expressions for all specific value mappings
    • Safe fallback logic for categories without default icons (lines 163-184)
  2. symbols.png - Sprite sheet properly loaded

    • _loadIconsFromSprite() at lines 268-328 extracts icons from PNG
    • Uses rootBundle.load('assets/poi/symbols.png') to load sprite image
    • Creates individual icon PNGs and adds to MapLibre via addImage()
  3. symbols.sdf - Coordinates properly parsed

    • XML parsing with xml.XmlDocument.parse() at line 272
    • Extracts minX, minY, maxX, maxY coordinates for each icon
    • Coordinates used to clip icons from sprite sheet

Integration Flow:

  • poi-mapping.json → _buildIconImageExpression() → MapLibre icon-image expression
  • symbols.sdf → parsed for coordinates → used to extract icons
  • symbols.png → loaded as sprite sheet → icons extracted using SDF coords
  • All icons → addImage() → MapLibre style → displayed on map

Android Status: ✅ Running with BUILD_TIMESTAMP_2025_01_17_v2

  • App successfully restarted with comprehensive mappings
  • Icons loading from sprite sheet (bookmark-animals-m, bookmark-bar-m, etc.)
  • Both _setupPoiLayer() and _togglePoiLayer() using dynamic expression

iOS Status: ⏳ Needs rebuild to apply fix

Ready for Testing:

  • Zoom to POI level (zoom ≥14)
  • Verify icons display for all 18 categories
  • Test specific examples: leisure=garden, amenity=fast_food, shop=supermarket

Session: 2025-01-18 (continued) - Category Default Icons Fix

Objective

Add default icon values to all 18 categories in poi-mapping.json to prevent POIs from falling through to global tourism-m fallback when no specific mapping exists.

Problem Discovery

Issue: Shop cosmetics showing tourism-m icon instead of shop-m User Report: "this now defaults to the tourism icon" when viewing a cosmetics shop Root Cause: Only 2 out of 18 categories had default values defined in poi-mapping.json (shop and office)

Investigation

Verified which category-level icons exist in symbols.sdf sprite sheet:

  • office-m (line 238)
  • park-m (line 246)
  • power-m (line 283)
  • shop-m (line 309)
  • sports-m (line 322)
  • tourism-m (line 929)

Missing category icons: amenity-m, leisure-m, craft-m, healthcare-m, historic-m, natural-m, aeroway-m, railway-m, highway-m, public_transport-m, man_made-m, emergency-m, barrier-m (13 categories)

Changes Made

1. Added Default Values to poi-mapping.json

File: example/assets/poi/poi-mapping.json

Added "default" property to 16 categories that were missing them:

Category Defaults Using Existing Icons:

  • leisure: "park-m" (uses existing park-m icon)
  • sport: "sports-m" (uses existing sports-m icon)
  • power: "power-m" (uses existing power-m icon)

Category Defaults Using Safe Fallback:

  • amenity: "tourism-m"
  • tourism: "tourism-m"
  • craft: "tourism-m"
  • healthcare: "tourism-m"
  • historic: "tourism-m"
  • natural: "tourism-m"
  • aeroway: "tourism-m"
  • railway: "tourism-m"
  • highway: "tourism-m"
  • public_transport: "tourism-m"
  • man_made: "tourism-m"
  • emergency: "tourism-m"
  • barrier: "tourism-m"

Existing Defaults (unchanged):

  • shop: "shop-m"
  • office: "office-m"

Verification

cat poi-mapping.json | jq -r '.mappings | to_entries[] | .key + ": " + (if .value.default then "✅ " + .value.default else "❌ NO DEFAULT" end)'

Result: All 18 categories now have default values ✅

Testing Discovery - Asset File Not Reloaded

Issue: After hot restart (R), user tested POIs and found:

  • leisure: picnic_table → still showing tourism-m fallback
  • shop: "houseware" → still showing tourism-m fallback

Root Cause: Hot restart does NOT always reload asset files (poi-mapping.json). The app was still using the old version without category defaults.

Solution: Performed complete clean rebuild

Clean Rebuild Process

Triggered by: User request "clean the android emulator"

Steps Executed:

  1. Attempted uninstall: adb -s emulator-5554 uninstall com.github.mapmetrics.maplibre_example
    • Result: DELETE_FAILED_INTERNAL_ERROR (continued anyway)
  2. Flutter clean: flutter clean
  3. Removed build artifacts: rm -rf build .dart_tool android/build
  4. Fresh build: flutter run -d emulator-5554 --debug

Build Results:

  • Running Gradle task 'assembleDebug'... 20.8s ✅
  • Built build/app/outputs/flutter-apk/app-debug.apk ✅
  • Installing build/app/outputs/flutter-apk/app-debug.apk... 5.0s ✅
  • App launched successfully (process ID 15865) ✅
  • POI layer setup complete ✅
  • All icons loading from sprite sheet ✅

Testing Status

poi-mapping.json: All category defaults added ✅ Clean Rebuild: Completed successfully ✅ App Running: Android emulator (process 15865) ⏳ Visual Verification: User tested houseware shop again at 13:06:14 - ready for visual confirmation

User Testing

Test Case: Golden Bend shop (Herengracht 510, Amsterdam)

  • Properties: {shop: "houseware", name: "Golden Bend", description: "Specialized in porcelain plates"...}
  • Expected: Should now show shop-m icon (not tourism-m)
  • User tested at: 13:06:14 (after clean rebuild)

Expected Behavior After Fix

  • shop=cosmetics (unmapped) → will now show shop-m icon instead of tourism-m
  • shop=houseware (unmapped) → will now show shop-m icon instead of tourism-m
  • leisure=picnic_area (unmapped) → will now show park-m icon instead of tourism-m
  • leisure=picnic_table (mapped) → will show picnic_table-m icon (specific mapping exists)
  • sport=fitness (unmapped) → will now show sports-m icon instead of tourism-m
  • power=line (unmapped) → will now show power-m icon instead of tourism-m
  • All other unmapped POIs → will show tourism-m as safe fallback

Files Modified

  • example/assets/poi/poi-mapping.json - Added "default" property to 16 categories

Key Learning

Category defaults in poi-mapping.json are critical for proper icon fallback behavior. The _buildIconImageExpression() function uses these defaults when specific POI type mappings don't exist, preventing fallthrough to the global tourism-m fallback.