mapbox / mapbox-gl-style-spec

76 stars 38 forks source link

Tool for generating iOS/macOS runtime styling code from style diff #642

Closed 1ec5 closed 7 years ago

1ec5 commented 7 years ago

This is a proof of concept of a tool for transforming a style diff into Objective-C code compatible with the runtime styling API in the Mapbox iOS SDK or Mapbox macOS SDK. The code compiles but isn’t particularly idiomatic or efficient. Here’s an example:

{
    MGLSource *compositeSource = [mapView.style sourceWithIdentifier:@"composite"];
    MGLSymbolStyleLayer *placeResidentialLayer = [[MGLSymbolStyleLayer alloc] initWithIdentifier:@"place-residential" source:compositeSource];
    placeResidentialLayer.textLineHeight = [MGLStyleValue<NSNumber *> valueWithRawValue:@1.2];
    placeResidentialLayer.textSize = [MGLStyleValue<NSNumber *> valueWithStops:@{
        @10: [MGLStyleValue<NSNumber *> valueWithRawValue:@11],
        @18: [MGLStyleValue<NSNumber *> valueWithRawValue:@14],
    }];
    placeResidentialLayer.maximumTextAngle = [MGLStyleValue<NSNumber *> valueWithRawValue:@38];
    placeResidentialLayer.symbolSpacing = [MGLStyleValue<NSNumber *> valueWithRawValue:@250];
    placeResidentialLayer.textFont = [MGLStyleValue<NSArray<NSString *> *> valueWithRawValue:@[@"DIN Offc Pro Regular", @"Arial Unicode MS Regular"]];
    placeResidentialLayer.textPadding = [MGLStyleValue<NSNumber *> valueWithRawValue:@2];
    placeResidentialLayer.textOffset = [MGLStyleValue<NSValue *> valueWithRawValue:[NSValue valueWithCGVector:CGVectorMake(0, 0)]];
    placeResidentialLayer.textRotationAlignment = [MGLStyleValue<NSValue *> valueWithRawValue:[NSValue valueWithMGLTextRotationAlignment:MGLTextRotationAlignmentViewport]];
    placeResidentialLayer.textField = [MGLStyleValue<NSString *> valueWithRawValue:@"{name_en}"];
    placeResidentialLayer.maximumTextWidth = [MGLStyleValue<NSNumber *> valueWithRawValue:@7];
    placeResidentialLayer.textColor = [MGLStyleValue<UIColor *> valueWithRawValue:[UIColor colorWithRed:102 / 255.0 green:79 / 255.0 blue:61 / 255.0 alpha:1]];
    placeResidentialLayer.textHaloColor = [MGLStyleValue<UIColor *> valueWithRawValue:[UIColor colorWithRed:255 / 255.0 green:255 / 255.0 blue:255 / 255.0 alpha:1]];
    placeResidentialLayer.textHaloWidth = [MGLStyleValue<NSNumber *> valueWithRawValue:@1];
    placeResidentialLayer.textHaloBlur = [MGLStyleValue<NSNumber *> valueWithRawValue:@0.5];
    MGLStyleLayer *placeNeighbourhoodLayer = [mapView.style layerWithIdentifier:@"place-neighbourhood"];
    [mapView.style insertLayer:placeResidentialLayer belowLayer:placeNeighbourhoodLayer];
    MGLSymbolStyleLayer *mountainPeakLabelWithElevationLayer = [[MGLSymbolStyleLayer alloc] initWithIdentifier:@"mountain-peak-label-with-elevation" source:compositeSource];
    mountainPeakLabelWithElevationLayer.textLineHeight = [MGLStyleValue<NSNumber *> valueWithRawValue:@1.1];
    mountainPeakLabelWithElevationLayer.textSize = [MGLStyleValue<NSNumber *> valueWithStops:@{
        @10: [MGLStyleValue<NSNumber *> valueWithRawValue:@11],
        @18: [MGLStyleValue<NSNumber *> valueWithRawValue:@14],
    }];
    mountainPeakLabelWithElevationLayer.iconImageName = [MGLStyleValue<NSString *> valueWithRawValue:@"{maki}-15"];
    mountainPeakLabelWithElevationLayer.textFont = [MGLStyleValue<NSArray<NSString *> *> valueWithRawValue:@[@"DIN Offc Pro Medium", @"Arial Unicode MS Regular"]];
    mountainPeakLabelWithElevationLayer.textOffset = [MGLStyleValue<NSValue *> valueWithRawValue:[NSValue valueWithCGVector:CGVectorMake(0, 0.65)]];
    mountainPeakLabelWithElevationLayer.textAnchor = [MGLStyleValue<NSValue *> valueWithRawValue:[NSValue valueWithMGLTextAnchor:MGLTextAnchorTop]];
    mountainPeakLabelWithElevationLayer.textField = [MGLStyleValue<NSString *> valueWithRawValue:@"{name_en}, {elevation_m}m"];
    mountainPeakLabelWithElevationLayer.textLetterSpacing = [MGLStyleValue<NSNumber *> valueWithRawValue:@0.01];
    mountainPeakLabelWithElevationLayer.maximumTextWidth = [MGLStyleValue<NSNumber *> valueWithRawValue:@8];
    mountainPeakLabelWithElevationLayer.textColor = [MGLStyleValue<UIColor *> valueWithRawValue:[UIColor colorWithRed:34 / 255.0 green:102 / 255.0 blue:0 / 255.0 alpha:1]];
    mountainPeakLabelWithElevationLayer.textHaloColor = [MGLStyleValue<UIColor *> valueWithRawValue:[UIColor colorWithRed:255 / 255.0 green:255 / 255.0 blue:255 / 255.0 alpha:1]];
    mountainPeakLabelWithElevationLayer.textHaloWidth = [MGLStyleValue<NSNumber *> valueWithRawValue:@0.5];
    mountainPeakLabelWithElevationLayer.textHaloBlur = [MGLStyleValue<NSNumber *> valueWithRawValue:@0.5];
    MGLStyleLayer *poiScalerank2Layer = [mapView.style layerWithIdentifier:@"poi-scalerank2"];
    [mapView.style insertLayer:mountainPeakLabelWithElevationLayer belowLayer:poiScalerank2Layer];
}
…
{
    MGLVectorStyleLayer *bridgeOnewayArrowsWhiteLayer = (MGLVectorStyleLayer *)[mapView.style layerWithIdentifier:@"bridge-oneway-arrows-white"];
    bridgeOnewayArrowsWhiteLayer.predicate = [NSPredicate predicateWithFormat:@"%K = 'LineString' AND (class IN {'link', 'trunk'} AND oneway = 'true' AND structure = 'bridge' AND NOT type IN {'primary_link', 'secondary_link', 'tertiary_link'})", @"$type"];
    MGLStyleLayer *aerialwayLayer = [mapView.style layerWithIdentifier:@"aerialway"];
    [mapView.style removeLayer:aerialwayLayer];
}

In order to support the properties renamed as part of mapbox/mapbox-gl-native#6098, I’ve integrated the overridden properties into the JSON specification as an sdk-name member on each property. Extending this tool to support Swift would require for transforming the data into an intermediate, object-oriented representation before doing any string munging.

Depends on #641.

/cc @tmcw @scothis

1ec5 commented 7 years ago

The tool now generates more-or-less an AST of the statements necessary to reproduce the style commands. By default, it outputs the AST in JSON format, but the --language option translates the tree to Objective-C, Swift, or AppleScript.

The Swift is far from idiomatic. It doesn’t take advantage of type inference, which would simplify the code greatly:

_ = {
    let compositeSource = mapView.style.source(withIdentifier: "composite")!
    let placeResidentialLayer = MGLSymbolStyleLayer(identifier: "place-residential", source: compositeSource)
    placeResidentialLayer.textLineHeight = MGLStyleValue<NSNumber>(rawValue: 1.2)
    placeResidentialLayer.textSize = MGLStyleValue<NSNumber>(stops: [
        10: MGLStyleValue<NSNumber>(rawValue: 11),
        18: MGLStyleValue<NSNumber>(rawValue: 14),
    ])
    placeResidentialLayer.maximumTextAngle = MGLStyleValue<NSNumber>(rawValue: 38)
    placeResidentialLayer.symbolSpacing = MGLStyleValue<NSNumber>(rawValue: 250)
    placeResidentialLayer.textFont = MGLStyleValue<NSArray>(rawValue: ["DIN Offc Pro Regular", "Arial Unicode MS Regular"])
    placeResidentialLayer.textPadding = MGLStyleValue<NSNumber>(rawValue: 2)
    placeResidentialLayer.textOffset = MGLStyleValue<NSValue>(rawValue: NSValue(cgVector: CGVector(dx: 0, dy: 0)))
    placeResidentialLayer.textRotationAlignment = MGLStyleValue<NSValue>(rawValue: NSValue(mglTextRotationAlignment: .viewport))
    placeResidentialLayer.textField = MGLStyleValue<NSString>(rawValue: "{name_en}")
    placeResidentialLayer.maximumTextWidth = MGLStyleValue<NSNumber>(rawValue: 7)
    placeResidentialLayer.textColor = MGLStyleValue<UIColor>(rawValue: UIColor(red: 102 / 255.0, green: 79 / 255.0, blue: 61 / 255.0, alpha: 1))
    placeResidentialLayer.textHaloColor = MGLStyleValue<UIColor>(rawValue: UIColor(red: 255 / 255.0, green: 255 / 255.0, blue: 255 / 255.0, alpha: 1))
    placeResidentialLayer.textHaloWidth = MGLStyleValue<NSNumber>(rawValue: 1)
    placeResidentialLayer.textHaloBlur = MGLStyleValue<NSNumber>(rawValue: 0.5)
    let placeNeighbourhoodLayer = mapView.style.layer(withIdentifier: "place-neighbourhood")!
    mapView.style.insertLayer(placeResidentialLayer, below: placeNeighbourhoodLayer)
    let mountainPeakLabelWithElevationLayer = MGLSymbolStyleLayer(identifier: "mountain-peak-label-with-elevation", source: compositeSource)
    mountainPeakLabelWithElevationLayer.textLineHeight = MGLStyleValue<NSNumber>(rawValue: 1.1)
    mountainPeakLabelWithElevationLayer.textSize = MGLStyleValue<NSNumber>(stops: [
        10: MGLStyleValue<NSNumber>(rawValue: 11),
        18: MGLStyleValue<NSNumber>(rawValue: 14),
    ])
    mountainPeakLabelWithElevationLayer.iconImageName = MGLStyleValue<NSString>(rawValue: "{maki}-15")
    mountainPeakLabelWithElevationLayer.textFont = MGLStyleValue<NSArray>(rawValue: ["DIN Offc Pro Medium", "Arial Unicode MS Regular"])
    mountainPeakLabelWithElevationLayer.textOffset = MGLStyleValue<NSValue>(rawValue: NSValue(cgVector: CGVector(dx: 0, dy: 0.65)))
    mountainPeakLabelWithElevationLayer.textAnchor = MGLStyleValue<NSValue>(rawValue: NSValue(mglTextAnchor: .top))
    mountainPeakLabelWithElevationLayer.textField = MGLStyleValue<NSString>(rawValue: "{name_en}, {elevation_m}m")
    mountainPeakLabelWithElevationLayer.textLetterSpacing = MGLStyleValue<NSNumber>(rawValue: 0.01)
    mountainPeakLabelWithElevationLayer.maximumTextWidth = MGLStyleValue<NSNumber>(rawValue: 8)
    mountainPeakLabelWithElevationLayer.textColor = MGLStyleValue<UIColor>(rawValue: UIColor(red: 34 / 255.0, green: 102 / 255.0, blue: 0 / 255.0, alpha: 1))
    mountainPeakLabelWithElevationLayer.textHaloColor = MGLStyleValue<UIColor>(rawValue: UIColor(red: 255 / 255.0, green: 255 / 255.0, blue: 255 / 255.0, alpha: 1))
    mountainPeakLabelWithElevationLayer.textHaloWidth = MGLStyleValue<NSNumber>(rawValue: 0.5)
    mountainPeakLabelWithElevationLayer.textHaloBlur = MGLStyleValue<NSNumber>(rawValue: 0.5)
    let poiScalerank2Layer = mapView.style.layer(withIdentifier: "poi-scalerank2")!
    mapView.style.insertLayer(mountainPeakLabelWithElevationLayer, below: poiScalerank2Layer)
}()
…
_ = {
    let bridgeOnewayArrowsWhiteLayer = mapView.style.layer(withIdentifier: "bridge-oneway-arrows-white") as! MGLVectorStyleLayer
    bridgeOnewayArrowsWhiteLayer.predicate = NSPredicate(format: "%K = 'LineString' AND (class IN {'link', 'trunk'} AND oneway = 'true' AND structure = 'bridge' AND NOT type IN {'primary_link', 'secondary_link', 'tertiary_link'})", "$type")
    let aerialwayLayer = mapView.style.layer(withIdentifier: "aerialway")!
    mapView.style.removeLayer(aerialwayLayer)
}()

And for fun, AppleScript is almost there too:

set compositeSource to mapView's style's sourceWithIdentifier:"composite"
set placeResidentialLayer to (the current application's MGLSymbolStyleLayer's alloc)'s initWithIdentifier:"place-residential" source:compositeSource
tell placeResidentialLayer
    set its textLineHeight to the current application's MGLStyleValue's valueWithRawValue:1.2
    set its textSize to the current application's MGLStyleValue's valueWithStops:{ ¬
        |10|: the current application's MGLStyleValue's valueWithRawValue:11, ¬
        |18|: the current application's MGLStyleValue's valueWithRawValue:14 ¬
    }
    set its maximumTextAngle to the current application's MGLStyleValue's valueWithRawValue:38
    set its symbolSpacing to the current application's MGLStyleValue's valueWithRawValue:250
    set its textFont to the current application's MGLStyleValue's valueWithRawValue:{"DIN Offc Pro Regular", "Arial Unicode MS Regular"}
    set its textPadding to the current application's MGLStyleValue's valueWithRawValue:2
    set its textOffset to the current application's MGLStyleValue's valueWithRawValue:(the current application's NSValue's valueWithCGVector:{0, 0})
    set its textRotationAlignment to the current application's MGLStyleValue's valueWithRawValue:(the current application's NSValue's valueWithMGLTextRotationAlignment:the current application's MGLTextRotationAlignmentViewport)
    set its textField to the current application's MGLStyleValue's valueWithRawValue:"{name_en}"
    set its maximumTextWidth to the current application's MGLStyleValue's valueWithRawValue:7
end tell
tell placeResidentialLayer
    set its textColor to the current application's MGLStyleValue's valueWithRawValue:(the current application's UIColor's colorWithRed:102 / 255.0 green:79 / 255.0 blue:61 / 255.0 alpha:1)
    set its textHaloColor to the current application's MGLStyleValue's valueWithRawValue:(the current application's UIColor's colorWithRed:255 / 255.0 green:255 / 255.0 blue:255 / 255.0 alpha:1)
    set its textHaloWidth to the current application's MGLStyleValue's valueWithRawValue:1
    set its textHaloBlur to the current application's MGLStyleValue's valueWithRawValue:0.5
end tell
set placeNeighbourhoodLayer to mapView's style's layerWithIdentifier:"place-neighbourhood"
tell mapView's style to insertLayer:placeResidentialLayer belowLayer:placeNeighbourhoodLayer
set mountainPeakLabelWithElevationLayer to (the current application's MGLSymbolStyleLayer's alloc)'s initWithIdentifier:"mountain-peak-label-with-elevation" source:compositeSource
tell mountainPeakLabelWithElevationLayer
    set its textLineHeight to the current application's MGLStyleValue's valueWithRawValue:1.1
    set its textSize to the current application's MGLStyleValue's valueWithStops:{ ¬
        |10|: the current application's MGLStyleValue's valueWithRawValue:11, ¬
        |18|: the current application's MGLStyleValue's valueWithRawValue:14 ¬
    }
    set its iconImageName to the current application's MGLStyleValue's valueWithRawValue:"{maki}-15"
    set its textFont to the current application's MGLStyleValue's valueWithRawValue:{"DIN Offc Pro Medium", "Arial Unicode MS Regular"}
    set its textOffset to the current application's MGLStyleValue's valueWithRawValue:(the current application's NSValue's valueWithCGVector:{0, 0.65})
    set its textAnchor to the current application's MGLStyleValue's valueWithRawValue:(the current application's NSValue's valueWithMGLTextAnchor:the current application's MGLTextAnchorTop)
    set its textField to the current application's MGLStyleValue's valueWithRawValue:"{name_en}, {elevation_m}m"
    set its textLetterSpacing to the current application's MGLStyleValue's valueWithRawValue:0.01
    set its maximumTextWidth to the current application's MGLStyleValue's valueWithRawValue:8
end tell
tell mountainPeakLabelWithElevationLayer
    set its textColor to the current application's MGLStyleValue's valueWithRawValue:(the current application's UIColor's colorWithRed:34 / 255.0 green:102 / 255.0 blue:0 / 255.0 alpha:1)
    set its textHaloColor to the current application's MGLStyleValue's valueWithRawValue:(the current application's UIColor's colorWithRed:255 / 255.0 green:255 / 255.0 blue:255 / 255.0 alpha:1)
    set its textHaloWidth to the current application's MGLStyleValue's valueWithRawValue:0.5
    set its textHaloBlur to the current application's MGLStyleValue's valueWithRawValue:0.5
end tell
set poiScalerank2Layer to mapView's style's layerWithIdentifier:"poi-scalerank2"
tell mapView's style to insertLayer:mountainPeakLabelWithElevationLayer belowLayer:poiScalerank2Layer
…
set bridgeOnewayArrowsWhiteLayer to mapView's style's layerWithIdentifier:"bridge-oneway-arrows-white"
set bridgeOnewayArrowsWhiteLayer's iconOpacity to the current application's MGLStyleValue's valueWithRawValue:0.5
set bridgeOnewayArrowsWhiteLayer to mapView's style's layerWithIdentifier:"bridge-oneway-arrows-white"
set bridgeOnewayArrowsWhiteLayer's predicate to the current application's NSPredicate's predicateWithFormat_("%K = 'LineString' AND (class IN {'link', 'trunk'} AND oneway = 'true' AND structure = 'bridge' AND NOT type IN {'primary_link', 'secondary_link', 'tertiary_link'})", "$type")
set aerialwayLayer to mapView's style's layerWithIdentifier:"aerialway"
tell mapView's style to removeLayer:aerialwayLayer
lucaswoj commented 7 years ago

What are some use cases you imagine this tool addressing?

1ec5 commented 7 years ago

Tighter integration between Mapbox Studio and the iOS and macOS SDKs in the typical runtime styling development workflow.

1ec5 commented 7 years ago

Near as I can tell, the CI errors are caused by this repository’s eslint rules differing from those of GL JS, specifically the omission of no-var and es6. How do I enable those flags for just this file?

lucaswoj commented 7 years ago

@1ec5 It's easiest for me if you stick with this repository's eslint rules for the time being.

1ec5 commented 7 years ago

Closing. We intend to merge this repository into the mapbox/mapbox-gl-js repository imminently, and it doesn’t make sense to hold up that merge for this proof of concept.

My next step is to issue a similar PR against gl-native for this tool. It makes more sense for this tool to live alongside the formal APIs that its generated output makes use of; by contrast, its only relation to GL JS is that it’s written in the same language. Keeping the tool in the gl-native repository will enable it to draw directly from its existing runtime styling codegen files, allowing us to keep this tool in sync with the iOS and macOS SDKs with minimal overhead.

We’ll need to find a way for a JavaScript-based tool like Studio to make use of the tool. If it would be problematic for Studio to depend on gl-native, then we can put the tool in a separate repository that depends on gl-native either formally or informally.