This commit is contained in:
Morten Olsen
2022-05-02 14:26:11 +02:00
commit d83a4aebc7
77 changed files with 16638 additions and 0 deletions

View File

@@ -0,0 +1,144 @@
import { Context, GraphNode } from "#/types/graph";
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
import { getNext } from "./get-next";
enum Strategies {
all = 'all',
allValid = 'all-valid',
firstValid = 'first-valid',
firstComplet = 'first-complete',
}
type RunningStatus = {
current: 'running';
nodes: number;
start: Date;
cancel: () => void;
}
type CompletedStatus = {
current: 'completed';
start: Date;
end: Date;
nodes: number;
}
type Status = RunningStatus | CompletedStatus;
type BuildGraphOptions = {
location: UserLocation;
time: Date;
tasks: Task[];
context: Context;
strategy?: Strategies;
batchSize?: number;
sleepTime?: number;
callback?: (status: Status) => void;
};
const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time));
const fil = <T>(
fn: ((item: T) => boolean)[],
input: T[],
): T[][] => {
const output: T[][] = new Array(fn.length).fill(undefined).map(() => []);
for (let i = 0; i < input.length; i++) {
for (let b = 0; b < fn.length; b++) {
if (fn[b](input[i])) {
output[b].push(input[i]);
continue;
}
}
}
return output;
};
const buildGraph = async ({
location,
time,
tasks,
context,
strategy = Strategies.allValid,
callback,
batchSize = 1000,
sleepTime = 10,
}: BuildGraphOptions) => {
const start = new Date();
let leafs: GraphNode[] = [{
location,
time: {
end: time,
start: time,
},
score: 0,
remainingTasks: tasks,
impossibeTasks: [],
status: {
dead: false,
completed: false,
},
}];
let nodes = 0;
let running = true;
const final: GraphNode[] = [];
while (true) {
nodes++;
if (!running) {
return [];
}
const node = leafs.pop();
if (!node) {
break;
}
if (nodes % batchSize === 1) {
if (callback) {
callback({
current: 'running',
nodes,
start,
cancel: () => {
running = false;
}
})
}
await sleep(sleepTime);
}
const next = await getNext(node, context);
const [alive, completed] = fil([
n => !n.status.dead && !n.status.completed,
n => !!n.status.completed && !n.status.dead
], next);
leafs.push(...alive);
if (strategy === Strategies.firstValid && completed.length > 0) {
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return completed;
}
if (completed.length > 0) {
final.push(...completed)
}
if (strategy === Strategies.firstComplet) {
const fullComplete = completed.find(c => c.impossibeTasks.length === 0);
if (fullComplete) {
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return [fullComplete];
}
}
}
console.log('nodes', nodes);
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return final
.filter(n => n.status.completed)
.sort((a, b) => b.score - a.score);
}
export type { Status, BuildGraphOptions };
export { buildGraph, Strategies };

View File

@@ -0,0 +1,40 @@
import { GraphNode } from "#/types/graph";
import { PlanItem } from "#/types/plans";
const constructDay = (node: GraphNode) => {
let current: GraphNode | undefined = node;
const plans: PlanItem[] = [];
while(current) {
if (current.task) {
plans.push({
type: 'task',
name: current.task?.name || 'start',
external: current.task?.external,
start: new Date(
current.time.start.getTime()
+ (current.transition?.time || 0),
),
end: current.time.end,
score: current.score,
})
}
if (current.transition) {
plans.push({
type: 'transition',
start: current.time.start,
end: new Date(
current.time.start.getTime()
+ current.transition.time,
),
from: current.transition.from,
to: current.transition.to,
})
}
current = current.parent;
}
return plans.reverse();
}
export { constructDay };

View File

@@ -0,0 +1,146 @@
import { GraphNode, Context } from '#/types/graph';
import { Transition } from '#/types/location';
import { Task } from '#/types/task';
import { getRemainingLocations, listContainLocation } from './utils';
const isDead = (impossible: Task[]) => {
const missingRequered = impossible.find(t => t.required);
return !!missingRequered;
}
type GetImpossibleResult = {
remaining: Task[];
impossible: Task[];
}
const getImpossible = (
tasks: Task[],
time: Date,
) => {
const result: GetImpossibleResult = {
remaining: [],
impossible: [],
}
for (let task of tasks) {
if (time > task.start.max) {
result.impossible.push(task);
} else {
result.remaining.push(task);
}
};
return result;
}
type CalculateScoreOptions = {
tasks?: Task[];
transition?: Transition;
impossible: Task[];
}
const calculateScore = ({
tasks,
transition,
impossible,
}: CalculateScoreOptions) => {
let score = 0;
tasks?.forEach((task) => {
score += task.priority * 10;
impossible.forEach((task) => {
if (task.required) {
score -= 1000;
} else {
score -= task.priority;
}
});
});
if (transition) {
const minutes = transition.time / 1000 / 60
score -= minutes;
}
return score;
}
const getNext = async (
currentNode: GraphNode,
context: Context,
): Promise<GraphNode[]> => {
const nextNodes: GraphNode[] = [];
if (!currentNode.transition) {
const remainingLocations = getRemainingLocations(currentNode.remainingTasks, currentNode.location);
await Promise.all(remainingLocations.map(async(location) => {
const transition = await context.getTransition(currentNode.location, location, currentNode.time.end);
const endTime = new Date(currentNode.time.end.getTime() + transition.time);
const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime);
const score = calculateScore({
transition,
impossible,
});
nextNodes.push({
parent: currentNode,
location: transition.to,
remainingTasks: remaining,
transition,
impossibeTasks: [
...impossible,
...currentNode.impossibeTasks,
],
score: currentNode.score + score,
status: {
completed: false,
dead: isDead(impossible),
},
time: {
start: currentNode.time.end,
end: endTime,
},
})
}));
}
const possibleTasks = currentNode.remainingTasks.filter(task => !task.locations || listContainLocation(task.locations, currentNode.location))
await Promise.all(possibleTasks.map(async (orgTask) => {
const task = {...orgTask};
task.count = (task.count || 1) - 1
let startTime = new Date(
Math.max(
currentNode.time.end.getTime(),
task.start.min.getTime(),
),
);
const parentRemainging = currentNode.remainingTasks.filter(t => t !== orgTask);
let endTime = new Date(startTime.getTime() + task.duration.min);
const { remaining, impossible } = getImpossible(
task.count > 0
? [...parentRemainging, task]
: parentRemainging,
endTime,
);
const score = calculateScore({
tasks: [task],
impossible,
});
nextNodes.push({
parent: currentNode,
location: currentNode.location,
task,
remainingTasks: remaining,
impossibeTasks: [
...impossible,
...currentNode.impossibeTasks,
],
score: currentNode.score + score,
status: {
completed: remaining.length === 0,
dead: isDead(impossible),
},
time: {
start: startTime,
end: endTime,
},
})
}));
return nextNodes;
};
export { getNext };

View File

@@ -0,0 +1,38 @@
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
export const locationEqual = (a: UserLocation, b: UserLocation) => {
if (a === b) {
return true;
}
if (a.location === b.location) {
return true;
}
if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) {
return true;
}
if (a.title === b.title) {
return true;
}
return false;
}
export const listContainLocation = (list: UserLocation[], target: UserLocation) => {
return !!list.find(l => locationEqual(l, target));
}
export const getRemainingLocations = (tasks: Task[], current: UserLocation) => {
const result: UserLocation[] = [];
tasks.forEach((task) => {
if (!task.locations) {
return;
}
for (let location of task.locations) {
if (!listContainLocation(result, location) && !locationEqual(current, location)) {
result.push(location)
}
}
})
return result;
};

View File

@@ -0,0 +1,65 @@
import { useGetTransition } from "#/features/location";
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
import { constructDay } from "./algorithm/construct-day";
import { useAsyncCallback } from "#/hooks/async";
import { UserLocation } from "#/types/location";
import { useDate } from "../calendar";
import { useTasksWithContext } from "../agenda-context";
import { useMemo, useState } from "react";
import { PlanItem } from "#/types/plans";
import { Task } from "#/types/task";
export type UsePlanOptions = {
location: UserLocation;
}
export type UsePlan = [
(start?: Date) => Promise<any>,
{
result?: { agenda: PlanItem[], impossible: Task[] };
status?: Status;
loading: boolean;
error?: any;
}
]
export const usePlan = ({
location,
}: UsePlanOptions): UsePlan => {
const today = useDate();
const [status, setStatus] = useState<Status>();
const all = useTasksWithContext();
const enabled = useMemo(() => all.filter(f => f.enabled), [all])
const getTransition = useGetTransition();
const [invoke, options] = useAsyncCallback(
async (start?: Date) => {
const graph = await buildGraph({
location,
time: start || today,
tasks: enabled,
strategy: Strategies.firstComplet,
context: {
getTransition,
},
callback: setStatus,
});
const valid = graph.filter(a => !a.status.dead && a.status.completed).sort((a, b) => b.score - a.score);
const day = constructDay(valid[0]);
return {
impossible: valid[0].impossibeTasks,
agenda: day,
};
},
[today, location, all, setStatus],
);
return [
invoke,
{
result: options.result,
loading: options.loading,
error: options.error,
status: status,
}
];
}

View File

@@ -0,0 +1 @@
export * from './hooks';