adding nearest device for towers

This commit is contained in:
areeqakbr 2025-06-18 16:42:03 +07:00
parent 0f034cc332
commit caee765b8b
8 changed files with 365 additions and 0 deletions

View File

@ -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 {

View File

@ -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"`
}

View File

@ -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"`
}

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}