diff --git a/README.md b/README.md index dfaac317..d56108da 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # chulbong- :kr:
- +
diff --git a/backend/handler/comment_api.go b/backend/handler/comment_api.go index 81e5e3fb..fcb0074b 100644 --- a/backend/handler/comment_api.go +++ b/backend/handler/comment_api.go @@ -1,8 +1,8 @@ package handler import ( + "errors" "strconv" - "strings" "github.com/Alfex4936/chulbong-kr/dto" "github.com/Alfex4936/chulbong-kr/middleware" @@ -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) } diff --git a/backend/resource/korea_addresses.dawg b/backend/resource/korea_addresses.dawg new file mode 100644 index 00000000..12090c2e Binary files /dev/null and b/backend/resource/korea_addresses.dawg differ diff --git a/backend/resource/marker.webp b/backend/resource/marker.webp new file mode 100644 index 00000000..f7413916 Binary files /dev/null and b/backend/resource/marker.webp differ diff --git a/backend/resource/marker_40x40.webp b/backend/resource/marker_40x40.webp new file mode 100644 index 00000000..8e50fccd Binary files /dev/null and b/backend/resource/marker_40x40.webp differ diff --git a/backend/resource/nanum.ttf b/backend/resource/nanum.ttf new file mode 100644 index 00000000..6e4dd874 Binary files /dev/null and b/backend/resource/nanum.ttf differ diff --git a/backend/service/clustering/cluster.go b/backend/service/clustering/cluster.go index b9d9de1b..9120bd4d 100644 --- a/backend/service/clustering/cluster.go +++ b/backend/service/clustering/cluster.go @@ -3,55 +3,56 @@ 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) + } } } } @@ -59,83 +60,96 @@ func regionQuery(tree *rtreego.Rtree, points []Point, p Point, eps float64) []in 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) } } diff --git a/backend/service/comment_service.go b/backend/service/comment_service.go index e9c5fce5..9b9d60cc 100644 --- a/backend/service/comment_service.go +++ b/backend/service/comment_service.go @@ -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 @@ -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 diff --git a/backend/service/error_service.go b/backend/service/error_service.go index 7346e127..1a07d2ff 100644 --- a/backend/service/error_service.go +++ b/backend/service/error_service.go @@ -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") ) diff --git a/backend/service/marker_location_service.go b/backend/service/marker_location_service.go index 21e8244c..c748828f 100644 --- a/backend/service/marker_location_service.go +++ b/backend/service/marker_location_service.go @@ -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() }