Custom Resolvers In Neo4j GraphQL & Photos
Building A Real Estate Search App w/ GRANDStack: Part 7
In this session we add custom logic to our GraphQL API by implementing a custom resolver that adds photos to our application using the Mapillary API.
Links And Resources#
- Hey, folks. Welcome to the Neo4j Twitch stream. My name's Will. And today we are picking up where we left off working on our GRANDstack real estate search application. So working with GraphQL, React, Apollo, and Neo4j database. I'll drop the link to the GitHub repo just so we know where we're starting off. Cool, everyone. So I am Olive says, "Hi, everyone." Hello. Cool, yeah, so feel free to ask questions in the chat if you have them. What we're going to do today is pick up where we left off last time. Last time, if you remember, we added a map component, and we were showing some search results. So we were searching for properties within the bounds of the map and displaying a little marker, and we can click on that and view some detailed information about the property. And what I wanna do today is explore how we can add photos to our application and to our GraphQL API. And I'll talk about how we're going to do that in a minute. But first of all, let's go ahead and get our application running. So let's make sure we have our database running. Yep, we got the database running. Do an npm run start. And while that's building our React app, let's take a look at the code we were working on last time. So we added this search component last time where we are executing this GraphQL query, so searching for the first 200 properties, filtering for properties within one kilometer of this point. And then in our selection set, bringing back some detailed information about each property including its address, square footage, what subdivision it's in, its location. And then once we get data back from our GraphQL API, we are rendering this MapResults component, which we also added. And in MapResults, we're pulling in the React MapGL React wrapper around Mapbox GL JS from Urbica Designs. We have a little bit of styling. Then we're using this useState hook to define some state for the viewport for our map. So initially notice we have this set to the same area as what we're using for the center of our search radius in our GraphQL query. So we'll have to think about how we can connect those two. So how instead of just having this hardcoded in our GraphQL query, what we really wanna do is have some GraphQL variables here where we're binding those to the value in the viewport here that's changing as we're moving the map around. So we'll have to think about that. And then we are rendering in a nice kind of grid layout here our map component. And we're iterating through all of the properties that come back from our GraphQL API, adding those as markers on the map. And then we have some detail off to the side where when I click on one of those maps, that triggers this setCurrentProperty onClick handler, which is defined right here. So that's just a useState hook that's updating the current property. So that's how we know which property we've clicked on and when to rerender this piece that has our detailed information on the property we clicked on. Okay, so that's what we did last time. Let's see what it looks like. So here's our simple dashboard component that we started off with. We want the search. So here's the search. We have just kind of some properties that are shown within the radius that we're searching for. And as we click on them, we can view some detailed information, like the address, how many bedrooms, bathrooms, and so on the house has. So that's fine. But what we really want are some pictures. That's kinda part of the fun of perusing these real estate search apps, right, are looking at the houses. Now, we don't have pictures of every house, need to start off somewhere with some pictures to show. So what we're going to do is try to pull in the Mapillary API to see if we can find any photos of these houses. Is anyone familiar with Mapillary? Anyone used Mapillary for anything before? So Mapillary, actually it's my Chrome plugin anytime I open a new tab, it pulls in a photo from Mapillary just kind of at random. So what Mapillary is, it's crowdsourced photos. So you download the Mapillary app on your phone, and then as you're, in this case, I guess, as you're out skiing, you're just sort of taking pictures of what you see around you. The Mapillary app is capturing that. Then those images get sent up to Mapillary and any of the GPS coordinates that you've tracked get updated to OpenStreetMap. So it's a great way to add data to OpenStreetMap and also crowdsource some of these photos. And so like here I can click on this and go to Mapillary to view, in this case, so for that one random photo that I was looking at, I can see the whole sequence of those photos that gets added to OpenStreetMap for defining the way that is this ski trail here. And Mapillary also has an API, so you can also in your application build in the ability to search for photos and include some of these photos in your own application, which is what we're going to do today. So there's a couple things we need to do. If we think about how we wanna approach this, what I want to happen, I just wanna make one call to my GraphQL API and pull down all of the data that I need to load this view. I don't wanna have to go out from this client application, I don't wanna have to go out to the Mapillary API directly, instead, I want my GraphQL API to have the logic on the backend for fetching all of the data that I need to render this view. I don't want to worry about that from the front-end. And that's one of the great things about GraphQL is I just sort of specify the data that I need to render the view in my application, and we let the backend GraphQL implementation take care of finding the different data sources and stitching those together. So from the front-end, that's kind of what I want to happen. If we think about how this is going to work on the backend, well, we're gonna have to, from our GraphQL server, call out to the Mapillary API. So anytime that I click on one of these property locations, what's going on is it's going to our GraphQL API, which is running localhost.4001/graphql. That's not, let's just copy our query that we're running here. So this is the GraphQL query we're running to populate our view. And so what I want is all of the necessary information to be included in this data that I'm getting back. So I want the URL for the photos to be a field that I can select in my GraphQL API. So I wanna be able to say like photos, maybe give me the first 10. So for each property, give me 10 photos and give me the URL for those photos, and then I can load those in my client application. So that's what we're gonna build today is this piece in our GraphQL API, how we can find photos for each property, and then add those here in the property details view over here. Cool, so the first thing we need to do, I guess, is to take a look at the Mapillary API to see how we're gonna go about this. If I go to mapillary.com, and I've already signed in, so I can go to the app dashboard version of this. So here's all of the photos that are available. Actually, we can zoom in on the area that we've pulled into our database. Hopefully, we actually have mapped some of that area. Let's see, so Gallatin County in Montana is where we have most of our property listings. Here's Bozeman. So we have, some of these are mapped, so we should be able to see some of the properties in our database. See how this turns out. Anyway, what we want to do is go to, let's see, no, not the Leaderboard. The Dashboard? Yeah, there we go. So Developers, and Developer Documentation Page. There we go. So what we wanna do is call the Mapillary API. So let's look at the docs for that. Okay, so here's the docs for the Mapillary API. And we're specifically interested in images. There's various things we can pull down in this API. We can look for features. I think Mapillary does some computer imagery where they're looking for like street signs. So we could also search for images that include street signs and that kind of thing, as well as sequences. And we're just looking for images for a specific latitude and longitude. So what we have is we know the location of our property and we wanna say, hey, show me a picture that has that latitude and longitude essentially in the image. So here's a Search Images endpoint. So this looks pretty simple. So we go to a.mapillary.com/v3/images, pass in a client ID, and then that gives us, it should give us a bunch of images. So it should be easy enough to just test in our browser here. So let's try this. And, of course, we need to create a client ID. So here's in that dashboard/developers, we drop the link into Mapillary, just so you can see exactly where to go to create a application token. So I'm gonna click on Register an application with Mapillary. Let's call this "Willow GRANDstack." Real estate search application with GRANDstack. Company name. Callback URL. We're not gonna go through, we're not gonna go through any sort of OAuth flow, so I don't think we actually need the callback URL. If we requested some of these more specific permissions, then I think we would need to worry about going through the OAuth flow and the callback URL and all that stuff. But no, it says, "By default, application has access to public entities such as images. You can set additional scopes below." Yeah, so we just want public images. So we don't need to worry about adding any additional scopes. Cool, so I have my client ID. Let's copy that. And in the URL, there's our client ID. So this is gonna show me, I don't know, however many photos are in the default page size. So let's see. So this, if you remember when we were first doing our data import when we had pulled down those shapefiles from the state of Montana (indistinct) website and we converted those to GeoJSON. This looks like it is that GeoJSON format. So we have the featureCollection and then an array of these features. And each feature, and that's, yep, that's all we have in here, and each feature has a geometry, so a point, latitude and longitude, and then it has some properties associated with it. So it has like the ID of the user that took the photo, their username, whether or not this is a panoramic image, I'm guessing that's what this is, the sequence it belongs to, the date it was captured, any camera metadata. And then it has, what are we interested in? So we wanna construct a URL for this image, 'cause we wanna display it in our application. We want to retrieve an image file. So given an image key, retrieve the image file using the following URL patterns, which is mapillary.com/key, thumb, and then I guess if we want different sizes. So 320 all the way up to 2,048, guessing that's for the size of the image. Ours are gonna be pretty small. See what the 320s look like. So in a new tab, so this is kind of the skeleton for one of these images, and if we grab the key out of our first one. And there we go. So that is an image that we've pulled down through the Mapillary API. What we wanna add now, is we wanna specify the latitude and longitude for the area that we're searching. We don't just want random photos. Search images. So some URL parameters we can include, so we could include a bounding box, closeTo, which is the latitude and longitude, or lookAt. So closeTo, filter location the images are close to given as longitude and latitude. lookAt will filter images that are taken in the direction of the specified location, and therefore, that location is likely to be visible in the image given as latitude and longitude. If lookAt is provided without geospatial filters like closeTo or bounding box, then it will search global images. Okay, so I think what we want here is a combination of closeTo, 'cause I mean we're talking about looking at houses and real estate photos, so we want to be close to that location. But then we wanna be pointed in the right direction, right? So lookAt should give us photos taken in the direction of the property you're looking at. And closeAt when sure that we're close enough to actually see it. Cool, so let's try, let's just grab the latitude and longitude from our center of our map that we're using to search for. Let's just use that as kind of an example. So. Okay, so client ID and lookAt equals, and then it says lookAt is given as longitude, comma, latitude. Here's our longitude, comma, latitude. I'll delete that at the end. And then we also wanna specify closeTo equals, and then the same value. So looking at this point and also close to the point. Okay, and we get back, oh, we get back a bunch of photos. Great. So here's the key, so let's copy the key, and then place that here. Okay, cool. So that is one of our images. Let's make this a little bigger. Was there a 640, I think was the next one? Oh, not 6,400, 640. There we go, a little bigger. You can see that better. Cool, so this point that we were using, that's the Montana State University campus. This looks kind of like a college campus. This kind of looks like a football stadium over here. I don't know, let's maybe pick another one a little bit down the list and just see what that one looks like. Yeah, okay, so that's someone tooling around the neighborhood around Montana State University. Cool, that's a good start. Okay, so anything else we wanna specify in here? Radius, so filter images within radius around the closeTo location. Default is 100 meters. Okay, so that might be useful. Let's add that in here. And radius, so default 100, so I don't know, that seems like a pretty good, if we move out to 200, I guess we maybe get more results. Yeah, we get a lot more results. But they might not be directly pointed at or close enough to the house that we wanna look at. And then, what else, per_page. So the number of images per page default is 200. 200 is probably too many to bring back. I mean, we're showing these for each property listing in our application. We don't need to show 200. So let's change this. Ah, what was it, per_page? Let's just say 10. Seems a bit more reasonable. Okay, great. So let's Save this URL. I'm gonna leave it up in the tab. I'm also gonna put it down in my Scratch file here just so we don't lose that, 'cause we'll wanna use that later on. Cool, so now we know how to search the Mapillary API for photos around a certain point. Now, what we wanna do is figure out how to add this to our GraphQL API. So let's go to api, src, schema.graphql. And what we know we want, so here's our property. A lot of fields on property. So we know what we want. Let's put this down at the end, that's where we added our relationship fields. So we want something like this, photos, and I don't know, this could be an array of strings where a string is just the value, or the URL of the photo. Or if we have more than one field that we want to include per photo, we might wanna make a photo type. So in addition to the URL, maybe we also wanna include return something like the coordinates where the photo was actually taken, or ca, is that, I'm not sure what ca is. Is that like the angle that the photo was taken? Yeah, ca is the image's camera angle. So maybe we want to include something like that so then we can include that information in our client application as well. So I'm gonna add a photo type here, and let's say url is gonna be a string. And let's just leave that for now. Okay, so now we've seen how we can define custom logic or computed fields. Previously, we saw how to do that using the Cypher directive, right? So here we're doing an aggregation, grouping properties by city, and then computing the average of their total value, right? So now we could query city values just from the root query field, and that gives us the data that we're using here to render this chart. So we've seen how we can do custom logic using Cypher, where we're not just reading values from the database, instead we're actually computing those, binding the value of these fields to a Cypher query. In this case, we probably could I think actually define this field with a Cypher query because we have apoc.load.json, so we've previously used apoc.load.json to load that GeoJSON file when we were first starting out to load data in Neo4j. But we can call procedures from these Cypher directives. So it would be something like CALL apoc.load.json, and then the URL that we call would be constructed from this URL that we're using to call the Mapillary API. I think that would work. We could call that directly from Cypher here and then pull out specifically the fields that we wanna return. Instead, though, I wanna do this a different way where we'll actually implement the resolvers for this field. And this is a really useful thing to know how to do, how to implement your own resolvers to add custom logic to your GraphQL API. One thing that's really, really nice about Neo4j GraphQL JS, so if we take a look at index.js, and here for our makeAugmentedSchema process, you'll notice that we just pass our type definitions and some configuration. We haven't written any boilerplate, data-fetching code. So far all of that is driven from our type definitions in the schema.graphql file. So it's really nice to not have to worry about implementing our own resolvers that say, hey, here's how you go to the database and do this bit of data-fetching, do this bit of graph traversal. Instead, our library Neo4j GraphQL JS is just generating those database queries for us. So that's really nice. But there are some cases where we have custom logic that we want to add to our GraphQL API and we want to be able to have some flexibility to be able to do that. So that's what we're gonna look at. So if we go to the grandstack.io docs, and we want to look specifically for Adding Custom Logic. Okay, so we've done this, the @cypher GraphQL Schema Directive. We've done this for computed scalar fields. That, for example, like this one, like propertyCount. So this is just counting the number of property nodes in the database. That's a computed scalar field. And then for computed object fields like this one, like cityValues where we're doing this aggregation that's returning objects of the cityValue type that have the city name and the average of the total value. And now, what we're gonna do is implementing custom resolvers. Let's drop that in the chat. Cool, so with GraphQL, typically when we're building a GraphQL API, we define the type definitions, which are these, and then we define resolvers, which are a bunch of functions that specify how to fetch the data for that field. And as I mentioned earlier, with Neo4j GraphQL JS, those resolvers are being generated for us automatically, so we're now going to create our first resolver. So every resolver has this sort of signature, let's make this a bit bigger, there we go, where it is passed an object, argument, context, and info. So this first parameter, the object, this is what has been resolved so far up until this point. So the resolvers are called in a nested fashion. So in this case here, this is a resolver for waitTime, so this field, this integer scaler field on the business type. So by the time that this waitTime resolver is called, we've called a higher level business resolver and maybe things like a businessId and the name and city have already been resolved. And those are gonna be available in this first parameter, the object. This is important because when we call the Mapillary API, we need to know the latitude and longitude for the property that we're searching for images for. So we're gonna fetch those out of this object parameter. Then we have an args, or argument. So these are gonna be any field-level arguments passed to this field. In this case, waitTime doesn't have any. Context, context object, this is something that gets passed to every resolver, and this is where we can add things like a connection to the database, or maybe add a user object or something like that. So inside our resolver function, we can take some specific action or callout to some data-fetching layer somewhere. And then info, this is the GraphQL ResolveInfo object, which is actually what we use to generate database queries. I wrote a blog post about this on the GRANDstack blog. Here we go. GraphQL ResolveInfo Deep Dive. So if you're interested in how we actually go about generating these database queries, oh yeah, and this is also mostly from a talk that I gave at GraphQL Summit last year sort of digging into the GraphQL ResolveInfo object. So this shows kind of how we use what's in the GraphQL ResolveInfo object inside the resolvers to generate data-fetching code as part of Neo4j GraphQL JS. So I'll drop a link to that in the chat if anyone's interested in learning more about that. But anyway, that's what the info argument is here. And we shouldn't need to do anything with that. Okay, so we add our field to our type definitions where we're going to add our custom logic. We add this neo4j_ignore directive. And this means that, hey, we're going to implement this resolver ourselves. Don't try to fetch this field from the database. Then we implement the resolver function. In our case, we're gonna callout to the Mapillary API. Then we pass that resolver map of functions to makeAugmentedSchema, and then we can query it in our GraphQL query. Cool, so let's go ahead and do that. Okay, so I started adding this photos field. Instead of Cypher, we're gonna say neo4j_ignore. And let's add some arguments here. So if you remember in the call to the Mapillary API, let's see, we had lookAt and closeTo. So these are gonna come from the object, so that first parameter passed into our resolver, so we know the area or the point of the property that we're going to be searching for images for. But then we also had radius and per_page. So let's allow those to be, define a query time. So radius, that's going to be an integer, and let's give it a default value of 10. And then let's call it first, let's follow our convention of first offset for pagination. It's gonna be an integer and, oh, that's what we wanna have. We wanna have the first 10 photos, and then for radius we want that to have, let's make this, say, two kilometers starting out. That's pretty big, but, let's just have that as our default value. Okay, cool. So now, we need to create some resolvers. So let's create a new file, resolvers.js. And here we're going to, zoom in a little bit, we're going to export our resolver map. So we'll say export resolvers. And this is on the property, right, type. So this will be property. And then this is specifically photos. And we're passing in, we said the object, let's call it what it is, it's gonna be a property. And we're gonna have some arguments, and we don't need to worry about the rest. So these args here are these values that we're specifying. So in our query we can write something like this, first 10, radius, 1,000, something like that. And so you notice that I'm not getting an error anymore because I've added this to my type definitions. But if I Run it, Resolve function for Property.photos returned undefined. That's because there is no Property.photos resolver. Okay. So what are we gonna do here? Well, we're gonna have some requestURL. And starting off I will just grab this. So we have some URL that we're going to call to the Mapillary API. We can fill in, so per_page, we know this is going to be args.first. And radius, this is going to be args.radius. And longitude and latitude, so this is longitude, this is going to be property.location.longitude. So how did we come up with that? Well, property, so it's the property that we're resolving that's passed into this function. And then if we look maybe at our GraphQL query is the best place to see this, so here's the property, location, longitude, and latitude. So we'll fill that in, property.location.longitude, and this is going to be property.location.latitude. Oh, and then we also have closeTo. So we're just gonna kind of repeat this bit. We could pull that out into just like a longitude and latitude variable, but nah, this is fine. So closeTo, longitude, latitude. Okay, cool. And then the other thing, I've got my client ID in here, which I don't really want to just have hardcoded in here. So we'll take that out. And we'll say this is going to be process.env.MAPILLARY_KEY, and we'll add this to our .env file, and that's now our APIENV file. MAPILLARY_KEY equals that. Cool, and then we need to call dotenv. So this is the library that we use that sets our environment variables by reading that dotenv file, dotenv.config, I think it is. Okay, so that constructs the URL, and then we need to make that URL request. We could use Fetch or something like that. I like to use the Axios library. This works from the browser and for nodes as the same API. So something like this where we say axios.get, and then we can use a async/await, an example, using await, yeah, so something like this, where we then pull out those features and so on. So let's install this, I don't think we've included it in the GRANDstack project so far. So we wanna install this in the API application, npm install. You don't actually need the --save anymore. But install Axios. Okay, so that will install Axios. So then I should be able to, once we pull it in, we'll say axios equals require axios. And then once we've constructed our URL, we can say something like response equals await axios.get requestURL. And this is probably complaining about not having an async function, so we wanna say that this photo resolver is an async function. Cool, so let's just log our response, npm run start. Okay, so let's fire this up and then just log the response. So if we request Property.photos. Do-do-do. Oh right, we're not actually calling this because in index.js, make sure I'm keeping an eye on the chat here. Cool, in index.js, we need to import resolvers from resolvers. And then when we call makeAugmentedSchema, pass resolvers. Okay, I think that started up again. So now, let's make our query. Okay, so now we've called that function, so the function isn't undefined, so we're not returning an error. The function doesn't return anything, so we just get back null for our photos. And if we take a look here we can see, yep, logging some response from the Mapillary API. Cool, okay, so we've got our response. So if we go back and look here, here, this one, when we're calling the Mapillary API, we get back something that looks like this. So what we wanna grab out of this is the key to construct our photo URL. So we're going to go into the features and then map over this features array, and for each feature, pull out the key and construct a URL that looks like this, 'cause this what we actually wanna return so we can load those in our application. Okay, so instead of just logging the response, let's say features equals response.data.features. So if we scroll to the top here, kinda hard to, oh yeah, we've actually gone over the scroll buffer. That's okay. Okay, so in our features, so we're going to, what we wanna return is mapping over features, so for each value in our array of features, we wanna return something where there's a URL. And it's going to have this format. But instead of this hardcoded key, the value for the key is going to be v. So in the features, so v is the features. Let's look at this, so v is the feature, so like one of these, and we want v.properties.key. Like that, I think. Let's see if that works. So we'll Run our GraphQL query again, and okay, cool. So now, we've got some photos. And if we take a look at one of these, that, yep, that is, that looks like somewhere on the campus of Montana State University. Cool, so we've added that now to our GraphQL API. So now we're pulling in photos for each of our properties from the GraphQL API going out to the Mapillary API. So for our client application, we just getting that data just from GraphQL, which is nice. So let's go now into our React application, so web-react, src, and start in Search. So here's our GraphQL query that we're running to populate our map. And the first thing we wanna do is add photos to this. So let's add that to the selection set. So photos URL. So now we're pulling that data in. And then the next thing we wanna do is make sure that we're actually showing those photos in our application. So we switch over here to Search. Okay, so here's, zoom in a little bit, so here's some properties, and we can view the detailed information. What we want is to show those photos in our property detail view down here. So we're using Material-UI. So let's see if there's, I'm just gonna search, if there's something like an image grid, Image-only Grid list. That sounds like something that we want. Oh yeah, there we go. So there's titlebars. We don't have any descriptive information for each photo, so I don't think we need a titlebar. Maybe just something kinda like this. So we'll stick in one of these grid lists below our property information. See how that works. Okay, cool, so this is our search component, which then renders a MapResults, so we want in MapResults, here is our property detail information, so that's this area here. We didn't break that out into its own component. I suppose we could, but can also just do it kind of inline here, that should be fine. Okay, let's see, so this is a paper, so we probably want it within the paper thing. Let's see here. So how involved is this grid list? So the grid list itself sets a cell height, okay, so there's a fixed height for each one of the photos. And then we also set columns, and then each grid list tile takes up a certain number of columns. Okay, so if it's like a pano image, it takes up more columns, I guess. We don't really have to worry about that. All of our images are fixed dimensions, 'cause we're calling that 640 version of the thumbnailed image, so I guess all of our images can be one column. Maybe put like two images together, something like that. Okay, so, and then just calling an img src, and here's where we will have the URL for our image. Okay, that seems doable. So let's update that. So we're going to import, what was it, GridList and GridListTile from material-ui/core. And then we're going to, oops, here, we're going to have a GridList. And so we have to specify a cell height. So the fixed height for each one of these photos, I know it's, for example, uses 160, so we can use that. And then the number of columns that we want to allow. So we said each one of our photos would take up one column, so we'll do two at a time. So two columns. And then what we wanna do now is map over all of the photos that we have. So the photos live in this currentProperty object. So we're gonna say currentProperty.photos.map. And then we want the value for the photo object and then also the index. So anytime we do this mapping over an array that's gonna render React elements, I like to always bring back the index and then use that for the key so that we can assign the unique key to each element, each React element that we render. So now, this is going to be the GridListTile. Give it the key, and then it specifies the number of columns, and all these are the same size. So we'll say it's gonna be one column, cols equal. And, oops, we need that semicolon there. Okay, and then, we're gonna do img src equals v, so the URL of the photo. Okay, and I think that should be pretty close. It looks like we have an error right here on line 89. Expected an assignment or function call and instead saw an expression no-unused-expressions. What are we doing there? So GridList. Realize I've been ignoring the chat here. There we go. Okay, so GridList, and then we do currentProperty.photos.map. Do-do-do. Oh, this should be... There we go. Okay, so it should've been a paren instead of curly brace. Okay, so now we are creating this GridList, doing a map over all of our photos to render a tile for each photo fetching the image source of the URL of the photo coming from our GraphQL API. So if we now take a look on the map, we can see for our first selected property, we get back some photos. And as we select different properties, we can see we have some different photos. Can't really see, it's not super clear if I'm actually looking at the house that I should be. This looks like we're on the same street. So that's a start. And some of these looks like we're not getting back. There we go. So here's a photo of 305 S 12th Avenue. Cool, so we can try to fiddle around with the... Here we're searching for radius, so we can set the radius to be a lot smaller. We can pull in more photos, so pull in 100 photos. So part of the issue is, you know, how much of this area do we have mapped? Do we actually have... Those 100 photos may have been a bit too many in the Mapillary API. You know, 100 meters might be a bit much. We may not have photos that were taken exactly right out in front of the house. But if we move out a little bit, I think we can find some that at least seem to be in the area or on the same street. Think that error we're getting is maybe a timeout on the Mapillary API for requesting too many photos. Oh, there it goes. Cool, so that anyway was at least a good start. That seems like a good place to stop for today. So just to review a little bit what we did today, we added some custom logic to our GraphQL API, specifically we added this photos field on our property type where we're calling out to the Mapillary API for each photo to find images that are showing the latitude and longitude of our property, and then we added that into our property detail view so that we can show those along with information about the property that we're looking at. Cool, so I'll push this code up to GitHub. And definitely let us know if you have any questions or feedback. For next time, I'm not sure what we'll work on next time exactly. I guess one thing we do need to do is to make our map a little bit more integrated with our search functionality. So instead of just showing us results around a fixed location, instead, as we move around the map we should see our search results change. So maybe we'll focus on that next time as well as adding some filter functionality so we can search for more detailed filters on our properties, specifying the number of bedrooms and so on that we're searching for. Cool, that's all for today and we'll see you again next week. Bye-bye.
Subscribe To Will's Newsletter
Want to know when the next blog post or video is published? Subscribe now!