This commit is contained in:
Morten Olsen
2022-05-10 22:45:38 +02:00
committed by Morten Olsen
parent 8259232a83
commit 6181eeb0c8
44 changed files with 1099 additions and 220 deletions

View File

@@ -1,6 +1,5 @@
import { Context, GraphNode } from "#/types/graph";
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
import { Task, Time, UserLocation } from "#/features/data";
import { Context, GraphNode } from "../types";
import { getImpossible, getNext } from "./get-next";
enum Strategies {
@@ -29,7 +28,7 @@ type Status = RunningStatus | CompletedStatus;
type BuildGraphOptions = {
location: UserLocation;
time: Date;
time: Time;
tasks: Task[];
context: Context;
strategy?: Strategies;
@@ -49,7 +48,7 @@ const fil = <T>(
for (let b = 0; b < fn.length; b++) {
if (fn[b](input[i])) {
output[b].push(input[i]);
continue;
break;
}
}
}
@@ -150,7 +149,9 @@ const buildGraph = async ({
return complete([fullComplete]);
}
}
deadList.push(...dead);
if (strategy !== Strategies.all) {
deadList.push(...dead);
}
}
return complete(completedList);

View File

@@ -1,20 +1,16 @@
import { GraphNode } from "#/types/graph";
import { PlanItem } from "#/types/plans";
import { timeUtils } from "#/features/data";
import { GraphNode, PlannedEntry } from "../types";
const constructDay = (node: GraphNode) => {
let current: GraphNode | undefined = node;
const plans: PlanItem[] = [];
const plans: PlannedEntry[] = [];
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),
),
name: current.task?.title || 'start',
start: timeUtils.add(current.time.start, (current.transition?.time || 0)),
end: current.time.end,
score: current.score,
})
@@ -23,10 +19,7 @@ const constructDay = (node: GraphNode) => {
plans.push({
type: 'transition',
start: current.time.start,
end: new Date(
current.time.start.getTime()
+ current.transition.time,
),
end: timeUtils.add(current.time.start, current.transition.time),
from: current.transition.from,
to: current.transition.to,
})

View File

@@ -1,8 +1,10 @@
import { GraphNode, Context } from '#/types/graph';
import { Transition } from '#/types/location';
import { Task } from '#/types/task';
import { Task, Time, timeUtils } from '#/features/data';
import { Transition } from '#/features/location';
import { Context, GraphNode } from '../types';
import { getRemainingLocations, listContainLocation } from './utils';
const DEFAULT_PRIORITY = 50;
const isDead = (impossible: Task[]) => {
const missingRequered = impossible.find(t => t.required);
return !!missingRequered;
@@ -15,7 +17,7 @@ type GetImpossibleResult = {
export const getImpossible = (
tasks: Task[],
time: Date,
time: Time,
) => {
const result: GetImpossibleResult = {
remaining: [],
@@ -23,7 +25,7 @@ export const getImpossible = (
}
for (let task of tasks) {
if (time > task.start.max) {
if (timeUtils.largerThan(time, task.startTime.max)) {
result.impossible.push(task);
} else {
result.remaining.push(task);
@@ -47,17 +49,17 @@ const calculateScore = ({
let score = 0;
tasks?.forEach((task) => {
score += task.priority * 10;
score += (task.priority || DEFAULT_PRIORITY) * 10;
impossible.forEach((task) => {
if (task.required) {
score -= 10000 + (1 * task.priority);
score -= 10000 + (1 * (task.priority || DEFAULT_PRIORITY));
} else {
score -= 100 + (1 * task.priority);
score -= 100 + (1 * (task.priority || DEFAULT_PRIORITY));
}
});
});
if (transition) {
const minutes = transition.time / 1000 / 60
const minutes = transition.time;
score -= 10 + (1 * minutes);
}
return score;
@@ -71,7 +73,7 @@ const getNext = async (
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 endTime = timeUtils.add(currentNode.time.end, transition.time);
const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime);
const score = calculateScore({
transition,
@@ -89,7 +91,7 @@ const getNext = async (
score: currentNode.score + score,
status: {
completed: false,
dead: isDead(impossible),
dead: false, // TODO: fix isDead(impossible),
},
time: {
start: currentNode.time.end,
@@ -101,21 +103,14 @@ const getNext = async (
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(),
),
);
let startTime =
timeUtils.max(
currentNode.time.end,
task.startTime.min,
);
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,
);
let endTime = timeUtils.add(startTime, task.duration);
const { remaining, impossible } = getImpossible(parentRemainging, endTime);
const score = calculateScore({
tasks: [task],
impossible,

View File

@@ -5,12 +5,12 @@ 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.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;
}

View File

@@ -1,8 +1,10 @@
import { createDataContext } from '#/utils/data-context';
import { Time } from '../data';
import { Strategies } from "./algorithm/build-graph";
type PlannerOptions = {
strategy: Strategies;
startTime: Time;
}
const {
@@ -10,6 +12,7 @@ const {
Provider: PlannerProvider,
} = createDataContext<PlannerOptions>({
createDefault: () => ({
startTime: { hour: 7, minute: 0 },
strategy: Strategies.firstComplet,
}),
});

View File

@@ -1,22 +1,129 @@
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
import { useContext } from "react";
import { PlanItem } from "#/types/plans";
import { add } from 'date-fns';
import { PlannerContext } from "./context";
import { Task, UserLocation } from "../data";
import { Task, Time, UserLocation } from "../data";
import { useRoutines } from "../routines";
import { useGoals } from "../goals/hooks";
import { useAsyncCallback } from "../async";
import { Day, dayUtils } from "../day";
import { useGetOverride } from "../overrides";
import { useGetAppointments } from "../appointments";
import { useGetTransition } from "../location";
import { PlannedEntry } from "./types";
import { constructDay } from "./algorithm/construct-day";
export type UsePlanOptions = {
export type PreparePlanOptions = {
start: Day;
end: Day;
}
export type PlanOptions = PreparePlanOptions & {
location: UserLocation;
}
export type UsePlan = [
(start?: Date) => Promise<any>,
{
result?: { agenda: PlanItem[], impossible: Task[] };
status?: Status;
loading: boolean;
error?: any;
export type PlanResultDay = {
day: Day;
start: Time;
} & ({
status: 'waiting',
} | {
status: 'running',
nodes: number;
strategy: Strategies;
} | {
status: 'done';
nodes: number;
strategy: Strategies;
plan: PlannedEntry[];
impossible: Task[];
});
export type PlanResult = {
impossible: Task[];
days: {
[day: string]: PlanResultDay;
}
]
}
const getDays = (start: Day, end: Day): Day[] => {
const result: Day[] = [];
let currentDate = dayUtils.dayToDate(start);
const stopDate = dayUtils.dayToDate(end);
while (currentDate <= stopDate) {
result.push(dayUtils.dateToDay(currentDate));
currentDate = add(currentDate, {
days: 1,
});
}
return result;
}
const firstValue = <T>(...args: (T | undefined)[]): T => {
for (let arg of args) {
if (typeof arg !== 'undefined') {
return arg;
}
}
return undefined as unknown as T;
}
export const useOptions = () => {
const { data } = useContext(PlannerContext);
return data;
}
export const useSetOptions = () => {
const { setData } = useContext(PlannerContext);
return setData;
}
const usePreparePlan = () => {
const routines = useRoutines();
const goals = useGoals();
const getOverrides = useGetOverride();
const [getAppontments] = useGetAppointments();
const preparePlan = useAsyncCallback(
async ({ start, end }: PreparePlanOptions) => {
const days = await Promise.all(getDays(start, end).map(async (day) => {
const overrides = await getOverrides(day);
const start: Time = firstValue(overrides.startTime, { hour: 7, minute: 0 });
const appointments = await getAppontments(day);
const tasks = [...routines, ...appointments].map<Task | undefined>((task) => {
const override = overrides.tasks[task.id];
if (override?.enabled === false) {
return undefined;
}
const result: Task = {
...task,
startTime: {
min: firstValue(override?.startMin, task.startTime.min),
max: firstValue(override?.startMax, task.startTime.max),
},
duration: firstValue(override?.duration, task.duration),
required: firstValue(override?.required, task.required),
}
return result;
}).filter(Boolean).map(a => a as Exclude<typeof a, undefined>);
return {
day,
start,
tasks,
}
}));
return {
goals: [...goals],
days,
}
},
[routines, goals, getOverrides, getAppontments],
);
return preparePlan;
}
export const usePlanOptions = () => {
const { data } = useContext(PlannerContext);
@@ -29,6 +136,87 @@ export const useSetPlanOptions = () => {
}
export const usePlan = () => {
}
const [preparePlan] = usePreparePlan();
const getTransition = useGetTransition();
const options = usePlanOptions();
const createPlan = useAsyncCallback(
async ({ location, ...prepareOptions}: PlanOptions) => {
const prepared = await preparePlan(prepareOptions);
let result: PlanResult = {
impossible: [],
days: prepared.days.reduce((output, current) => ({
...output,
[dayUtils.toId(current.day)]: {
day: current.day,
start: current.start,
status: 'waiting',
},
}), {} as {[name: string]: PlanResultDay})
}
const update = (next: PlanResult) => {
result = next;
}
for (let day of prepared.days) {
const id = dayUtils.toId(day.day);
const dayGoal = prepared.goals;
const graph = await buildGraph({
location,
time: day.start,
tasks: [...day.tasks, ...dayGoal],
strategy: options.strategy,
context: {
getTransition,
},
callback: (status) => {
update({
...result,
days: {
...result.days,
[id]: {
day: day.day,
start: day.start,
status: 'running',
nodes: status.nodes,
strategy: status.strategy,
}
}
});
}
});
const [winner] = graph;
if (!winner) {
continue;
}
const plan = constructDay(winner);
update({
...result,
days: {
...result.days,
[id]: {
...result.days[id],
impossible: winner.impossibeTasks,
status: 'done',
plan,
}
}
})
prepared.goals = prepared.goals.filter((goal) => {
if (!dayGoal.find(d => d.id === goal.id)) {
return true;
}
if (!winner.impossibeTasks.find(d => d.id === goal.id)) {
return false;
}
return true;
})
}
return {
...result,
impossible: prepared.goals,
};
},
[preparePlan, getTransition, options],
);
return createPlan;
}

View File

@@ -1,3 +1,5 @@
import { Task, Time, UserLocation } from "../data";
import { GetTransition, Transition } from "../location";
type Context = {
getTransition: GetTransition;
@@ -6,20 +8,22 @@ type Context = {
export type PlannedTask = {
type: 'task';
name: string;
start: Date;
start: Time;
external?: boolean;
end: Date;
end: Time;
score: number;
}
export type PlannedTransition = {
type: 'transition';
start: Date;
end: Date;
start: Time;
end: Time;
from: UserLocation;
to: UserLocation;
};
export type PlannedEntry = PlannedTask | PlannedTransition;
type GraphNode = {
location: UserLocation;
task?: Task;
@@ -29,8 +33,8 @@ type GraphNode = {
impossibeTasks: Task[];
score: number;
time: {
start: Date;
end: Date;
start: Time;
end: Time;
};
status: {
dead: boolean;