Skip to main content

One post tagged with "PostGIS"

View All Tags

Building Location Based Search Service with Spring Boot PostgreSQL and PostGIS

· 12 min read
Huseyin BABAL
Software Developer

Introduction to Geospatial Data

Geospatial data, also known as spatial data, represents the physical location and shape of objects on the Earth's surface. It includes information such as latitude, longitude, altitude, and the spatial relationships between different objects. Geospatial data is used in a wide range of applications, from mapping and navigation to environmental monitoring and urban planning.

Use Cases for Geospatial Data

Geospatial data has numerous applications across various industries. Some common use cases include:

  • Navigation and Routing: GPS systems use geospatial data to provide real-time navigation and routing information.
  • Environmental Monitoring: Track changes in land use, deforestation, and urban sprawl using satellite imagery and geospatial analysis.
  • Urban Planning: Plan infrastructure projects, analyze traffic patterns, and manage public services using geospatial data.
  • Location-Based Services: Deliver personalized content, offers, and services based on a user's location.

Using Geospatial Data in PostgreSQL with PostGIS Extension

In this article, we will be using PostgreSQL as our database with PostGIS extension. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases with built-in postgis extension, simplifying setup and maintenance.

tip

Create a free database with built-in postgis extension in Rapidapp in seconds here

If you choose to maintain your own PostgreSQL database, you can enable PostGIS extension with the following command for each database as shown below.;

CREATE EXTENSION postgis;

Step-by-Step Guide to Creating the Location-Based Search Service

One practical application of geospatial data is a geolocation search application, where users can find nearby points of interest within a specified radius. In this article, we will build a Spring Boot application that searches for cities within specified radius of a given point.

Project Initialization and Dependencies

We will be using Spring Boot and PostgreSQL to build the application. You can initialize a spring boot project by using Spring Boot CLI. Once installed, you can use following command to initialize a project with required dependencies.

spring init \
--dependencies=web,data-jpa,postgresql,lombok \
--type=maven-project \
--javaVersion=21 \
spring-postgres-spatial

Line 2: web for implementing REST endpoints, data-jpa for database persistence, and postgresql for PostgreSQL driver.

Line 3: --type=maven-project for creating a Maven project.

Line 4: --javaVersion=21 we will use Java 21 in Google Cloud Run environment.

There is one more dependency we need to add to enable spatial feature of hibernate: hibernate-spatial. Open pom.xml and add following dependency to dependencies section.

<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-spatial</artifactId>
<version>6.5.2.Final</version>
</dependency>

Now that we initialized the project, go to the folder spring-postgres-spatial and open it with your favourite IDE.

Implementing Entity and Repository

We have only one entity here, City, which will be used to store city information including its location. Let's create a new entity called City as follows.

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
class City {

@Id
@GeneratedValue
private Long id;

private String name;

@Column(columnDefinition = "geography(Point, 4326)")
private Point location;
}

Line 2: Automatically enable getter/setter methods by using Lombok

Line 3: Generate no-arg constructor

Line 4: Generate constructor with all instance variables

Line 13: This is for using special PostGIS data type geography described as follows;

geography: This indicates that the column will use the PostGIS geography data type, which is designed for storing geospatial data in a way that accounts for the Earth's curvature. This type is particularly useful for global, large-scale datasets where you want accurate distance and area calculations.

Point: Specifies that the data type for this column is a geographic point. Points are used to store coordinates (latitude and longitude).

4326: This is the Spatial Reference System Identifier (SRID) for WGS 84, which is the standard coordinate system used by GPS. SRID 4326 ensures that the coordinates are stored in a globally recognized format.

In order to manage City entity in database, we will use following repository interface.

interface CityRepository extends CrudRepository<City, Integer>{
@Query("SELECT c FROM City c WHERE function('ST_DWithin', c.location, :point, :distance) = true")
Iterable<City> findNearestCities(Point point, double distance);
}

ST_DWithin returns true if the geometries are within a given distance. In our case it will return cities which has location in City table is in a distance :distance of :point

Implementing Rest Endpoints

We have one root endpoint /api/v1/cities inside one controller and implement 3 actions for create, list, and find nearest locations as follows

@RestController
@RequestMapping("/api/v1/cities")
@RequiredArgsConstructor
class CityController {

private final CityRepository cityRepository;

private final GeometryFactory geometryFactory;

@PostMapping
void create(@RequestBody CreateCityRequest request) {
Point point = geometryFactory.createPoint(new Coordinate(request.getLng(), request.getLat()));
City city = new City();
city.setName(request.getName());
city.setLocation(point);
cityRepository.save(city);
}

@GetMapping
List<CityDto> findAll() {
List<CityDto> cities = new ArrayList<>();
cityRepository.findAll().forEach(c -> {
cities.add(new CityDto(c.getName(), c.getLocation().getY(), c.getLocation().getX()));
});
return cities;
}

@GetMapping("/nearest")
List<CityDto> findNearestCities(@RequestParam("lat") float lat, @RequestParam("lng") float lng, @RequestParam("distance") int distance) {
List<CityDto> cities = new ArrayList<>();
Point point = geometryFactory.createPoint(new Coordinate(lng, lat));
cityRepository.findNearestCities(point, distance).forEach(c -> {
cities.add(new CityDto(c.getName(), c.getLocation().getY(), c.getLocation().getX()));
});
return cities;
}
}

Line 8: This comes from hibernate-spatial and it is used to do basic conversions between geometric shapes. In our case, we convert latitude-longitude pair to Point which will be used for repository operations.

create method accepts a request CreateCityRequest which contains name, latitude and longitude information as shown below.

@AllArgsConstructor
@NoArgsConstructor
@Data
class CreateCityRequest {

private String name;
private double lat;
private double lng;
}

findAll is used to list all available cities in the database.

findNearestCities is used for finding neighbour cities for a given coordinate and radius (meters).

Now we have persistence layer and rest endpoints ready and we are ready to configure application.

Application Configuration

This section contains application level configurations such as the application name, datasource, and jpa as shown below:

application.yaml
spring:
application:
name: spring-postgres-spatial
datasource:
url: <connection-string-from-rapidapp|or your own managed postgres url>
username: <username>
password: <password>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update

Line 5: Connection URL for the PostgreSQL database. You can obtain this from Rapidapp or your own managed PostgreSQL service. It should have a format like jdbc:postgresql://<host>:<port>/<database>?sslmode=require.

Running Application

You can run application as follows

./mvnw spring-boot:run

Demo

Create City

In this section, we will be creating cities of Turkey

Click to see create city requests
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Adana", "lat": "37.0000", "lng": "35.3213"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Adıyaman", "lat": "37.7648", "lng": "38.2786"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Afyonkarahisar", "lat": "38.7507", "lng": "30.5567"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Ağrı", "lat": "39.7191", "lng": "43.0503"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Amasya", "lat": "40.6499", "lng": "35.8353"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Ankara", "lat": "39.9208", "lng": "32.8541"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Antalya", "lat": "36.8841", "lng": "30.7056"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Artvin", "lat": "41.1828", "lng": "41.8183"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Aydın", "lat": "37.8560", "lng": "27.8416"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Balıkesir", "lat": "39.6484", "lng": "27.8826"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bilecik", "lat": "40.0567", "lng": "30.0665"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bingöl", "lat": "39.0626", "lng": "40.7696"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bitlis", "lat": "38.3938", "lng": "42.1232"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bolu", "lat": "40.5760", "lng": "31.5788"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Burdur", "lat": "37.4613", "lng": "30.0665"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bursa", "lat": "40.2669", "lng": "29.0634"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Çanakkale", "lat": "40.1553", "lng": "26.4142"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Çankırı", "lat": "40.6013", "lng": "33.6134"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Çorum", "lat": "40.5506", "lng": "34.9556"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Denizli", "lat": "37.7765", "lng": "29.0864"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Diyarbakır", "lat": "37.9144", "lng": "40.2306"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Edirne", "lat": "41.6818", "lng": "26.5623"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Elâzığ", "lat": "38.6810", "lng": "39.2264"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Erzincan", "lat": "39.7500", "lng": "39.5000"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Erzurum", "lat": "39.9000", "lng": "41.2700"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Eskişehir", "lat": "39.7767", "lng": "30.5206"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Gaziantep", "lat": "37.0662", "lng": "37.3833"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Giresun", "lat": "40.9128", "lng": "38.3895"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Gümüşhane", "lat": "40.4386", "lng": "39.5086"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Hakkâri", "lat": "37.5833", "lng": "43.7333"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Hatay", "lat": "36.4018", "lng": "36.3498"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Isparta", "lat": "37.7648", "lng": "30.5566"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Mersin", "lat": "36.8000", "lng": "34.6333"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "İstanbul", "lat": "41.0053", "lng": "28.9770"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "İzmir", "lat": "38.4189", "lng": "27.1287"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kars", "lat": "40.6167", "lng": "43.1000"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kastamonu", "lat": "41.3887", "lng": "33.7827"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kayseri", "lat": "38.7312", "lng": "35.4787"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kırklareli", "lat": "41.7333", "lng": "27.2167"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kırşehir", "lat": "39.1425", "lng": "34.1709"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kocaeli", "lat": "40.8533", "lng": "29.8815"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Konya", "lat": "37.8667", "lng": "32.4833"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kütahya", "lat": "39.4167", "lng": "29.9833"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Malatya", "lat": "38.3552", "lng": "38.3095"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Manisa", "lat": "38.6191", "lng": "27.4289"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kahramanmaraş", "lat": "37.5858", "lng": "36.9371"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Mardin", "lat": "37.3212", "lng": "40.7245"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Muğla", "lat": "37.2153", "lng": "28.3636"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Muş", "lat": "38.9462", "lng": "41.7539"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Nevşehir", "lat": "38.6939", "lng": "34.6857"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Niğde", "lat": "37.9667", "lng": "34.6833"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Ordu", "lat": "40.9839", "lng": "37.8764"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Rize", "lat": "41.0201", "lng": "40.5234"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Sakarya", "lat": "40.6940", "lng": "30.4358"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Samsun", "lat": "41.2928", "lng": "36.3313"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Siirt", "lat": "37.9333", "lng": "41.9500"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Sinop", "lat": "42.0231", "lng": "35.1531"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Sivas", "lat": "39.7477", "lng": "37.0179"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Tekirdağ", "lat": "40.9833", "lng": "27.5167"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Tokat", "lat": "40.3167", "lng": "36.5500"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Trabzon", "lat": "41.0015", "lng": "39.7178"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Tunceli", "lat": "39.3074", "lng": "39.4388"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Şanlıurfa", "lat": "37.1591", "lng": "38.7969"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Uşak", "lat": "38.6823", "lng": "29.4082"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Van", "lat": "38.4891", "lng": "43.4089"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Yozgat", "lat": "39.8181", "lng": "34.8147"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Zonguldak", "lat": "41.4564", "lng": "31.7987"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Aksaray", "lat": "38.3687", "lng": "34.0370"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bayburt", "lat": "40.2552", "lng": "40.2249"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Karaman", "lat": "37.1759", "lng": "33.2287"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kırıkkale", "lat": "39.8468", "lng": "33.5153"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Batman", "lat": "37.8812", "lng": "41.1351"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Şırnak", "lat": "37.4187", "lng": "42.4918"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Bartın", "lat": "41.5811", "lng": "32.4610"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Ardahan", "lat": "41.1105", "lng": "42.7022"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Iğdır", "lat": "39.8880", "lng": "44.0048"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Yalova", "lat": "40.6500", "lng": "29.2667"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Karabük", "lat": "41.2061", "lng": "32.6204"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Kilis", "lat": "36.7184", "lng": "37.1212"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Osmaniye", "lat": "37.2130", "lng": "36.1763"}'
curl -XPOST -H "Content-Type: application/json" http://localhost:8080/api/v1/cities -d '{"name": "Düzce", "lat": "40.8438", "lng": "31.1565"}'

List Cities

curl -XGET http://localhost:8080/api/v1/cities

Find Nearest Cities

To find nearest cities of Ankara within a radius 300km, you can use the following.

curl -XGET http://localhost:8080/api/v1/cities/nearest\?lat\=39.9208\&lng\=32.8541\&distance\=300000

Conclusion

In this article, we explored the power of geospatial data and how to effectively utilize it within a Spring Boot application using PostgreSQL with the PostGIS extension. We covered the fundamental concepts of geospatial data, the benefits of using PostGIS for geospatial operations, and real-world use cases such as navigation, environmental monitoring, urban planning, and location-based services.

tip

You can find the complete source code for this project on GitHub.