From 5fdcf1fbe9a56951799d89bfc43e286742a2495f Mon Sep 17 00:00:00 2001 From: Jakub Hampl Date: Tue, 19 Jun 2018 09:05:14 +0100 Subject: Initial commit --- generate-elm.js | 359 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 generate-elm.js (limited to 'generate-elm.js') 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')}` +} -- cgit v1.2.3