Skip to content
This repository has been archived by the owner on Apr 10, 2018. It is now read-only.

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

Closed
wants to merge 33 commits into from

Conversation

1ec5
Copy link
Contributor

@1ec5 1ec5 commented Dec 28, 2016

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 1ec5 self-assigned this Dec 28, 2016
@1ec5
Copy link
Contributor Author

1ec5 commented Dec 29, 2016

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

@1ec5 1ec5 changed the title [POC] Tool for generating Objective-C code from style diff [POC] Tool for generating Objective-C and Swift code from style diff Dec 29, 2016
@1ec5 1ec5 changed the title [POC] Tool for generating Objective-C and Swift code from style diff [POC] Tool for generating iOS/macOS runtime styling code from style diff Dec 31, 2016
@1ec5 1ec5 force-pushed the 1ec5-diff-darwin branch from a053175 to 45d8602 Compare January 5, 2017 00:16
}
let refLayerId;
let refLayerVar;
if ('ref' in layer) {
Copy link
Contributor Author

@1ec5 1ec5 Jan 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is incapable of copying over most aspects of a ref’d layer. A correct implementation would require a second pass over the diff, plus accessing the original style in order to get the required context in all cases. Alternatively, we’d have to enumerate all the layout and paint properties by name, which would bloat the code quite a bit. mapbox/mapbox-gl-native#7600 would make that a lot easier. Fortunately, we’re getting rid of ref support anyways in mapbox/mapbox-gl-native#6900.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style diffs with refs are as mess. In lib/diff.js I ignored that refs will ever exist. On the plus side, refs are going away anyway.

@lucaswoj
Copy link

lucaswoj commented Jan 5, 2017

What are some use cases you imagine this tool addressing?

@1ec5
Copy link
Contributor Author

1ec5 commented Jan 6, 2017

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

Renamed more properties according to mapbox/mapbox-gl-native#7603.
Copy link

@lucaswoj lucaswoj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good from an architectural standpoint. Let's merge once CI is 🍏.

MGLMapView.style is becoming optional in mapbox/mapbox-gl-native#7664. Avoid having to support optional chaining in Swift by referring to style directly, since that would be the name of the second parameter to -[MGLMapViewDelegate mapView:didFinishLoadingStyle:], which is the most likely place for the developer to put runtime styling code.
@1ec5 1ec5 changed the title [POC] Tool for generating iOS/macOS runtime styling code from style diff Tool for generating iOS/macOS runtime styling code from style diff Jan 11, 2017
@1ec5
Copy link
Contributor Author

1ec5 commented Jan 11, 2017

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
Copy link

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

@1ec5
Copy link
Contributor Author

1ec5 commented Jan 27, 2017

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.

@1ec5 1ec5 closed this Jan 27, 2017
@1ec5 1ec5 removed the in progress label Jan 27, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants