diff --git a/delivery/controller/nearest_device_controller.go b/delivery/controller/nearest_device_controller.go index 40ee5db..59e33cd 100644 --- a/delivery/controller/nearest_device_controller.go +++ b/delivery/controller/nearest_device_controller.go @@ -33,9 +33,79 @@ func (c *NearestDeviceController) Route() { { nearestDevices.POST("/search", c.getNearestDevices) nearestDevices.GET("/:id", c.getNearestDeviceByID) + + nearestDevices.POST("/towers/search", c.getNearestTowers) + nearestDevices.GET("/towers/:id", c.getNearestTowerByID) } } +func (c *NearestDeviceController) getNearestTowers(ctx *gin.Context) { + var request req.NearestTowerDTO + if err := ctx.ShouldBindJSON(&request); err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + towers, err := c.nearestDeviceUC.GetNearestTowers(request) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, err.Error()) + return + } + + response := gin.H{ + "towers": towers, + "total": len(towers), + "search_params": gin.H{ + "latitude": request.Latitude, + "longitude": request.Longitude, + "radius": request.Radius, + "province": request.Province, + "city": request.City, + "district": request.District, + }, + } + + common.SingleResponses(ctx, "Nearest towers retrieved successfully", response) +} + +func (c *NearestDeviceController) getNearestTowerByID(ctx *gin.Context) { + id := ctx.Param("id") + towerID, err := uuid.Parse(id) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid tower ID") + return + } + + // Get user coordinates from query params + latStr := ctx.Query("lat") + lngStr := ctx.Query("lng") + + if latStr == "" || lngStr == "" { + common.ErrorResponses(ctx, http.StatusBadRequest, "User coordinates (lat, lng) are required") + return + } + + userLat, err := strconv.ParseFloat(latStr, 64) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid latitude") + return + } + + userLng, err := strconv.ParseFloat(lngStr, 64) + if err != nil { + common.ErrorResponses(ctx, http.StatusBadRequest, "Invalid longitude") + return + } + + tower, err := c.nearestDeviceUC.GetNearestTowerByID(towerID, userLat, userLng) + if err != nil { + common.ErrorResponses(ctx, http.StatusNotFound, err.Error()) + return + } + + common.SingleResponses(ctx, "Tower details retrieved successfully", tower) +} + func (c *NearestDeviceController) getNearestDevices(ctx *gin.Context) { var request req.NearestDeviceDTO if err := ctx.ShouldBindJSON(&request); err != nil { diff --git a/model/dto/req/nearest_towers.go b/model/dto/req/nearest_towers.go new file mode 100644 index 0000000..d64bbfe --- /dev/null +++ b/model/dto/req/nearest_towers.go @@ -0,0 +1,11 @@ +package req + +type NearestTowerDTO struct { + Longitude float64 `json:"longitude" validate:"required"` + Latitude float64 `json:"latitude" validate:"required"` + Radius float64 `json:"radius" validate:"omitempty,min=0.1,max=50"` // Default 5km, max 50km + Limit int `json:"limit" validate:"omitempty,min=1,max=100"` // Default 10, max 100 + Province *string `json:"province,omitempty"` + City *string `json:"city,omitempty"` + District *string `json:"district,omitempty"` +} \ No newline at end of file diff --git a/model/dto/res/nearest_towers.go b/model/dto/res/nearest_towers.go new file mode 100644 index 0000000..701fd74 --- /dev/null +++ b/model/dto/res/nearest_towers.go @@ -0,0 +1,54 @@ +package res + +import ( + "time" + "github.com/google/uuid" +) + +// Simplified response for list +type NearestTowerResponse struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` + Distance float64 `json:"distance_km"` + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL *string `json:"image_url,omitempty"` + ExternalTower *bool `json:"external_tower,omitempty"` + DeviceCode *string `json:"device_code,omitempty"` + Province *string `json:"province,omitempty"` + City *string `json:"city,omitempty"` + District *string `json:"district,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// Detailed response for single tower +type NearestTowerDetailResponse struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` + Distance float64 `json:"distance_km"` + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL *string `json:"image_url,omitempty"` + ImageURLs []string `json:"image_urls,omitempty"` + ExternalTower *bool `json:"external_tower,omitempty"` + + // Connected device information + Device *DeviceConnectionInfo `json:"device,omitempty"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type DeviceConnectionInfo struct { + ID uuid.UUID `json:"id"` + DeviceCode string `json:"device_code"` + DeviceType string `json:"device_type"` + Distance float64 `json:"distance_from_towers_to_device_km"` // Distance from tower to device + Address string `json:"address"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Status string `json:"status"` + PortAmount int `json:"port_amount"` +} \ No newline at end of file diff --git a/model/entity/tower.go b/model/entity/tower.go index 07424f6..1ba0f93 100644 --- a/model/entity/tower.go +++ b/model/entity/tower.go @@ -22,6 +22,23 @@ type Tower struct { Device Device `json:"device,omitempty" gorm:"foreignKey:DeviceID"` } +type TowerWithDistance struct { + ID uuid.UUID `json:"id"` + TowerCode string `json:"tower_code"` + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + ImageURL *string `json:"image_url"` + ExternalTower *bool `json:"external_tower"` + DevID *uuid.UUID `json:"dev_id"` + DeviceCode *string `json:"device_code"` + Province *string `json:"province"` + City *string `json:"city"` + District *string `json:"district"` + Distance float64 `json:"distance"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + // Helper method to get all image URLs func (t *Tower) GetAllImageURLs() []string { var allImages []string diff --git a/repository/nearest_device_repo.go b/repository/nearest_device_repo.go index 025b364..75865f5 100644 --- a/repository/nearest_device_repo.go +++ b/repository/nearest_device_repo.go @@ -13,6 +13,8 @@ type NearestDeviceRepo interface { GetFishbonesByDeviceID(deviceID uuid.UUID) ([]entity.Fishbone, error) GetTowersByDeviceID(deviceID uuid.UUID) ([]entity.Tower, error) CountConnectionsByDeviceID(deviceID uuid.UUID) (backboneCount, fishboneCount, towerCount int, err error) + GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) + GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) } @@ -26,6 +28,45 @@ func NewNearestDeviceRepo(db *gorm.DB) NearestDeviceRepo { } } +func (r *nearestDeviceRepo) GetNearestTowers(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.TowerWithDistance, error) { + var towers []entity.TowerWithDistance + + // Build the subquery first - join with devices to get location filters + subQuery := r.db.Table("towers"). + Select(`towers.id, towers.tower_code, towers.longitude, towers.latitude, + towers.image_url, towers.external_tower, towers.dev_id, towers.created_at, towers.updated_at, + devices.device_code, devices.province, devices.city, devices.district, + (6371 * acos(cos(radians(?)) * cos(radians(towers.latitude)) * cos(radians(towers.longitude) - radians(?)) + sin(radians(?)) * sin(radians(towers.latitude)))) AS distance`, + latitude, longitude, latitude). + Joins("LEFT JOIN devices ON towers.dev_id = devices.id") + + // Apply location filters to subquery + if province != nil && *province != "" { + subQuery = subQuery.Where("devices.province = ?", *province) + } + if city != nil && *city != "" { + subQuery = subQuery.Where("devices.city = ?", *city) + } + if district != nil && *district != "" { + subQuery = subQuery.Where("devices.district = ?", *district) + } + + // Use the subquery in the main query + err := r.db.Table("(?) as towers_with_distance", subQuery). + Where("distance <= ?", radius). + Order("distance ASC"). + Limit(limit). + Scan(&towers).Error + + return towers, err +} + +func (r *nearestDeviceRepo) GetTowerByIDWithConnections(id uuid.UUID) (entity.Tower, error) { + var tower entity.Tower + err := r.db.Preload("Device").Where("id = ?", id).First(&tower).Error + return tower, err +} + func (r *nearestDeviceRepo) GetNearestDevices(longitude, latitude, radius float64, limit int, province, city, district *string) ([]entity.DeviceWithDistance, error) { var devices []entity.DeviceWithDistance diff --git a/repository/tower_repo.go b/repository/tower_repo.go index 2da4470..f61ea31 100644 --- a/repository/tower_repo.go +++ b/repository/tower_repo.go @@ -28,6 +28,7 @@ func NewTowerRepo(db *gorm.DB) TowerRepo { } } + func (r *towerRepo) Post(tower entity.Tower) error { err := r.db.Create(&tower).Error if err != nil { diff --git a/usecase/nearest_device.go b/usecase/nearest_device.go index 504398d..c5dd4fd 100644 --- a/usecase/nearest_device.go +++ b/usecase/nearest_device.go @@ -15,6 +15,8 @@ import ( type NearestDeviceUseCase interface { GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) GetNearestDeviceByID(id uuid.UUID, userLat, userLng float64) (res.NearestDeviceDetailResponse, error) + GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error) + GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error) } type nearestDeviceUseCase struct { @@ -31,6 +33,61 @@ func NewNearestDeviceUseCase(nearestDeviceRepo repository.NearestDeviceRepo, geo } } +func (u *nearestDeviceUseCase) GetNearestTowers(request req.NearestTowerDTO) ([]res.NearestTowerResponse, error) { + err := u.validate.Struct(request) + if err != nil { + return nil, err + } + + // Set defaults + radius := request.Radius + if radius == 0 { + radius = 5.0 // Default 5km + } + + limit := request.Limit + if limit == 0 { + limit = 10 // Default 10 towers + } + + towers, err := u.nearestDeviceRepo.GetNearestTowers( + request.Longitude, + request.Latitude, + radius, + limit, + request.Province, + request.City, + request.District, + ) + if err != nil { + return nil, err + } + + responses, err := helper.ConvertToNearestTowerResponses(towers, u.geocoder) + if err != nil { + return nil, err + } + + return responses, nil +} + +func (u *nearestDeviceUseCase) GetNearestTowerByID(id uuid.UUID, userLat, userLng float64) (res.NearestTowerDetailResponse, error) { + tower, err := u.nearestDeviceRepo.GetTowerByIDWithConnections(id) + if err != nil { + return res.NearestTowerDetailResponse{}, err + } + + // Calculate distance + distance := calculateDistance(userLat, userLng, tower.Latitude, tower.Longitude) + + response, err := helper.ConvertToNearestTowerDetailResponse(tower, distance, u.nearestDeviceRepo, u.geocoder) + if err != nil { + return res.NearestTowerDetailResponse{}, err + } + + return response, nil +} + func (u *nearestDeviceUseCase) GetNearestDevices(request req.NearestDeviceDTO) ([]res.NearestDeviceResponse, error) { err := u.validate.Struct(request) if err != nil { diff --git a/utils/helper/nearest_towers.go b/utils/helper/nearest_towers.go new file mode 100644 index 0000000..46f1720 --- /dev/null +++ b/utils/helper/nearest_towers.go @@ -0,0 +1,114 @@ +package helper + +import ( + "fmt" + "log" + "users_management/m/model/dto/res" + "users_management/m/model/entity" + "users_management/m/repository" + "users_management/m/utils/service" + + "github.com/google/uuid" +) + +func ConvertToNearestTowerResponses(towers []entity.TowerWithDistance, geocoder service.GeocodingService) ([]res.NearestTowerResponse, error) { + var responses []res.NearestTowerResponse + + for _, tower := range towers { + // Get address + address := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", tower.Latitude, tower.Longitude) + } else { + address = addr + } + } + + response := res.NearestTowerResponse{ + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: tower.Distance, + Address: address, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + ImageURL: tower.ImageURL, + ExternalTower: tower.ExternalTower, + DeviceCode: tower.DeviceCode, + Province: tower.Province, + City: tower.City, + District: tower.District, + CreatedAt: tower.CreatedAt, + } + + responses = append(responses, response) + } + + return responses, nil +} + +func ConvertToNearestTowerDetailResponse(tower entity.Tower, distance float64, repo repository.NearestDeviceRepo, geocoder service.GeocodingService) (res.NearestTowerDetailResponse, error) { + // Get address + address := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(tower.Latitude, tower.Longitude) + if err != nil { + log.Printf("Geocoding error for tower %s: %v", tower.TowerCode, err) + address = fmt.Sprintf("Coordinates: %.6f, %.6f", tower.Latitude, tower.Longitude) + } else { + address = addr + } + } + + // Get all image URLs + allImageURLs := tower.GetAllImageURLs() + + var deviceInfo *res.DeviceConnectionInfo + if tower.Device.ID != uuid.Nil { + // Get device address + deviceAddress := "" + if geocoder != nil { + addr, err := geocoder.GetAddressFromCoordinates(tower.Device.Latitude, tower.Device.Longitude) + if err != nil { + log.Printf("Geocoding error for device %s: %v", tower.Device.DeviceCode, err) + deviceAddress = fmt.Sprintf("Coordinates: %.6f, %.6f", tower.Device.Latitude, tower.Device.Longitude) + } else { + deviceAddress = addr + } + } + + // Calculate distance from tower to device + deviceDistance := calculateDistance(tower.Latitude, tower.Longitude, tower.Device.Latitude, tower.Device.Longitude) + + deviceInfo = &res.DeviceConnectionInfo{ + ID: tower.Device.ID, + DeviceCode: tower.Device.DeviceCode, + DeviceType: string(tower.Device.DeviceType), + Distance: deviceDistance, + Address: deviceAddress, + Longitude: tower.Device.Longitude, + Latitude: tower.Device.Latitude, + Status: string(tower.Device.Status), + PortAmount: tower.Device.PortAmount, + } + } + + response := res.NearestTowerDetailResponse{ + ID: tower.ID, + TowerCode: tower.TowerCode, + Distance: distance, + Address: address, + Longitude: tower.Longitude, + Latitude: tower.Latitude, + ImageURL: &tower.ImageURL, + ImageURLs: allImageURLs, + ExternalTower: tower.ExternalTower, + Device: deviceInfo, + CreatedAt: tower.CreatedAt, + UpdatedAt: tower.UpdatedAt, + } + + return response, nil +} \ No newline at end of file