Skip to content

Commit

Permalink
[Go] Test DBSCAN
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfex4936 committed Sep 20, 2024
1 parent ca121e7 commit da4261a
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 94 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# chulbong- :kr:
<p align="center">
<img width="100" src="https://github.com/Alfex4936/chulbong-kr/assets/2356749/6236863a-11e1-45d5-b2e2-9bcf40363e1d" alt="k-pullup logo"/></br>
<img width="1024" alt="2024-09-16 pullupbars" src="https://github.com/user-attachments/assets/66de6def-2b11-4f9b-88d3-810e44728914" />
<img width="1024" alt="2024-09-20 pullupbars" src="https://github.com/user-attachments/assets/a9ff64b2-52fa-4f40-be37-82d7893ba963" />
<img alt="GitHub commit activity (branch)" src="https://img.shields.io/github/commit-activity/w/Alfex4936/chulbong-kr/main">
</p>

Expand Down
13 changes: 9 additions & 4 deletions backend/handler/comment_api.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package handler

import (
"errors"
"strconv"
"strings"

"github.com/Alfex4936/chulbong-kr/dto"
"github.com/Alfex4936/chulbong-kr/middleware"
Expand Down Expand Up @@ -55,11 +55,16 @@ func (h *CommentHandler) HandlePostComment(c *fiber.Ctx) error {

comment, err := h.CommentService.CreateComment(req.MarkerID, userID, userName, req.CommentText)
if err != nil {
if strings.Contains(err.Error(), "already commented") {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "you have already commented 3 times on this marker"})
switch {
case errors.Is(err, service.ErrMaxCommentsReached):
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "You have already commented 3 times on this marker"})
case errors.Is(err, service.ErrMarkerNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Marker not found"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create comment"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create comment"})
}

return c.Status(fiber.StatusOK).JSON(comment)
}

Expand Down
Binary file added backend/resource/korea_addresses.dawg
Binary file not shown.
Binary file added backend/resource/marker.webp
Binary file not shown.
Binary file added backend/resource/marker_40x40.webp
Binary file not shown.
Binary file added backend/resource/nanum.ttf
Binary file not shown.
170 changes: 92 additions & 78 deletions backend/service/clustering/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,139 +3,153 @@ package main
import (
"fmt"
"math"
"sync"

"github.com/dhconnelly/rtreego"
)

// Point represents a geospatial point
type Point struct {
Latitude float64
Longitude float64
ClusterID int // Used to identify which cluster this point belongs to
ClusterID int // 0: unvisited, -1: noise, >0: cluster ID
Address string
}

// RTreePoint wraps a Point to implement the rtreego.Spatial interface
type RTreePoint struct {
Point
type Grid struct {
CellSize float64
Cells map[int]map[int][]*Point
}

func (p RTreePoint) Bounds() rtreego.Rect {
return rtreego.Point{p.Latitude, p.Longitude}.ToRect(0.00001)
func NewGrid(cellSize float64) *Grid {
return &Grid{
CellSize: cellSize,
Cells: make(map[int]map[int][]*Point),
}
}

// Haversine formula to calculate geographic distance between points
func distance(lat1, lon1, lat2, lon2 float64) float64 {
rad := math.Pi / 180
r := 6371e3 // Earth radius in meters
dLat := (lat2 - lat1) * rad
dLon := (lon2 - lon1) * rad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*rad)*math.Cos(lat2*rad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return r * c // Distance in meters
func (g *Grid) Insert(p *Point) {
xIdx := int(p.Longitude / g.CellSize)
yIdx := int(p.Latitude / g.CellSize)

if _, ok := g.Cells[xIdx]; !ok {
g.Cells[xIdx] = make(map[int][]*Point)
}
g.Cells[xIdx][yIdx] = append(g.Cells[xIdx][yIdx], p)
}

// regionQuery returns the indices of all points within eps distance of point p using an R-tree for efficient querying
func regionQuery(tree *rtreego.Rtree, points []Point, p Point, eps float64) []int {
epsDeg := eps / 111000 // Approximate conversion from meters to degrees
searchRect := rtreego.Point{p.Latitude, p.Longitude}.ToRect(epsDeg)
results := tree.SearchIntersect(searchRect)

var neighbors []int
for _, item := range results {
rtp := item.(RTreePoint)
if distance(p.Latitude, p.Longitude, rtp.Latitude, rtp.Longitude) < eps {
for idx, pt := range points {
if pt.Latitude == rtp.Latitude && pt.Longitude == rtp.Longitude {
neighbors = append(neighbors, idx)
break
func (g *Grid) GetNeighbors(p *Point, eps float64) []*Point {
epsDeg := eps / 111000.0 // Convert meters to degrees
cellRadius := int(math.Ceil(epsDeg / g.CellSize))

xIdx := int(p.Longitude / g.CellSize)
yIdx := int(p.Latitude / g.CellSize)

neighbors := []*Point{}

for dx := -cellRadius; dx <= cellRadius; dx++ {
for dy := -cellRadius; dy <= cellRadius; dy++ {
nx := xIdx + dx
ny := yIdx + dy

if cell, ok := g.Cells[nx][ny]; ok {
for _, np := range cell {
if distance(p.Latitude, p.Longitude, np.Latitude, np.Longitude) <= eps {
neighbors = append(neighbors, np)
}
}
}
}
}
return neighbors
}

// Optimized Haversine formula for small distances
func distance(lat1, lon1, lat2, lon2 float64) float64 {
const (
rad = math.Pi / 180
r = 6371e3 // Earth radius in meters
)
dLat := (lat2 - lat1) * rad
dLon := (lon2 - lon1) * rad
lat1Rad := lat1 * rad
lat2Rad := lat2 * rad

sinDLat := math.Sin(dLat / 2)
sinDLon := math.Sin(dLon / 2)

a := sinDLat*sinDLat + math.Cos(lat1Rad)*math.Cos(lat2Rad)*sinDLon*sinDLon
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return r * c // Distance in meters
}

// expandCluster expands the cluster with id clusterID by recursively adding all density-reachable points
func expandCluster(tree *rtreego.Rtree, points []Point, pIndex int, neighbors []int, clusterID int, eps float64, minPts int, wg *sync.WaitGroup, mu *sync.Mutex) {
defer wg.Done()

points[pIndex].ClusterID = clusterID
i := 0
for i < len(neighbors) {
nIndex := neighbors[i]
if points[nIndex].ClusterID == 0 { // Not visited
points[nIndex].ClusterID = clusterID
nNeighbors := regionQuery(tree, points, points[nIndex], eps)
func expandCluster(grid *Grid, p *Point, neighbors []*Point, clusterID int, eps float64, minPts int) {
p.ClusterID = clusterID

queue := make([]*Point, 0, len(neighbors))
queue = append(queue, neighbors...)

for len(queue) > 0 {
current := queue[0]
queue = queue[1:]

if current.ClusterID == 0 { // Unvisited
current.ClusterID = clusterID
nNeighbors := grid.GetNeighbors(current, eps)
if len(nNeighbors) >= minPts {
neighbors = append(neighbors, nNeighbors...)
queue = append(queue, nNeighbors...)
}
} else if points[nIndex].ClusterID == -1 { // Change noise to border point
points[nIndex].ClusterID = clusterID
}
i++
}

mu.Lock()
for _, neighborIdx := range neighbors {
if points[neighborIdx].ClusterID == 0 {
points[neighborIdx].ClusterID = clusterID
} else if current.ClusterID == -1 { // Noise
current.ClusterID = clusterID
}
}
mu.Unlock()
}

// DBSCAN performs DBSCAN clustering on the points
func DBSCAN(points []Point, eps float64, minPts int) []Point {
func DBSCAN(points []*Point, eps float64, minPts int) {
clusterID := 0
tree := rtreego.NewTree(2, 25, 50) // Create an R-tree for 2D points
epsDeg := eps / 111000.0 // Convert meters to degrees
cellSize := epsDeg // Use epsDeg as cell size

grid := NewGrid(cellSize)

for _, p := range points {
tree.Insert(RTreePoint{p})
grid.Insert(p)
}

var wg sync.WaitGroup
var mu sync.Mutex

for i := range points {
if points[i].ClusterID != 0 {
for _, p := range points {
if p.ClusterID != 0 {
continue
}
neighbors := regionQuery(tree, points, points[i], eps)
neighbors := grid.GetNeighbors(p, eps)
if len(neighbors) < minPts {
points[i].ClusterID = -1
p.ClusterID = -1 // Mark as noise
} else {
clusterID++
wg.Add(1)
go expandCluster(tree, points, i, neighbors, clusterID, eps, minPts, &wg, &mu)
expandCluster(grid, p, neighbors, clusterID, eps, minPts)
}
}

wg.Wait()
return points
}

func main() {
points := []Point{
points := []*Point{
{Latitude: 37.55808862059195, Longitude: 126.95976545165765, Address: "서울 서대문구 북아현동 884"},
{Latitude: 37.568166, Longitude: 126.974102, Address: "서울 중구 정동 1-76"},
{Latitude: 37.568661, Longitude: 126.972375, Address: "서울 종로구 신문로2가 171"},
{Latitude: 37.56885, Longitude: 126.972064, Address: "서울 종로구 신문로2가 171"},
{Latitude: 37.56589411615361, Longitude: 126.96930309974685, Address: "서울 중구 순화동 1-1"},
{Latitude: 37.55808862059195, Longitude: 126.95976545165765, Address: "서울 서대문구 북아현동 884"},
{Latitude: 37.57838984677184, Longitude: 126.98853202207196, Address: "서울 종로구 원서동 181"},
{Latitude: 37.57318309415514, Longitude: 126.95501424473001, Address: "서울 서대문구 현저동 101"},
{Latitude: 37.5541479820707, Longitude: 126.98370331932351, Address: "서울 중구 회현동1가 산 1-2"},
{Latitude: 37.58411863798303, Longitude: 126.97246285644356, Address: "서울 종로구 궁정동 17-3"},
{Latitude: 36.33937565888829, Longitude: 127.41575408006757, Address: "대전 중구 선화동 223"},
{Latitude: 36.346176003613984, Longitude: 127.41482385609581, Address: "대전 대덕구 오정동 496-1"},
}

eps := 500.0 // Epsilon in meters
minPts := 2 // Minimum number of points to form a dense region
eps := 5000.0 // Epsilon in meters
minPts := 2 // Minimum number of points to form a dense region

clusteredPoints := DBSCAN(points, eps, minPts)
for _, p := range clusteredPoints {
// Will group points within a region of 500 meters
DBSCAN(points, eps, minPts)

for _, p := range points {
fmt.Printf("Point at (%f, %f), Address: %s, Cluster ID: %d\n", p.Latitude, p.Longitude, p.Address, p.ClusterID)
}
}
4 changes: 2 additions & 2 deletions backend/service/comment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (s *MarkerCommentService) CreateComment(markerID, userID int, userName, com
return nil, fmt.Errorf("error checking if marker exists: %w", err)
}
if !exists {
return nil, fmt.Errorf("marker with ID %d does not exist", markerID)
return nil, ErrMarkerNotFound
}

// Check if the user has already commented 3 times on this marker
Expand All @@ -68,7 +68,7 @@ func (s *MarkerCommentService) CreateComment(markerID, userID int, userName, com
return nil, fmt.Errorf("error checking comment count: %w", err)
}
if commentCount >= 3 {
return nil, fmt.Errorf("user with ID %d has already commented 3 times on marker with ID %d", userID, markerID)
return nil, ErrMaxCommentsReached
}

// Create the comment instance
Expand Down
11 changes: 7 additions & 4 deletions backend/service/error_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package service
import "errors"

var (
ErrFileUpload = errors.New("an error during file upload")
ErrNoFiles = errors.New("upload at least one picture to prove")
ErrMarkerNotExist = errors.New("check if a marker exists")
ErrNoPhotos = errors.New("upload at least one photo")
ErrFileUpload = errors.New("an error during file upload")
ErrNoFiles = errors.New("upload at least one picture to prove")
ErrNoPhotos = errors.New("upload at least one photo")

// Comment
ErrMarkerNotFound = errors.New("marker not found")
ErrMaxCommentsReached = errors.New("user has reached the maximum number of comments")
)
7 changes: 2 additions & 5 deletions backend/service/marker_location_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,11 @@ func (s *MarkerLocationService) TestDynamic(latitude, longitude, zoomScale float
}

func formatPoint(lat, long float64) string {
// Format the latitude and longitude with 6 decimal places
latStr := strconv.FormatFloat(lat, 'f', 6, 64)
longStr := strconv.FormatFloat(long, 'f', 6, 64)
var sb strings.Builder
sb.WriteString("POINT(")
sb.WriteString(latStr)
sb.WriteString(strconv.FormatFloat(lat, 'f', 6, 64))
sb.WriteString(" ")
sb.WriteString(longStr)
sb.WriteString(strconv.FormatFloat(long, 'f', 6, 64))
sb.WriteString(")")
return sb.String()
}

0 comments on commit da4261a

Please sign in to comment.