Blog

How to search data objects by distance from a location

by on April 22, 2015

In another article, we described how a data object may have a related geopoint (or a collection of geopoints). One of the benefits of data-to-geo relationships is search by distance. That means Backendless can search for data objects using the location of the related geopoints.

Consider an example from a taxi-reservation system. There may be several cabs-for-hire in the area. Your app needs to locate all available cars within the specified distance from where the customer is located.

A class representing a cab may look like this:

    import com.backendless.geo.GeoPoint;
    
    public class Car {
       public String make;
       public String model;
       public boolean available;
       public GeoPoint location;
       public String objectId;
    }

    Notice the class has the location field of type GeoPoint. The related GeoPoint object represents the current location of the car.

    import com.backendless.geo.GeoPoint;
    
    public class Car {
       public String make;
       public String model;
       public boolean available;
       public GeoPoint location;
       public String objectId;
    }

    @interface Car : NSObject
    
    @property (strong, nonatomic) NSString *objectId;
    @property (strong, nonatomic) NSString *make;
    @property (strong, nonatomic) NSString *model;
    @property (nonatomic) BOOL available;
    @property (strong, nonatomic) GeoPoint *location;
    
    @end

    @objcMembers class Car: NSObject {
        var objectId: String?
        var make: String?
        var model: String?
        var available = false
        var location: GeoPoint?
    }

    class Car {
      constructor(args) {
        args = args || {}
    
        this.make = args.make
        this.model = args.model
        this.available = args.available
        this.location = args.location
      }
    }
    

    Class definition is not required. Backendless SDK for Flutter uses the map/dictionary approach for object persistence.

    To search for all available cars within distance from a location (which is the place where the customer who needs to get a car is located), Backendless provides a special function which can be used in the SQL queries. The following SQL where clause can be used in Backendless to find all available cars located within 10 miles from the specified coordinates (32.803, -96.770 – near Dallas, Texas downtown):

    distance( 32.803, -96.770, location.latitude, location.longitude ) < mi(5) and available = true

    The query when sent to the Car table should be read in plain English as: “locate all available cars for which the location.latitude and location.longitude coordinates are located within 5 miles from the point at 32.803, -96.770.

    Notice the location.latitude  and location.longitude arguments. The “location” part refers to the location field of the Car class shown above. The “latitude” and “longitude” parts refer to the properties of the GeoPoint class identified by the location field. The mi(5) operator is used by the distance function and indicates the distance of 5 miles.

    The code to setup some sample data is:

      Car car1 = new Car();
      car1.make = "Toyota";
      car1.model = "Prius";
      car1.available = true;
      GeoPoint location1 = new GeoPoint( 32.800755, -96.816140 );
      saveCar(car1, location1);
      
      Car car2 = new Car();
      car2.make = "Kia";
      car2.model = "Sorento";
      car2.available = true;
      GeoPoint location2 = new GeoPoint( 32.778892, -96.781465 );
      saveCar(car2, location2);
      
      Car car3 = new Car();
      car3.make = "Toyota";
      car3.model = "Camry";
      car3.available = false;
      GeoPoint location3 = new GeoPoint( 32.770304, -96.796056 );
      saveCar(car3, location3);
      
      private static void saveCar(Car car, final GeoPoint location) {
         Backendless.Data.of(Car.class).save(car, new AsyncCallback<Car>() {
             @Override
             public void handleResponse(final Car savedCar) {
                 Backendless.Geo.savePoint(location, new AsyncCallback<GeoPoint>() {
                     @Override
                     public void handleResponse(GeoPoint savedLocation) {
                         Backendless.Data.of(Car.class).setRelation(savedCar, "location",
                                 Collections.singletonList(savedLocation), null);
                     }
      
                     @Override
                     public void handleFault(BackendlessFault fault) {
                         Log.e(TAG, fault.getMessage());
                     }
                 });
             }
      
             @Override
             public void handleFault(BackendlessFault fault) {
                 Log.e(TAG, fault.getMessage());
             }
         });
      }

      val car1 = Car()
      car1.make = "Toyota"
      car1.model = "Prius"
      car1.available = true
      val location1 = GeoPoint(32.800755, -96.816140)
      saveCar(car1, location1)
      
      val car2 = Car()
      car2.make = "Kia"
      car2.model = "Sorento"
      car2.available = true
      val location2 = GeoPoint(32.778892, -96.781465)
      saveCar(car2, location2)
      
      val car3 = Car()
      car3.make = "Toyota"
      car3.model = "Camry"
      car3.available = false
      val location3 = GeoPoint(32.770304, -96.796056)
      saveCar(car3, location3)
      
      private fun saveCar(car: Car, location: GeoPoint) {
         Backendless.Data.of(Car::class.java).save(car, object : AsyncCallback<Car> {
      
             override fun handleResponse(savedCar: Car) {
                 Backendless.Geo.savePoint(location, object : AsyncCallback<GeoPoint> {
      
                     override fun handleResponse(savedLocation: GeoPoint) {
                         Backendless.Data.of(Car::class.java).setRelation(savedCar,
                             "location", listOf(savedLocation), null)
                     }
      
                     override fun handleFault(fault: BackendlessFault) {
                         Log.e(TAG, fault.message)
                     }
                 })
             }
      
             override fun handleFault(fault: BackendlessFault) {
                 Log.e(TAG, fault.message)
             }
         })
      }

      DataStoreFactory *dataStore = [Backendless.shared.data of:[Car class]];
          
      Car *car1 = [Car new];
      car1.make = @"Toyota";
      car1.model = @"Prius";
      car1.available = YES;
          
      [dataStore saveWithEntity:car1 responseHandler:^(Car *savedCar) {
          GeoPoint *location = [[GeoPoint alloc] initWithLatitude:32.800755 longitude:-96.816140];
          [Backendless.shared.geo saveGeoPointWithGeoPoint:location responseHandler:^(GeoPoint *savedPoint) {
              [dataStore setRelationWithColumnName:@"location" parentObjectId:savedCar.objectId childrenObjectIds:@[savedPoint.objectId] responseHandler:^(NSInteger relationSet) {
              } errorHandler:^(Fault *fault) {
                  NSLog(@"Error: %@", fault.message);
              }];
          } errorHandler:^(Fault *fault) {
              NSLog(@"Error: %@", fault.message);
          }];  
      } errorHandler:^(Fault *fault) {
          NSLog(@"Error: %@", fault.message);
      }];
          
      Car *car2 = [Car new];
      car2.make = @"Kia";
      car2.model = @"Sorento";
      car2.available = YES;
          
      [dataStore saveWithEntity:car2 responseHandler:^(Car *savedCar) {
          GeoPoint *location = [[GeoPoint alloc] initWithLatitude:32.778892 longitude:-96.781465];
          [Backendless.shared.geo saveGeoPointWithGeoPoint:location responseHandler:^(GeoPoint *savedPoint) {
              [dataStore setRelationWithColumnName:@"location" parentObjectId:savedCar.objectId childrenObjectIds:@[savedPoint.objectId] responseHandler:^(NSInteger relationSet) {
              } errorHandler:^(Fault *fault) {
                  NSLog(@"Error: %@", fault.message);
              }];
          } errorHandler:^(Fault *fault) {
              NSLog(@"Error: %@", fault.message);
          }];
      } errorHandler:^(Fault *fault) {
          NSLog(@"Error: %@", fault.message);
      }];
          
      Car *car3 = [Car new];
      car3.make = @"Toyota";
      car3.model = @"Camry";
      car3.available = NO;
          
      [dataStore saveWithEntity:car3 responseHandler:^(Car *savedCar) {
          GeoPoint *location = [[GeoPoint alloc] initWithLatitude:32.770304 longitude:-96.796056];
          [Backendless.shared.geo saveGeoPointWithGeoPoint:location responseHandler:^(GeoPoint *savedPoint) {
              [dataStore setRelationWithColumnName:@"location" parentObjectId:savedCar.objectId childrenObjectIds:@[savedPoint.objectId] responseHandler:^(NSInteger relationSet) {
              } errorHandler:^(Fault *fault) {
                  NSLog(@"Error: %@", fault.message);
              }];
          } errorHandler:^(Fault *fault) {
              NSLog(@"Error: %@", fault.message);
          }];
      } errorHandler:^(Fault *fault) {
          NSLog(@"Error: %@", fault.message);
      }];

      let dataStore = Backendless.shared.data.of(Car.self)
              
      let car1 = Car()
      car1.make = "Toyota"
      car1.model = "Prius"
      car1.available = true
              
      dataStore.save(entity: car1, responseHandler: { savedCar in
          if let savedCar = savedCar as? Car, let carId = savedCar.objectId {
              let location = GeoPoint(latitude: 32.800755, longitude: -96.816140)
              Backendless.shared.geo.saveGeoPoint(geoPoint: location, responseHandler: { savedPoint in
                  if let locationId = savedPoint.objectId {
                      dataStore.setRelation(columnName: "location", parentObjectId: carId, childrenObjectIds: [locationId], responseHandler: { relationSet in
                      }, errorHandler: { fault in
                          print("Error: \(fault.message ?? "")")
                      })
                  }
              }, errorHandler: { fault in
                  print("Error: \(fault.message ?? "")")
              })
          }
      }, errorHandler: { fault in
          print("Error: \(fault.message ?? "")")
      })
              
      let car2 = Car()
      car2.make = "Kia"
      car2.model = "Sorento"
      car2.available = true
              
      dataStore.save(entity: car2, responseHandler: { savedCar in
          if let savedCar = savedCar as? Car, let carId = savedCar.objectId {
              let location = GeoPoint(latitude: 32.778892, longitude: -96.781465)
              Backendless.shared.geo.saveGeoPoint(geoPoint: location, responseHandler: { savedPoint in
                  if let locationId = savedPoint.objectId {
                      dataStore.setRelation(columnName: "location", parentObjectId: carId, childrenObjectIds: [locationId], responseHandler: { relationSet in
                      }, errorHandler: { fault in
                          print("Error: \(fault.message ?? "")")
                      })
                  }
              }, errorHandler: { fault in
                  print("Error: \(fault.message ?? "")")
              })
          }
      }, errorHandler: { fault in
          print("Error: \(fault.message ?? "")")
      })
            
      let car3 = Car()
      car3.make = "Toyota"
      car3.model = "Camry"
      car3.available = false
              
      dataStore.save(entity: car3, responseHandler: { savedCar in
          if let savedCar = savedCar as? Car, let carId = savedCar.objectId {
              let location = GeoPoint(latitude: 32.770304, longitude: -96.796056)
              Backendless.shared.geo.saveGeoPoint(geoPoint: location, responseHandler: { savedPoint in
                  if let locationId = savedPoint.objectId {
                      dataStore.setRelation(columnName: "location", parentObjectId: carId, childrenObjectIds: [locationId], responseHandler: { relationSet in
                      }, errorHandler: { fault in
                          print("Error: \(fault.message ?? "")")
                      })
                  }
              }, errorHandler: { fault in
                  print("Error: \(fault.message ?? "")")
              })
          }
      }, errorHandler: { fault in
          print("Error: \(fault.message ?? "")")
      })

      const Backendless = require('backendless')
      /*
       Or use `import Backendless from 'backendless'` for client side.
       If you don't use npm or yarn to install modules, you can add the following line
       <script src="//api.backendless.com/sdk/js/latest/backendless.min.js"></script>
       to your index.html file and use the global Backendless variable.
      */
      
      Backendless.initApp('YOUR_APP_ID', 'YOUR_JS_API_KEY')
      
      const car1 = new Car({
        make     : 'Toyota',
        model    : 'Prius',
        available: true,
        location : new Backendless.GeoPoint({ longitude: -96.816140, latitude: 32.800755 })
      })
      
      const car2 = new Car({
        make     : 'Kia',
        model    : 'Sorento',
        available: true,
        location : new Backendless.GeoPoint({ longitude: -96.781465, latitude: 32.778892 })
      })
      
      const car3 = new Car({
        make     : 'Toyota',
        model    : 'Camry',
        available: false,
        location : new Backendless.GeoPoint({ longitude: -96.796056, latitude: 32.770304 })
      })
      
      const saveCars = () => Promise.all([car1, car2, car3].map(async car => {
        const [createdCar, carLocation] = await Promise.all([
          Backendless.Data.of('Car').save(car),
          Backendless.Geo.savePoint(car.location)
        ])
      
        return Backendless.Data.of(Car).addRelation(createdCar, 'location', [carLocation])
      }))

      Map car1 = {
       "make": "Toyota",
       "model": "Prius",
       "available": true,
      };
      GeoPoint location1 = GeoPoint.fromLatLng(32.800755, -96.816140);
      _saveCar(car1, location1);
      
      Map car2 = {
       "make": "Kia",
       "model": "Sorento",
       "available": true,
      };
      GeoPoint location2 = GeoPoint.fromLatLng(32.778892, -96.781465);
      _saveCar(car2, location2);
      
      Map car3 = {
       "make": "Toyota",
       "model": "Camry",
       "available": false,
      };
      GeoPoint location3 = GeoPoint.fromLatLng(32.770304, -96.796056);
      _saveCar(car3, location3);
      
      static void _saveCar(Map car, GeoPoint location) {
         Backendless.Data.of("Car").save(car).then((savedCar) {
           Backendless.Geo.savePoint(location).then((savedLocation) {
             Backendless.Data.of("Car").setRelation(savedCar, "location", children: [savedLocation]);
           });
         });
       }

      The sample code below demonstrates the query in action:

        double myLatitude = 32.779108;
        double myLongitude = -96.797000;
        String query = "distance( %f, %f, location.latitude, location.longitude ) < mi(5) and available = true";
        String whereClause = String.format(query, myLatitude, myLongitude);
        
        DataQueryBuilder dataQuery = DataQueryBuilder
           .create()
           .setWhereClause(whereClause)
           .addRelated("location");
        
        Backendless.Data.of(Car.class).find(dataQuery, new AsyncCallback<List<Car>>() {
           @Override
           public void handleResponse(List<Car> cars) {
               if (cars.size() == 0)
                   Log.i(TAG, "Did not find any cars");
        
               for (Car car : cars)
                   Log.i(TAG, String.format("Found car: %s %s", car.make, car.model));
           }
        
           @Override
           public void handleFault(BackendlessFault fault) {
               Log.e(TAG, fault.getMessage());
           }
        });

        val myLatitude = 32.779108
        val myLongitude = -96.797000
        val whereClause = "distance( $myLatitude, $myLongitude, location.latitude, location.longitude ) < mi(5) and available = true"
        
        val dataQuery = DataQueryBuilder
           .create()
           .setWhereClause(whereClause)
           .addRelated("location")
        
        Backendless.Data.of(Car::class.java).find(dataQuery, object : AsyncCallback<List<Car>> {
           override fun handleResponse(cars: List<Car>) {
               if (cars.isEmpty())
                   Log.i(TAG, "Did not find any cars")
        
               for (car in cars)
                   Log.i(TAG, "Found car: ${car.make} ${car.model}")
           }
        
           override fun handleFault(fault: BackendlessFault) {
               Log.e(TAG, fault.message)
           }
        })

        double myLatitude = 32.779108;
        double myLongitude = -96.797000;
            
        DataQueryBuilder *queryBuilder = [DataQueryBuilder new];
        [queryBuilder setWhereClauseWithWhereClause:[NSString stringWithFormat:@"distance( %f, %f, location.latitude, location.longitude ) < mi(5) and available = true", myLatitude, myLongitude]];
        [queryBuilder setRelatedWithRelated:@[@"location"]];
            
        [[Backendless.shared.data of:[Car class]] findWithQueryBuilder:queryBuilder responseHandler:^(NSArray *cars) {
            for (Car *car in cars) {
                NSLog(@"Found car: %@ %@", car.make, car.model);
            }
        } errorHandler:^(Fault *fault) {
            NSLog(@"Error: %@", fault.message);
        }];

        let myLatitude = 32.779108
        let myLongitude = -96.797000
                
        let queryBuilder = DataQueryBuilder()
        queryBuilder.setWhereClause(whereClause: "distance( \(myLatitude), \(myLongitude), location.latitude, location.longitude ) < mi(5) and available = true")
        queryBuilder.setRelated(related: ["location"])
                
        Backendless.shared.data.of(Car.self).find(queryBuilder: queryBuilder, responseHandler: { cars in
            if let cars = cars as? [Car] {
                for car in cars {
                    print("Found car: \(car.make ?? "") \(car.model ?? "")")
                }
            }
        }, errorHandler: { fault in
            print("Error: \(fault.message ?? "")")
        })

        const Backendless = require('backendless')
        /*
         Or use `import Backendless from 'backendless'` for client side.
         If you don't use npm or yarn to install modules, you can add the following line
         <script src="//api.backendless.com/sdk/js/latest/backendless.min.js"></script>
         to your index.html file and use the global Backendless variable.
        */
        
        Backendless.initApp('YOUR_APP_ID', 'YOUR_JS_API_KEY')
        
        const searchCarsByDistance = () => {
          const myLatitude = 32.779108
          const myLongitude = -96.797000
          const whereClause = `distance(${ myLatitude }, ${ myLongitude }, location.latitude, location.longitude) < mi(5) and available = true` const dataQuery = Backendless.DataQueryBuilder.create() dataQuery.setWhereClause(whereClause) dataQuery.addRelated('location') return Backendless.Data.of(Car).find(dataQuery) } const onSearchSuccess = cars => {
          if (!cars.length) {
            console.log('Did not find any cars')
          }
        
          cars.forEach(car => {
            console.log(`Found car: ${ car.make } ${ car.model }`)
          })
        }
        
        const onError = error => {
          console.error('Server reported an error: ', error.message)
          console.error('error code: ', error.code)
          console.error('http status: ', error.status)
        }
        
        Promise.resolve()
          .then(saveCars)
          .then(searchCarsByDistance)
          .then(onSearchSuccess)
          .catch(onError)
        

        double myLatitude = 32.779108;
        double myLongitude = -96.797000;
        String whereClause = "distance( $myLatitude, $myLongitude, location.latitude, location.longitude ) < mi(5) and available = true";
        
        DataQueryBuilder dataQuery = DataQueryBuilder()
         ..whereClause = whereClause
         ..related = ["location"];
        
        Backendless.Data.of("Car").find(dataQuery).then((cars) {
         if (cars.isEmpty)
           print("Did not find any cars");
        
         cars.forEach((car) => print("Found car: ${car['make']} ${car['model']}"));
        });

        Program output:

        Found car: Toyota Prius
        Found car: Toyota Camry