Movatterモバイル変換


[0]ホーム

URL:


RSS

Elm, Geocoding & DarkSky: Pt. 2 – Geocoding an Address

Info

SummaryIn Part 2 we will use Elm & the Google Maps API to geocode an address.
Shared2017-07-30

This is part 2 of a multipart series where we will be building a small weather forecast app usingElm,Google’s Geocoding API and theDarkSky API. Instead of doing everything in one massive post, I’ve broken the steps down into parts of a series. Here is the series plan:

If you’d like to code along with this tutorial, check outpart 1 first to get set up.

Note: to learn more about the Elm language and syntax, check out theElm Tutorial, theEggHead.io Elm course, subscribe toDailyDrip’s Elm Topic,James Moore’s Elm Courses or check outElm on exercism.io.

Overview

Before we can send a weather forecast request to DarkSky, we need to geocode an address to get its latitude and longitutde. In this post, we’re going to use Elm and our geocoding server fromPart 1 to geocode an address based on a user’s input in a text box.

Warning: this is a hefty post.

Project Source Code

The project we’re making will be broken into parts here (branches will be named for each part):https://github.com/rpearce/elm-geocoding-darksky/. Be sure to check out the other branches to see the other parts as they become available.

The code for this part is located in thept-2 branch:https://github.com/rpearce/elm-geocoding-darksky/tree/pt-2.

Steps for Today

What we want to do with our program today is create an HTTP GET request with an address that is input by a user and returns the latitude and longitude. These steps will get us there:

  1. Defining our primary data model
  2. Understanding Google’s geocode response data
  3. Modeling the geocode response data
  4. Creating JSON decoders
  5. Building our view and listening for events
  6. Adding message types
  7. Writing our update function
  8. Making our request
  9. Handling the geocode response
  10. Final wiring up with the main function & defaults

1: Defining our primary data model

At the top level for our app, we only care about an address and latitude and longitude coordinates. While the address’ type will definitely beString, we can choose between arecord ortuple to house our coordinates; however, each of these values must be aFloat type, as coordinates come in decimal format. For no particular reason, we’re going to use a tuple.

typealiasModel={address:String,coords:Coords}typealiasCoords=(Float,Float)

I like to keep my models/type aliases fairly clean and primed for re-use in type definitions, so I created a separate type alias,Coords, to represent( Float, Float ).

2: Understanding Google’s geocode response data

Let’s take a look at what a geocoding request’s response data forAuckland looks like so we can understand what we’re working with.

{"results":[{"geometry":{"location":{"lat":-36.8484597,"lng":174.7633315},// ...},// ...}],"status":"OK"}

If you’ve set up yourgeocoding proxy, you can see these same results by running this command:

λ curl localhost:5050/geocode/Auckland

We can see here that we get back astatus string and aresults list where one of the results contains ageometry object, and inside of that, we findlocation and finally, our quarry:lat andlng. If we were searching for this with JavaScript, we might find this data like so:

response.results.find(x=>x['geometry']).geometry.location// { lat: -36.8484597, lng: 174.7633315 }

What would happen in vanilla JavaScript if there were no results, or those object keys didn’t exist? Elm steps up to help us solve for the unexpected.

3: Modeling the geocode response data

Based on the geocoding response, let’s list out what we’re looking at:

Since we’re going to need decode these bits of data and reuse the types a few times, let’s create type aliases for each of these concepts (prefixed withGeo):

typealiasGeoModel={status:String,results:ListGeoResult}typealiasGeoResult={geometry:GeoGeometry}typealiasGeoGeometry={location:GeoLocation}typealiasGeoLocation={lat:Float,lng:Float}

If you’re not sure whattype alias means, read more abouttype aliases inAn Introduction to Elm.

4: Creating JSON decoders

There are a number of ways to decode JSON in Elm, andBrian Hicks haswritten about this (and has ashort book on decoding JSON), and so have many others, such asThoughtbot. Today, we’re going to be working withNoRedInk’s elm-decode-pipeline.

First, we install the package into our project:

λ elm package install NoRedInk/elm-decode-pipeline

In ourMain.elm file, we can import what we’ll need from Elm’score Json-Decode module as well as the package we’ve just installed.

-- Importing from elm core.-- We know from our type aliases that all we're working-- with right now are floats, lists and strings.importJson.Decodeexposing(float,list,string,Decoder)-- importing from elm-decode-pipelineimportJson.Decode.Pipelineexposing(decode,required)

Now we can write our decoders!

decodeGeo:DecoderGeoModeldecodeGeo=decodeGeoModel|>required"status"string|>required"results"(listdecodeGeoResult)decodeGeoResult:DecoderGeoResultdecodeGeoResult=decodeGeoResult|>required"geometry"decodeGeoGeometrydecodeGeoGeometry:DecoderGeoGeometrydecodeGeoGeometry=decodeGeoGeometry|>required"location"decodeGeoLocationdecodeGeoLocation:DecoderGeoLocationdecodeGeoLocation=decodeGeoLocation|>required"lat"float|>required"lng"float

Here we declare that we’d like to decode the JSON string according to our type aliases, such asGeoModel, and we expect certain keys to have certain value types. In the case ofstatus, that’s just a string; however, withresults, we actually have a list of some other type of data,GeoResult, and so we create another decoder function down the line until we dig deep enough to find what we’re looking for. In short, we’re opting for functions and type-checking over deep nesting.

Why does this feel so verbose? Personally, I’m not yet comfortable usingJson.Decode.at, which might look like

decodeString(at["results"](list(at["geometry","location"](keyValuePairsfloat))))jsonString

But with the former approach, we get to bevery specific with exactly what we are expecting our data to be shaped like while maintaining clarity.

5: Building our view and listening for events

It’s time to add ourview function. All we’re going for today is

As usual, let’s downloadthe official elm-lang/html package:

λ elm package install elm-lang/html

Then let’s import what we need from it:

importHtmlexposing(Html,div,form,input,p,text)importHtml.Attributesexposing(placeholder,type_,value)importHtml.Eventsexposing(onInput,onSubmit)

Each import is a function that we can use to help generate HTML5 elements which Elm then works with behind the scenes.

view:Model->HtmlMsgviewmodel=div[][form[onSubmitSendAddress][input[type_"text",placeholder"City",valuemodel.address,onInputUpdateAddress][]],p[][text("Coords: "++(toStringmodel.coords))]]

Ourview function takes in our model and uses Elm functions to then render output. Great! But what areSendAdress andUpdateAddress? If you’re coming from JavaScript, you might think these are callbacks or higher-order functions, but they are not. They are custom message types (that we’ll define momentarily) that will be used in ourupdate function to determine what flow our application should take next.

6: Adding message types

Thus far, we know of two message types,Update andSendAddress, but how do we define them? If you look at ourview function again, you’ll see the return typeHtml Msg. The second part of this will be thetype that we create, and our custom message types will be a part of that! This is something called aunion type.

typeMsg=UpdateAddressString|SendAddress|NoOp

We will be adding more to this shortly, but this is all we have come across thus far.

7: Writing our update function

Staying consistent withThe Elm Architecture, we’ll define ourupdate function in order to update our data and fire off any commands that need happen. If you’re familiar with Redux, this is where the idea for a “reducer” came from.

This is tough to do in a blog post, so please be patient, and we’ll walk through this:

update:Msg->Model->(Model,CmdMsg)updatemsgmodel=casemsgofUpdateAddresstext->({model|address=text},Cmd.none)SendAddress->(model,sendAddressmodel.address)-- more code here shortly..._->(model,Cmd.none)

Let’s walk through this step-by-step:

8: Making our request

In order to build and send HTTP requests, we’ll need to make sure we download theelm-lang/http package:

λ elm package install elm-lang/http

and import it:

importHttp

In ourupdate function, we referenced a function namedsendAddress and passed it our model’s address as a parameter. This function should accept a string, initiate our HTTP request and return a command with a message.

sendAddress:String->CmdMsgsendAddressaddress=Http.get(geocodingUrladdress)decodeGeo|>Http.sendReceiveGeocodinggeocodingUrl:String->StringgeocodingUrladdress="http://localhost:5050/geocode/"++address

OursendAddress function does this:

  1. it builds a GET request using two arguments: a URL (derived fromgeocodingUrl) and ourdecodeGeo decoder function
  2. it then pipes the return value fromHttp.get to be the second argument forHttp.send

Note thatHttp.send’s first argument is aMsg that we haven’t defined yet, so let’s add that to ourMsg union type:

typeMsg=UpdateAddressString|SendAddress|ReceiveGeocoding(ResultHttp.ErrorGeoModel)|NoOp

Basically, we’ll either get back an HTTP error or a data structure in the shape of ourGeoModel.

9: Handling the geocode response

Finally, we now need to handle the successful and erroneous responses in our update function:

update:Msg->Model->(Model,CmdMsg)updatemsgmodel=casemsgofUpdateAddresstext->({model|address=text},Cmd.none)SendAddress->(model,sendAddressmodel.address)ReceiveGeocoding(Ok{results,status})->letresult=casestatusof"OK"->results|>List.head|>Maybe.withDefaultinitialGeoResult_->initialGeoResultlocation=result.geometry.locationnewModel={model|coords=(location.lat,location.lng)}in(newModel,Cmd.none)ReceiveGeocoding(Err_)->(model,Cmd.none)_->(model,Cmd.none)-- This should go with other `init`s-- but is placed here for relevanceinitialGeoResult:GeoResultinitialGeoResult={geometry={location={lat=0,lng=0}}}

Instead of having success/error logic inside oneReceiveGeocoding case match, we use Elm’s pattern matching to allow us to match on the message andOk orErrresults.

Again, let’s do this step-by-step:

10: Final wiring up with the main function & defaults

Now that we’re through the core of the application’s contents, we can wire up the remaining bits and get it to compile:

-- Define our HTML programmain:ProgramNeverModelMsgmain=Html.program{init=init,view=view,update=update,subscriptions=subscriptions}-- Here is our initial modelinit:(Model,CmdMsg)init=(initialModel,Cmd.none)initialModel:ModelinitialModel={address="",coords=(0,0)}-- We're not using any subscriptions,-- so we'll define nonesubscriptions:Model->SubMsgsubscriptionsmodel=Sub.none

Remember that you can look at thesource code for this part as a guide.

Wrapping Up

This has been a massive post on simply fetching geocode data from an API. I’ve found it’s difficult to write posts on Elm in little bits, for you have to have everything in the right place and defined before it’ll work. Subsequent posts in this series will be shorter, as we’ll have already done the heavy-lifting.

Until next time,
Robert


[8]ページ先頭

©2009-2025 Movatter.jp