aboutsummaryrefslogtreecommitdiffstats
path: root/style-generator/src/Main.elm
blob: fe07bc48c1fd7699840affcb76242319997b6863 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
port module Main exposing (main)

import Browser
import Decoder
import Element exposing (Element, centerY, fill, height, padding, px, rgb255, spacing, text, width)
import Element.Background as Background
import Element.Border as Border
import Element.Font as Font
import Element.Input as Input
import Html
import Html.Attributes exposing (property, style)
import Html.Events
import Http
import Json.Decode
import Json.Encode exposing (Value)


port requestStyleUpgrade : String -> Cmd msg


port styleUpgradeComplete : (Value -> msg) -> Sub msg


main =
    Browser.document
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }


init () =
    ( { styleUrl = ""
      , token = ""
      , style = Nothing
      , error = Nothing
      , code = Nothing
      }
    , Cmd.none
    )


type Msg
    = LoadedStyle (Result Http.Error String)
    | LoadStyle
    | StyleURLChanged String
    | TokenChanged String
    | StyleUpgradeCompleted Value


update msg model =
    case msg of
        LoadedStyle (Ok style) ->
            ( { model | style = Just style }, requestStyleUpgrade style )

        LoadedStyle (Err e) ->
            ( { model | error = Just (errorToString e) }, Cmd.none )

        LoadStyle ->
            ( model, fetchStyle model.styleUrl model.token )

        StyleURLChanged s ->
            ( { model | styleUrl = s }, Cmd.none )

        TokenChanged s ->
            ( { model | token = s }, Cmd.none )

        StyleUpgradeCompleted style ->
            ( { model
                | code =
                    case Json.Decode.decodeValue (Json.Decode.field "type" Json.Decode.string) style of
                        Ok "Ok" ->
                            Json.Decode.decodeValue (Json.Decode.field "result" Decoder.styleCode) style
                                |> Result.mapError Json.Decode.errorToString
                                |> Just

                        Ok "Err" ->
                            Json.Decode.decodeValue (Json.Decode.at [ "error", "message" ] Json.Decode.string) style
                                |> Result.withDefault "Something went wrong"
                                |> Err
                                |> Just

                        _ ->
                            Just (Err "Something went wrong")
              }
            , Cmd.none
            )


subscriptions l =
    styleUpgradeComplete StyleUpgradeCompleted


fetchStyle styleUrl token =
    String.replace "mapbox://styles/" "https://api.mapbox.com/styles/v1/" styleUrl
        ++ "?access_token="
        ++ token
        |> Http.getString
        |> Http.send LoadedStyle



-- UI


pad =
    20


body model =
    Element.layout [ width fill, height fill ] <|
        Element.column [ width fill, height fill, spacing pad ]
            [ Element.row [ width fill, height (px 60), Background.color (rgb255 238 238 238), padding pad, Border.color (rgb255 96 181 204), Border.widthEach { bottom = 2, left = 0, right = 0, top = 0 } ]
                [ Element.el [] <| Element.text "Mapbox to Elm Style Converter"
                , Element.link [ Font.color (rgb255 18 133 207), Element.alignRight ]
                    { url = "https://github.com/gampleman/elm-mapbox/tree/master/style-generator"
                    , label = text "GitHub"
                    }
                ]
            , Element.row [ width fill, height fill ]
                [ form [ height fill, width fill, spacing pad, padding pad ] model
                , results [ height fill, width fill ] model
                ]
            ]


form attrs model =
    Element.column attrs
        [ Element.el [] <| Element.text "Import style from Mapbox"
        , Input.text []
            { onChange = StyleURLChanged
            , placeholder = Nothing
            , label = Input.labelLeft [ centerY, width (px 100) ] <| Element.text "Style URL"
            , text = model.styleUrl
            }
        , Input.text []
            { onChange = TokenChanged
            , placeholder = Nothing
            , label = Input.labelLeft [ centerY, width (px 100) ] <| Element.text "Token"
            , text = model.token
            }
        , Input.button [ Background.color (rgb255 238 238 238), padding pad ] { onPress = Just LoadStyle, label = Element.text "Fetch style" }
        , Element.el [] <| Element.text "Or paste your style here:"
        , codeEditor
            { width = "100%"
            , height = "100%"
            , mode = "json"
            , code = model.style |> Maybe.withDefault ""
            , onChange = Just (Ok >> LoadedStyle)
            }
        ]


codeEditor : { width : String, height : String, mode : String, code : String, onChange : Maybe (String -> msg) } -> Element msg
codeEditor props =
    let
        handler =
            case props.onChange of
                Just tagger ->
                    Html.Events.on "editorChanged" <|
                        Json.Decode.map tagger <|
                            Json.Decode.at [ "detail" ]
                                Json.Decode.string

                Nothing ->
                    property "readonly" (Json.Encode.bool True)
    in
    Element.html <|
        Html.node "code-editor"
            [ props.code
                |> Json.Encode.string
                |> property "editorValue"
            , handler
            , property "mode" (Json.Encode.string "elm")
            , style "width" "50vw"
            , style "height" "100%"
            ]
            []


results attrs model =
    Element.el attrs <|
        case ( model.error, model.code ) of
            ( Just err, _ ) ->
                Element.paragraph [ Font.color (rgb255 207 7 19), padding pad ] [ Element.text err ]

            ( Nothing, Just (Err err) ) ->
                Element.paragraph [ Font.color (rgb255 207 7 19), padding pad ] [ Element.text err ]

            ( Nothing, Just (Ok srcCode) ) ->
                codeEditor
                    { width = "50vw"
                    , height = "100%"
                    , mode = "elm"
                    , code = srcCode
                    , onChange = Nothing
                    }

            ( Nothing, Nothing ) ->
                Element.column [ padding pad, spacing pad ]
                    [ Element.paragraph [] [ Element.text "This is a tool that helps you generate elm-mapbox styles from Mapbox Studio." ]
                    , Element.paragraph [] [ Element.text "In Studio, hit the share button. This will give you the style url and token. This tool will attempt to generate an elm-mapbox style for you. It is not perfect, but should give a nice head-start. Try to compile the file and see if you get any errors." ]
                    , Element.paragraph []
                        [ text "There are a few common limitations that are relatively easy to fix with some grepping. For example, "
                        , code "Layer.textField"
                        , text " is often followed by "
                        , code "E.toString"
                        , text ", but should instead be followed by "
                        , code "E.toFormattedText"
                        , text "."
                        ]
                    ]


code : String -> Element msg
code =
    Element.el [ Font.family [ Font.monospace ] ] << Element.text


errorToString : Http.Error -> String
errorToString err =
    case err of
        Http.BadUrl stringString ->
            "Invalid URL. Check the inputs to make sure that it is a valid https url or starts with mapbox://styles/"

        Http.Timeout ->
            "Request timed out. Try again later."

        Http.NetworkError ->
            "Network error. Are you online?"

        Http.BadStatus response ->
            case response.status.code of
                401 ->
                    "An authentication error occurred. Check your key and try again."

                404 ->
                    "Couldn't find that style"

                _ ->
                    response.status.message

        Http.BadPayload m _ ->
            m


view model =
    { title = "Style Generator"
    , body =
        [ body model ]
    }