Skip to content

Commit

Permalink
Merge branch 'optimize-route-order'
Browse files Browse the repository at this point in the history
  • Loading branch information
lil5 committed Jul 20, 2023
2 parents b94329c + 31041bf commit 3fba26d
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 6 deletions.
2 changes: 2 additions & 0 deletions frontend/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"approveParticipant": "Approve “{{ name }}'s” request to join the loop?",
"denyParticipant": "Deny “{{ name }}'s” request to join the loop?",
"route": "Route",
"routeOptimize": "Optimize Route",
"routeUndoOptimize": "Undo Optimize Route",
"approve": "Approve",
"pendingApproval": "Pending Host Approval",
"deny": "Deny",
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/api/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UID } from "./types";
import { OptimalPath, UID } from "./types";

export function routeGetOrder(chainUID: UID) {
return window.axios.get<UID[]>("/v2/route/order", {
Expand All @@ -12,3 +12,9 @@ export function routeSetOrder(chainUID: UID, userUIDs: UID[]) {
route_order: userUIDs,
});
}

export function routeOptimizeOrder(chainUID: UID) {
return window.axios.get<OptimalPath>("/v2/route/optimize", {
params: { chain_uid: chainUID },
});
}
5 changes: 5 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,8 @@ export interface Event {
chain_name: string;
image_url?: string;
}

export interface OptimalPath {
minimal_cost: number;
optimal_path: UID[];
}
59 changes: 54 additions & 5 deletions frontend/src/pages/ChainMemberList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { ToastContext } from "../providers/ToastProvider";
import { SizeBadges } from "../components/Badges";
import FormJup from "../util/form-jup";
import { GinParseErrors } from "../util/gin-errors";
import { routeGetOrder, routeSetOrder } from "../api/route";
import { routeGetOrder, routeOptimizeOrder, routeSetOrder } from "../api/route";
import useToClipboard from "../util/to-clipboard.hooks";
import { bagGetAllByChain } from "../api/bag";
import { Sleep } from "../util/sleep";
Expand Down Expand Up @@ -73,6 +73,9 @@ export default function ChainMemberList() {
const [users, setUsers] = useState<User[] | null>(null);
const [bags, setBags] = useState<Bag[] | null>(null);
const [route, setRoute] = useState<UID[] | null>(null);
const [routeWasOptimized, setRouteWasOptimized] = useState<boolean>(false);
const [previousRoute, setPreviousRoute] = useState<UID[] | null>(null);

const [participantsSortBy, setParticipantsSortBy] =
useState<ParticipantsSortBy>("date");
const [unapprovedUsers, setUnapprovedUsers] = useState<User[] | null>(null);
Expand Down Expand Up @@ -251,6 +254,29 @@ export default function ChainMemberList() {
});
}

function optimizeRoute(chainUID: UID) {
routeOptimizeOrder(chainUID)
.then((res) => {
const optimal_path = res.data.optimal_path;

// saving current rooute before changing in the database
setPreviousRoute(route);
setRouteWasOptimized(true);
// set new order
routeSetOrder(chainUID, optimal_path);
setRoute(optimal_path);
})
.catch((err) => {
addToastError(GinParseErrors(t, err), err.status);
});
}

function returnToPreviousRoute(chainUID: UID) {
setRoute(previousRoute);
setRouteWasOptimized(false);
routeSetOrder(chainUID, previousRoute!);
}

useEffect(() => {
refresh(true);
}, [history, authUser]);
Expand Down Expand Up @@ -464,14 +490,14 @@ export default function ChainMemberList() {
</div>

<div className="max-w-screen-xl mx-auto px-2 sm:px-8">
<div className="grid gap-4 sm:grid-cols-3 justify-items-center sm:justify-items-start">
<h2 className="order-1 sm:col-span-3 lg:col-span-1 font-semibold text-secondary self-center text-3xl">
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 justify-items-center md:justify-items-start">
<h2 className="order-1 md:col-span-full lg:col-span-1 font-semibold text-secondary self-center text-3xl">
{t("loopParticipant", {
count: unapprovedUsers.length + users.length,
})}
</h2>

<div className="order-3 sm:col-span-2 sm:order-2 lg:col-span-1 lg:justify-self-center flex border border-secondary bg-teal-light">
<div className="order-3 sm:order-2 lg:col-span-1 lg:justify-self-center flex border border-secondary bg-teal-light">
<label>
<input
type="radio"
Expand Down Expand Up @@ -536,7 +562,30 @@ export default function ChainMemberList() {
</div>
</label>
</div>
<div className="order-2 sm:justify-self-end sm:self sm:order-3">
<div className="order-2 md:justify-self-end md:self md:order-3">
{selectedTable === "route" ? (
!routeWasOptimized ? (
<button
type="button"
className="btn btn-secondary btn-outline mr-4 rtl:mr-0 rtl:ml-4"
onClick={() => optimizeRoute(chain.uid)}
>
{t("routeOptimize")}
<span className="feather feather-zap ms-3 text-primary" />
</button>
) : (
<button
type="button"
className="btn btn-secondary btn-outline mr-4 rtl:mr-0 rtl:ml-4"
onClick={() => returnToPreviousRoute(chain.uid)}
>
{t("routeUndoOptimize")}

<span className="feather feather-corner-left-up ms-3" />
</button>
)
) : null}

{selectedTable !== "unapproved" ? (
<UserDataExport
chainName={chain.name}
Expand Down
31 changes: 31 additions & 0 deletions server/internal/controllers/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/the-clothing-loop/website/server/internal/app/auth"
"github.com/the-clothing-loop/website/server/internal/models"
"github.com/the-clothing-loop/website/server/pkg/tsp"
)

func RouteOrderGet(c *gin.Context) {
Expand Down Expand Up @@ -56,3 +57,33 @@ func RouteOrderSet(c *gin.Context) {
return
}
}

func RouteOptimize(c *gin.Context) {
db := getDB(c)

var query struct {
ChainUID string `form:"chain_uid" binding:"required,uuid"`
}
if err := c.ShouldBindQuery(&query); err != nil {
c.String(http.StatusBadRequest, err.Error())
return
}

// the authenticated user should be a chain admin
ok, _, chain := auth.Authenticate(c, db, auth.AuthState3AdminChainUser, query.ChainUID)
if !ok {
return
}

minimalCost, optimalPath := tsp.OptimizeRoute(chain.ID, db)

result := struct {
MinimalCost float64 `json:"minimal_cost"`
OptimalPath []string `json:"optimal_path"`
}{
MinimalCost: minimalCost,
OptimalPath: optimalPath,
}

c.JSON(200, &result)
}
1 change: 1 addition & 0 deletions server/internal/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ func Routes() *gin.Engine {
// route
v2.GET("/route/order", controllers.RouteOrderGet)
v2.POST("/route/order", controllers.RouteOrderSet)
v2.GET("/route/optimize", controllers.RouteOptimize)

// contact
v2.POST("/contact/newsletter", controllers.ContactNewsletter)
Expand Down
76 changes: 76 additions & 0 deletions server/pkg/tsp/mst.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package tsp

// https://github.com/medchik/Travelling-Salesman-Problem-Approximate-Solution-using-MST

const INT_MAX = float64(1e9)

type MST struct {
}

func (MST) optimizeRoute(matrix [][]float64) (float64, []int) {
var n = len(matrix)

var rootNode int
var reached []int
var unreached []int

var tree = make([][]int, n)
for i := 0; i < n; i++ {
tree[i] = []int{}
}

unreached = make([]int, n)
for i := 0; i < n; i++ {
unreached[i] = i
}

var path = []int{}
var cost = float64(0)

prim := func() {
var max float64
var record float64
var parent int
var newNode int
var indexNewNode int
rootNode = unreached[0]
reached = append(reached, unreached[0])
unreached = append(unreached[:0], unreached[1:]...)

for len(unreached) > 0 {
max = INT_MAX
for i := 0; i < len(reached); i++ {
for j := 0; j < len(unreached); j++ {
record = matrix[reached[i]][unreached[j]]
if record < max {
max = record
indexNewNode = j
parent = reached[i]
newNode = unreached[j]
}
}
}
reached = append(reached, unreached[indexNewNode])
unreached = append(unreached[:indexNewNode], unreached[indexNewNode+1:]...)
tree[parent] = append(tree[parent], newNode)
}
}

var preorder func(index int)
preorder = func(index int) {
path = append(path, index)
for _, i := range tree[index] {
preorder(i)
}
}

prim()
preorder(rootNode)
path = append(path, rootNode) // to create the cycle
for i := 0; i < len(path)-1; i++ {
src := path[i]
dest := path[i+1]
cost = cost + matrix[src][dest]
}
return cost, path
}
113 changes: 113 additions & 0 deletions server/pkg/tsp/tsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package tsp

import (
"math"
"time"

"github.com/the-clothing-loop/website/server/internal/app/goscope"
"gorm.io/gorm"
)

type TSPAlgorithm interface {
optimizeRoute(matrix [][]float64) (float64, []int)
}

type UserChain struct {
UserID uint
UserUID string
UserLatitude float64
UserLongitude float64
IsChainAdmin bool
CreatedAt time.Time
}

var optimizer TSPAlgorithm = MST{}

/*
*
Given a ChainUID return an optimized route for all the approved participant of the loop
with latitude and longitude.
*
*/
func OptimizeRoute(ChainID uint, db *gorm.DB) (float64, []string) {
users := retrieveChainUsers(ChainID, db)
distanceMatrix := createDistanceMatrix(users)
minimalCost, optimalPath := optimizer.optimizeRoute(distanceMatrix)
// obtaining the uid of the users based in his index in the optimalPath
orderedUsersId := sortUsersByOptimalPath(users, optimalPath)
return minimalCost, orderedUsersId
}

func retrieveChainUsers(ChainID uint, db *gorm.DB) []UserChain {

allUserChains := &[]UserChain{}

err := db.Raw(`
SELECT
users.id AS user_id,
users.uid AS user_uid,
users.latitude AS user_latitude,
users.longitude AS user_longitude,
user_chains.is_chain_admin AS is_chain_admin,
user_chains.created_at AS created_at
FROM user_chains
LEFT JOIN users ON user_chains.user_id = users.id
WHERE user_chains.chain_id = ?
AND users.is_email_verified = TRUE
AND user_chains.is_approved = TRUE
AND users.latitude <> 0 AND users.longitude <> 0 `, ChainID).Scan(allUserChains).Error

if err != nil {
goscope.Log.Errorf("Unable to retrieve associations between a loop and its users: %v", err)
return nil
}
return *allUserChains
}

func createDistanceMatrix(users []UserChain) [][]float64 {
n := len(users)
matrix := make([][]float64, n)

for i := 0; i < n; i++ {
matrix[i] = make([]float64, n)
}

for i := 0; i < n; i++ {
for j := i; j < n; j++ {
distance := calculateDistance(users[i], users[j])
matrix[i][j] = distance
matrix[j][i] = distance
}
}
return matrix
}

func calculateDistance(user1, user2 UserChain) float64 {
lat1 := user1.UserLatitude
lon1 := user1.UserLongitude
lat2 := user2.UserLatitude
lon2 := user2.UserLongitude

// Calculate distance using Haversine formula
dLat := toRadians(lat2 - lat1)
dLon := toRadians(lon2 - lon1)
a := math.Sin(dLat/2)*math.Sin(dLat/2) + math.Cos(toRadians(lat1))*math.Cos(toRadians(lat2))*math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
distance := 6371 * c // Earth's radius in kilometers

return distance
}

func toRadians(degrees float64) float64 {
return degrees * math.Pi / 180
}

func sortUsersByOptimalPath(users []UserChain, optimalPath []int) []string {
orderedUsersId := make([]string, len(users))

for i := 0; i < len(users); i++ {
userIndex := optimalPath[i]
orderedUsersId[i] = users[userIndex].UserUID
}
return orderedUsersId
}

0 comments on commit 3fba26d

Please sign in to comment.