aboutsummaryrefslogtreecommitdiffstats
path: root/generate-elm.js
diff options
context:
space:
mode:
authorJakub Hampl <kopomir@gmail.com>2018-06-19 09:05:14 +0100
committerJakub Hampl <kopomir@gmail.com>2018-06-19 09:05:14 +0100
commit5fdcf1fbe9a56951799d89bfc43e286742a2495f (patch)
tree189ebccd30cba8ff373ee67ff69a2552e534de8f /generate-elm.js
Initial commit
Diffstat (limited to 'generate-elm.js')
-rw-r--r--generate-elm.js359
1 files changed, 359 insertions, 0 deletions
diff --git a/generate-elm.js b/generate-elm.js
new file mode 100644
index 0000000..1643428
--- /dev/null
+++ b/generate-elm.js
@@ -0,0 +1,359 @@
+function generateProperties(spec) {
+ const layouts = spec.layout;
+ const paints = spec.paint;
+ var codes = {};
+ var docs = {};
+ var enums = {};
+ layouts.forEach(l => {
+ const layerType = titleCase(l.split('_')[1])
+ docs[layerType] = [];
+ codes[layerType] = [];
+ Object.entries(spec[l]).forEach(([name, prop]) => {
+ if (name == 'visibility') return '';
+ if (prop.type === 'enum') {
+ enums[name] = Object.keys(prop.values).join(' | ');
+ }
+ codes[layerType].push(generateElmProperty(name, prop, layerType, 'Layout'));
+ docs[layerType].push(camelCase(name));
+ })
+ })
+ paints.forEach(l => {
+ const layerType = titleCase(l.split('_')[1])
+ Object.entries(spec[l]).forEach(([name, prop]) => {
+ if (name == 'visibility') return '';
+ if (prop.type === 'enum') {
+ enums[name] = Object.keys(prop.values).join(' | ');
+ }
+ codes[layerType].push(generateElmProperty(name, prop, layerType, 'Paint'))
+ docs[layerType].push(camelCase(name));
+ })
+ })
+ Object.values(docs).forEach(d => d.sort())
+ Object.values(codes).forEach(d => d.sort());
+ console.log(enums);
+ return `
+module Mapbox.Layer exposing (
+ Layer, SourceId, Background, Fill, Symbol, Line, Raster, Circle, FillExtrusion, Heatmap, Hillshade, LayerAttr,
+ encode,
+ background, fill, symbol, line, raster, circle, fillExtrusion, heatmap, hillshade,
+ metadata, source, sourceLayer, minzoom, maxzoom, filter, visible,
+ ${Object.values(docs).map(d => d.join(', ')).join(',\n ')})
+{-|
+Layers specify what is actually rendered on the map and are rendered in order.
+
+Except for layers of the background type, each layer needs to refer to a source. Layers take the data that they get from a source, optionally filter features, and then define how those features are styled.
+
+There are two kinds of properties: *Layout* and *Paint* properties.
+
+Layout properties are applied early in the rendering process and define how data for that layer is passed to the GPU. Changes to a layout property require an asynchronous "layout" step.
+
+Paint properties are applied later in the rendering process. Changes to a paint property are cheap and happen synchronously.
+
+
+### Working with layers
+
+@docs Layer, SourceId, encode
+
+### Layer Types
+
+@docs background, fill, symbol, line, raster, circle, fillExtrusion, heatmap, hillshade
+@docs Background, Fill, Symbol, Line, Raster, Circle, FillExtrusion, Heatmap, Hillshade
+
+### General Attributes
+
+@docs LayerAttr
+@docs metadata, source, sourceLayer, minzoom, maxzoom, filter, visible
+
+${Object.entries(docs).map(([section, docs]) => `### ${section} Attributes\n\n@docs ${docs.join(', ')}`).join('\n\n')}
+-}
+
+import Array exposing (Array)
+import Json.Decode
+import Json.Encode as Encode exposing (Value)
+import Mapbox.Expression as Expression exposing (Anchor, CameraExpression, Color, DataExpression, Expression, LineJoin)
+
+{-| Represents a layer. -}
+type Layer
+ = Layer Value
+
+{-| All layers (except background layers) need a source -}
+type alias SourceId = String
+
+{-| -}
+type Background
+ = BackgroundLayer
+
+{-| -}
+type Fill
+ = FillLayer
+
+{-| -}
+type Symbol
+ = SymbolLayer
+
+{-| -}
+type Line
+ = LineLayer
+
+{-| -}
+type Raster
+ = RasterLayer
+
+{-| -}
+type Circle
+ = CircleLayer
+
+{-| -}
+type FillExtrusion
+ = FillExtrusionLayer
+
+{-| -}
+type Heatmap
+ = HeatmapLayer
+
+{-| -}
+type Hillshade
+ = HillshadeLayer
+
+{-| Turns a layer into JSON -}
+encode : Layer -> Value
+encode (Layer value) =
+ value
+
+
+
+
+layerImpl tipe source id attrs =
+ [ ( "type", Encode.string tipe )
+ , ( "id", Encode.string id )
+ , ( "source", Encode.string source)
+ ]
+ ++ encodeAttrs attrs
+ |> Encode.object
+ |> Layer
+
+
+encodeAttrs attrs =
+ let
+ { top, layout, paint } =
+ List.foldl
+ (\\attr ({ top, layout, paint } as lists) ->
+ case attr of
+ Top key val ->
+ { lists | top = ( key, val ) :: top }
+
+ Paint key val ->
+ { lists | paint = ( key, val ) :: paint }
+
+ Layout key val ->
+ { lists | layout = ( key, val ) :: layout }
+ )
+ { top = [], layout = [], paint = [] }
+ attrs
+ in
+ ( "layout", Encode.object layout ) :: ( "paint", Encode.object paint ) :: top
+
+{-| The background color or pattern of the map. -}
+background : String -> List (LayerAttr Background) -> Layer
+background tipe id attrs =
+ [ ( "type", Encode.string "background" )
+ , ( "id", Encode.string id )
+ ]
+ ++ encodeAttrs attrs
+ |> Encode.object
+ |> Layer
+
+{-| A filled polygon with an optional stroked border. -}
+fill : String -> SourceId -> List (LayerAttr Fill) -> Layer
+fill =
+ layerImpl "fill"
+
+{-| A stroked line. -}
+line : String -> SourceId -> List (LayerAttr Line) -> Layer
+line =
+ layerImpl "line"
+
+{-| An icon or a text label. -}
+symbol : String -> SourceId -> List (LayerAttr Symbol) -> Layer
+symbol =
+ layerImpl "symbol"
+
+{-| Raster map textures such as satellite imagery. -}
+raster : String -> SourceId -> List (LayerAttr Raster) -> Layer
+raster =
+ layerImpl "raster"
+
+{-| A filled circle. -}
+circle : String -> SourceId -> List (LayerAttr Circle) -> Layer
+circle =
+ layerImpl "circle"
+
+{-| An extruded (3D) polygon. -}
+fillExtrusion : String -> SourceId -> List (LayerAttr FillExtrusion) -> Layer
+fillExtrusion =
+ layerImpl "fill-extrusion"
+
+{-| A heatmap. -}
+heatmap : String -> SourceId -> List (LayerAttr Heatmap) -> Layer
+heatmap =
+ layerImpl "heatmap"
+
+{-| Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB and Mapzen Terrarium tiles. -}
+hillshade : String -> SourceId -> List (LayerAttr Hillshade) -> Layer
+hillshade =
+ layerImpl "hillshade"
+
+{-| -}
+type LayerAttr tipe
+ = Top String Value
+ | Paint String Value
+ | Layout String Value
+
+
+
+-- General Attributes
+
+{-| Arbitrary properties useful to track with the layer, but do not influence rendering. Properties should be prefixed to avoid collisions, like 'mapbox:'. -}
+metadata : Value -> LayerAttr all
+metadata =
+ Top "metadata"
+
+
+{-| Layer to use from a vector tile source. Required for vector tile sources; prohibited for all other source types, including GeoJSON sources. -}
+sourceLayer : String -> LayerAttr all
+sourceLayer =
+ Encode.string >> Top "source-layer"
+
+{-| The minimum zoom level for the layer. At zoom levels less than the minzoom, the layer will be hidden. A number between 0 and 24 inclusive. -}
+minzoom : Float -> LayerAttr all
+minzoom =
+ Encode.float >> Top "minzoom"
+
+{-| The maximum zoom level for the layer. At zoom levels equal to or greater than the maxzoom, the layer will be hidden. A number between 0 and 24 inclusive. -}
+maxzoom : Float -> LayerAttr all
+maxzoom =
+ Encode.float >> Top "maxzoom"
+
+{-| A expression specifying conditions on source features. Only features that match the filter are displayed. -}
+filter : Expression any Bool -> LayerAttr all
+filter =
+ Expression.encode >> Top "filter"
+
+{-| Whether this layer is displayed. -}
+visible : Expression CameraExpression Bool -> LayerAttr any
+visible vis =
+ Layout "visibility" <| Expression.encode <| Expression.ifElse vis (Expression.str "visible") (Expression.str "none")
+
+${Object.entries(codes).map(([section, codes]) => `-- ${section}\n\n${codes.join('\n')}`).join('\n\n')}
+`
+}
+
+function requires(req) {
+ if (typeof req === 'string') {
+ return `Requires \`${camelCase(req)}\`.`;
+ } else if (req['!']) {
+ return `Disabled by \`${camelCase(req['!'])}\`.`;
+ } else if (req['<=']) {
+ return `Must be less than or equal to \`${camelCase(req['<='])}\`.`;
+ } else {
+ const [name, value] = Object.entries(req)[0];
+ if (Array.isArray(value)) {
+ return `Requires \`${camelCase(name)}\` to be ${
+ value
+ .reduce((prev, curr) => [prev, ', or ', curr])}.`;
+ } else {
+ return `Requires \`${camelCase(name)}\` to be \`${value}\`.`;
+ }
+ }
+}
+
+function generateElmProperty(name, prop, layerType, position) {
+ if (name == 'visibility') return ''
+ if (prop['property-type'] === 'constant') throw "Constant property type not supported";
+ const elmName = camelCase(name);
+ const exprKind = prop['sdk-support']['data-driven styling'] && prop['sdk-support']['data-driven styling'].js ? 'any' : 'CameraExpression';
+ const exprType = getElmType(prop);
+ let bounds = '';
+ if ('minimum' in prop && 'maximum' in prop) {
+ bounds = `\n\nShould be between \`${prop.minimum}\` and \`${prop.maximum}\` inclusive. `
+ } else if ('minimum' in prop) {
+ bounds = `\n\nShould be greater than or equal to \`${prop.minimum}\`. `
+ } else if ('maximum' in prop) {
+ bounds = `\n\nShould be less than or equal to \`${prop.maximum}\`. `
+ }
+ return `
+{-| ${prop.doc.replace(/`(\w+\-.+?)`/g, str => '`' + camelCase(str.substr(1)))} ${position} property. ${bounds}${prop.units ? `\nUnits in ${prop.units}. ` : ''}${prop.default !== undefined ? 'Defaults to `' + prop.default + '`. ' : ''}${prop.requires ? prop.requires.map(requires).join(' ') : ''}
+-}
+${elmName} : Expression ${exprKind} ${exprType} -> LayerAttr ${layerType}
+${elmName} =
+ Expression.encode >> ${position} "${name}"`
+}
+
+function getElmType({type, value, values}) {
+ switch(type) {
+ case 'number':
+ return 'Float';
+ case 'boolean':
+ return "Bool";
+ case 'string':
+ return 'String';
+ case 'color':
+ return 'Color';
+ case 'array':
+ switch(value) {
+ case 'number':
+ return '(Array Float)';
+ case 'string':
+ return '(Array String)';
+ }
+ case 'enum':
+ switch(Object.keys(values).join(' | ')) {
+ case "map | viewport":
+ return 'Anchor';
+ case "map | viewport | auto":
+ return 'AnchorAuto';
+ case "center | left | right | top | bottom | top-left | top-right | bottom-left | bottom-right":
+ return 'Position';
+ case 'none | width | height | both':
+ return 'TextFit';
+ case 'butt | round | square':
+ return 'LineCap';
+ case 'bevel | round | miter':
+ return 'LineJoin';
+ case 'point | line':
+ return 'SymbolPlacement';
+ case 'left | center | right':
+ return 'TextJustify';
+ case 'none | uppercase | lowercase':
+ return 'TextTransform';
+ }
+ }
+ throw `Unknown type ${type}`
+}
+
+function titleCase(str) {
+ return str.replace(/\-/, ' ').replace(
+ /\w\S*/g,
+ function(txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ }
+ ).replace(/\s/, '');
+ }
+
+function camelCase(str) {
+ return str.replace(/(?:^\w|[A-Z]|\b\w|\-\w)/g, function(letter, index) {
+ return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
+ }).replace(/(?:\s|\-)+/g, '');
+}
+
+
+function makeSignatures(name, constants) {
+ return `{-| -}
+type ${name} = ${name}
+
+ ${constants.split(' | ').map(c => `
+{-| -}
+${camelCase(name + ' ' + c)} : Expression exprType ${name}
+${camelCase(name + ' ' + c)} = Expression (Json.Encode.string "${c}")
+`).join('\n')}`
+}