mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
21
src/features/agenda-context/context.ts
Normal file
21
src/features/agenda-context/context.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { UserLocation } from "#/types/location"
|
||||
import { createContext } from "react"
|
||||
|
||||
type AgendaContext = {
|
||||
enabled: boolean;
|
||||
locations?: UserLocation[];
|
||||
startMax?: Date;
|
||||
startMin?: Date;
|
||||
duration?: number;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
type AgendaContextContextValue = {
|
||||
contexts: {[id: string]: AgendaContext};
|
||||
set: (id: string, context: AgendaContext) => Promise<void>;
|
||||
}
|
||||
|
||||
const AgendaContextContext = createContext<AgendaContextContextValue>(undefined as any);
|
||||
|
||||
export type { AgendaContext, AgendaContextContextValue };
|
||||
export {AgendaContextContext };
|
||||
56
src/features/agenda-context/hooks.ts
Normal file
56
src/features/agenda-context/hooks.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useAsyncCallback } from "#/hooks/async";
|
||||
import { Task } from "#/types/task";
|
||||
import { set } from "date-fns";
|
||||
import { useContext, useMemo } from "react"
|
||||
import { useDate } from "../calendar";
|
||||
import { useTasks } from "../tasks";
|
||||
import { AgendaContextContext } from "./context"
|
||||
|
||||
const toToday = (today: Date, target: Date) => set(target, {
|
||||
year: today.getFullYear(),
|
||||
month: today.getMonth(),
|
||||
date: today.getDate(),
|
||||
})
|
||||
|
||||
export const useAgendaContext = () => {
|
||||
const { contexts } = useContext(AgendaContextContext);
|
||||
return contexts;
|
||||
}
|
||||
|
||||
export const useSetAgendaContext = () => {
|
||||
const { set } = useContext(AgendaContextContext);
|
||||
const result = useAsyncCallback(set, [set]);
|
||||
return result;
|
||||
}
|
||||
|
||||
export const useTasksWithContext = () => {
|
||||
const { all } = useTasks();
|
||||
const date = useDate();
|
||||
const contexts = useAgendaContext();
|
||||
|
||||
const withContext = useMemo<(Task & { enabled: boolean })[]>(
|
||||
() => all.map((task) => {
|
||||
const context = contexts[task.id];
|
||||
if (!context) {
|
||||
return { ...task, enabled: true };
|
||||
}
|
||||
return {
|
||||
...task,
|
||||
locations: context.locations?.length || 0 > 0 ? context.locations : task.locations,
|
||||
start: {
|
||||
min: context.startMin ? toToday(date, context.startMin) : task.start.min,
|
||||
max: context.startMax ? toToday(date, context.startMax) : task.start.max,
|
||||
},
|
||||
duration: {
|
||||
...task.duration,
|
||||
min: context.duration || task.duration.min,
|
||||
},
|
||||
count: context.count,
|
||||
enabled: typeof context.enabled === 'undefined' ? true : context.enabled,
|
||||
}
|
||||
}),
|
||||
[all, contexts],
|
||||
);
|
||||
|
||||
return withContext;
|
||||
}
|
||||
2
src/features/agenda-context/index.ts
Normal file
2
src/features/agenda-context/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { AgendaContextProvider } from './provider';
|
||||
export * from './hooks';
|
||||
64
src/features/agenda-context/provider.tsx
Normal file
64
src/features/agenda-context/provider.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useAsync } from "#/hooks/async";
|
||||
import AsyncStorageLib from "@react-native-async-storage/async-storage";
|
||||
import { format } from "date-fns";
|
||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { AgendaContext, AgendaContextContext, AgendaContextContextValue } from "./context";
|
||||
|
||||
type AgendaContextProviderProps = {
|
||||
children: ReactNode;
|
||||
day: Date;
|
||||
}
|
||||
|
||||
const AGENDA_CONTEXT_STORAGE_KEY = 'agenda-contexts';
|
||||
|
||||
const AgendaContextProvider: React.FC<AgendaContextProviderProps> = ({
|
||||
children,
|
||||
day,
|
||||
}) => {
|
||||
const [contexts, setContexts] = useState<AgendaContextContextValue['contexts']>({});
|
||||
const key = useMemo(
|
||||
() => `${AGENDA_CONTEXT_STORAGE_KEY}-${format(day, 'yyyy-MM-dd')}`,
|
||||
[day],
|
||||
);
|
||||
|
||||
const set = useCallback(
|
||||
async (id: string, context: AgendaContext) => {
|
||||
const index = {
|
||||
...contexts,
|
||||
[id]: {...context},
|
||||
};
|
||||
setContexts(index);
|
||||
await AsyncStorageLib.setItem(key, JSON.stringify(contexts));
|
||||
},
|
||||
[setContexts, contexts, key],
|
||||
);
|
||||
|
||||
useAsync(
|
||||
async () => {
|
||||
const raw = await AsyncStorageLib.getItem(key);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const items = JSON.parse(raw) as AgendaContextContextValue['contexts'];
|
||||
Object.values(items).forEach((item) => {
|
||||
if (item.startMax) {
|
||||
item.startMax = new Date(item.startMax);
|
||||
}
|
||||
if (item.startMin) {
|
||||
item.startMin = new Date(item.startMin);
|
||||
}
|
||||
})
|
||||
setContexts(items);
|
||||
},
|
||||
[key],
|
||||
)
|
||||
|
||||
return (
|
||||
<AgendaContextContext.Provider value={{ contexts, set }}>
|
||||
{children}
|
||||
</AgendaContextContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export type { AgendaContextProviderProps };
|
||||
export { AgendaContextProvider };
|
||||
17
src/features/calendar/context.ts
Normal file
17
src/features/calendar/context.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Calendar } from "expo-calendar";
|
||||
import { createContext } from "react";
|
||||
|
||||
type CalendarContextValue = {
|
||||
date: Date;
|
||||
setDate: (date: Date) => void;
|
||||
calendars: Calendar[];
|
||||
calendar: Calendar;
|
||||
selected: Calendar[];
|
||||
setSelected: (calendars: Calendar[]) => void;
|
||||
error?: any;
|
||||
}
|
||||
|
||||
const CalendarContext = createContext<CalendarContextValue>(undefined as any);
|
||||
|
||||
export type { CalendarContextValue };
|
||||
export { CalendarContext };
|
||||
95
src/features/calendar/hooks.ts
Normal file
95
src/features/calendar/hooks.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useContext } from "react"
|
||||
import { CalendarContext } from "./context"
|
||||
import { set } from 'date-fns'
|
||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
||||
import { createEventAsync, deleteEventAsync, getEventsAsync } from "expo-calendar";
|
||||
import { PlanItem } from "#/types/plans";
|
||||
|
||||
export const useCalendar = () => {
|
||||
const { calendar } = useContext(CalendarContext);
|
||||
return calendar;
|
||||
}
|
||||
|
||||
export const useCalendars = () => {
|
||||
const { calendars } = useContext(CalendarContext);
|
||||
return calendars;
|
||||
}
|
||||
|
||||
export const useSelectedCalendars = () => {
|
||||
const { selected } = useContext(CalendarContext);
|
||||
return selected;
|
||||
}
|
||||
|
||||
export const useSetSelectedCalendars = () => {
|
||||
const { setSelected } = useContext(CalendarContext);
|
||||
return setSelected;
|
||||
}
|
||||
|
||||
export const useDate = () => {
|
||||
const { date } = useContext(CalendarContext);
|
||||
return date;
|
||||
}
|
||||
|
||||
export const useSetDate = () => {
|
||||
const { setDate } = useContext(CalendarContext);
|
||||
return setDate;
|
||||
}
|
||||
|
||||
export const useCommit = () => {
|
||||
const date = useDate();
|
||||
const calendar = useCalendar();
|
||||
const result = useAsyncCallback(
|
||||
async (plan: PlanItem[]) => {
|
||||
const end = set(date, {
|
||||
hours: 24,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
milliseconds: 0,
|
||||
});
|
||||
const current = await getEventsAsync([calendar.id], date, end);
|
||||
await Promise.all(
|
||||
current.map(async (item) => {
|
||||
await deleteEventAsync(item.id)
|
||||
}),
|
||||
);
|
||||
for (let item of plan) {
|
||||
if (item.type === 'task' && item.external) {
|
||||
continue;
|
||||
}
|
||||
const title = item.type === 'task' ? item.name : `${item.from.title} to ${item.to.title}`;
|
||||
await createEventAsync(calendar.id, {
|
||||
title: title,
|
||||
startDate: item.start,
|
||||
endDate: item.end,
|
||||
})
|
||||
}
|
||||
},
|
||||
[date],
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export const useToday = (start: Date, end?: Date) => {
|
||||
const selectedCalendars = useSelectedCalendars();
|
||||
if (!end) {
|
||||
end = set(start, {
|
||||
hours: 24,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
milliseconds: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const result = useAsync(
|
||||
async () => {
|
||||
if (selectedCalendars.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return getEventsAsync(selectedCalendars.map(c => c.id), start, end!)
|
||||
},
|
||||
[selectedCalendars, start.getTime()],
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
2
src/features/calendar/index.ts
Normal file
2
src/features/calendar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { CalendarProvider } from './provider';
|
||||
export * from './hooks';
|
||||
105
src/features/calendar/provider.tsx
Normal file
105
src/features/calendar/provider.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Calendar, CalendarAccessLevel, createCalendarAsync, EntityTypes, getCalendarsAsync, getDefaultCalendarAsync, requestCalendarPermissionsAsync } from "expo-calendar";
|
||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||
import { useAsync } from "#/hooks/async";
|
||||
import { CalendarContext } from "./context";
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
const SELECTED_STORAGE_KEY = 'selected_calendars';
|
||||
|
||||
type CalendarProviderProps = {
|
||||
calendarName?: string,
|
||||
date: Date;
|
||||
children: ReactNode;
|
||||
setDate: (date: Date) => void;
|
||||
}
|
||||
|
||||
type SetupResponse = {
|
||||
status: 'rejected';
|
||||
} | {
|
||||
status: 'ready';
|
||||
calendar: Calendar;
|
||||
calendars: Calendar[];
|
||||
};
|
||||
|
||||
const CalendarProvider: React.FC<CalendarProviderProps> = ({
|
||||
date,
|
||||
children,
|
||||
setDate,
|
||||
calendarName = 'Bob the planner',
|
||||
}) => {
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [value] = useAsync<SetupResponse>(
|
||||
async () => {
|
||||
const { status } = await requestCalendarPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
return { status: 'rejected' };
|
||||
}
|
||||
let calendars = await getCalendarsAsync(EntityTypes.EVENT);
|
||||
let calendar = calendars.find(c => c.title === calendarName);
|
||||
if (!calendar) {
|
||||
const defaultCalendar = await getDefaultCalendarAsync();
|
||||
await createCalendarAsync({
|
||||
title: calendarName,
|
||||
source: defaultCalendar.source,
|
||||
sourceId: defaultCalendar.source.id,
|
||||
ownerAccount: 'personal',
|
||||
accessLevel: CalendarAccessLevel.OWNER,
|
||||
entityType: EntityTypes.EVENT,
|
||||
name: calendarName,
|
||||
});
|
||||
calendars = await getCalendarsAsync(EntityTypes.EVENT);
|
||||
calendar = calendars.find(c => c.name === calendarName)!;
|
||||
}
|
||||
const selectedRaw = await AsyncStorage.getItem(SELECTED_STORAGE_KEY)
|
||||
if (selectedRaw) {
|
||||
setSelectedIds(JSON.parse(selectedRaw));
|
||||
}
|
||||
return {
|
||||
status: 'ready',
|
||||
calendars,
|
||||
calendar,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const setSelected = useCallback(
|
||||
(calendars: Calendar[]) => {
|
||||
const ids = calendars.map(c => c.id);
|
||||
setSelectedIds(ids);
|
||||
AsyncStorage.setItem(SELECTED_STORAGE_KEY, JSON.stringify(ids));
|
||||
},
|
||||
[setSelectedIds]
|
||||
)
|
||||
const selected = useMemo(
|
||||
() => {
|
||||
if (value?.status !== 'ready') {
|
||||
return [];
|
||||
}
|
||||
return value.calendars.filter(c => selectedIds.includes(c.id));
|
||||
},
|
||||
[value, selectedIds],
|
||||
);
|
||||
|
||||
if (!value || value.status !== 'ready') {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<CalendarContext.Provider
|
||||
value={{
|
||||
setDate,
|
||||
date,
|
||||
selected,
|
||||
setSelected,
|
||||
calendar: value.calendar,
|
||||
calendars: value.calendars,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CalendarContext.Provider>
|
||||
)
|
||||
};
|
||||
|
||||
export type { CalendarProviderProps };
|
||||
export { CalendarProvider };
|
||||
0
src/features/calendar/utils.ts
Normal file
0
src/features/calendar/utils.ts
Normal file
17
src/features/location/context.ts
Normal file
17
src/features/location/context.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GetTransition, UserLocation } from "#/types/location";
|
||||
import { createContext } from "react"
|
||||
|
||||
type LocationContextValue = {
|
||||
locations: {
|
||||
[id: string]: UserLocation;
|
||||
};
|
||||
set: (location: UserLocation) => any;
|
||||
remove: (id: string) => any;
|
||||
lookup?: (address: string) => UserLocation[];
|
||||
getTransition: GetTransition;
|
||||
}
|
||||
|
||||
const LocationContext = createContext<LocationContextValue>(undefined as any);
|
||||
|
||||
export type { LocationContextValue };
|
||||
export { LocationContext };
|
||||
73
src/features/location/hooks.ts
Normal file
73
src/features/location/hooks.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useAsync } from "#/hooks/async";
|
||||
import { useContext } from "react"
|
||||
import { requestForegroundPermissionsAsync, getCurrentPositionAsync } from 'expo-location';
|
||||
import { LocationContext } from "./context"
|
||||
import { UserLocation } from "#/types/location";
|
||||
import { getDistanceFromLatLonInKm } from "./utils";
|
||||
|
||||
export const useLocations = () => {
|
||||
const { locations } = useContext(LocationContext);
|
||||
return locations;
|
||||
}
|
||||
|
||||
export const useSetLocation = () => {
|
||||
const { set } = useContext(LocationContext);
|
||||
return set;
|
||||
}
|
||||
|
||||
export const useRemoveLocation = () => {
|
||||
const { remove } = useContext(LocationContext);
|
||||
return remove;
|
||||
}
|
||||
|
||||
export const useGetTransition = () => {
|
||||
const { getTransition } = useContext(LocationContext);
|
||||
return getTransition;
|
||||
}
|
||||
|
||||
export const useLookup = () => {
|
||||
const { lookup } = useContext(LocationContext);
|
||||
return lookup;
|
||||
}
|
||||
|
||||
export const useCurrentLocation = (proximity: number = 0.5) => {
|
||||
const locations = useLocations();
|
||||
const result = useAsync<UserLocation | undefined>(
|
||||
async () => {
|
||||
let { status } = await requestForegroundPermissionsAsync();
|
||||
if (status !== 'granted') {
|
||||
return undefined;
|
||||
}
|
||||
let position = await getCurrentPositionAsync({});
|
||||
const withDistance = Object.values(locations).map((location) => {
|
||||
if (!location.location) {
|
||||
return;
|
||||
}
|
||||
const distance = getDistanceFromLatLonInKm(
|
||||
position.coords.latitude,
|
||||
position.coords.longitude,
|
||||
location.location.latitude,
|
||||
location.location.longitute,
|
||||
)
|
||||
return {
|
||||
distance,
|
||||
location,
|
||||
}
|
||||
}).filter(Boolean).sort((a, b) => a!.distance - b!.distance)
|
||||
const current = withDistance.find(d => d!.distance < proximity);
|
||||
if (!current) {
|
||||
return {
|
||||
id: `${position.coords.longitude} ${position.coords.latitude}`,
|
||||
title: 'Unknown',
|
||||
location: {
|
||||
latitude: position.coords.latitude,
|
||||
longitute: position.coords.longitude,
|
||||
},
|
||||
};
|
||||
}
|
||||
return current.location;
|
||||
},
|
||||
[],
|
||||
);
|
||||
return result;
|
||||
}
|
||||
2
src/features/location/index.ts
Normal file
2
src/features/location/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { LocationProvider } from './provider';
|
||||
export * from './hooks';
|
||||
72
src/features/location/provider.tsx
Normal file
72
src/features/location/provider.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
||||
import { GetTransition, UserLocation } from "#/types/location";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { LocationContext } from "./context";
|
||||
|
||||
type LocationProviderProps = {
|
||||
children: ReactNode;
|
||||
lookup: (address: string) => UserLocation[];
|
||||
getTransition: GetTransition;
|
||||
}
|
||||
|
||||
const LOCATION_STORAGE_KEY = 'location_storage';
|
||||
|
||||
const LocationProvider: React.FC<LocationProviderProps> = ({
|
||||
children,
|
||||
lookup,
|
||||
getTransition,
|
||||
}) => {
|
||||
const [locations, setLocations] = useState<{[id: string]: UserLocation}>({});
|
||||
|
||||
useAsync(
|
||||
async () => {
|
||||
const raw = await AsyncStorage.getItem(LOCATION_STORAGE_KEY);
|
||||
if (raw) {
|
||||
setLocations(JSON.parse(raw));
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const [set] = useAsyncCallback(
|
||||
async (location: UserLocation) => {
|
||||
const index = {
|
||||
...locations,
|
||||
[location.id]: location,
|
||||
}
|
||||
setLocations(index);
|
||||
await AsyncStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(index));
|
||||
},
|
||||
[setLocations, locations],
|
||||
)
|
||||
|
||||
const [remove] = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
const index = {
|
||||
...locations,
|
||||
}
|
||||
delete index[id];
|
||||
setLocations(index);
|
||||
await AsyncStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(index));
|
||||
},
|
||||
[setLocations, locations],
|
||||
);
|
||||
|
||||
return (
|
||||
<LocationContext.Provider
|
||||
value={{
|
||||
locations,
|
||||
set,
|
||||
remove,
|
||||
lookup,
|
||||
getTransition,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LocationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export type { LocationProviderProps };
|
||||
export { LocationProvider };
|
||||
17
src/features/location/utils.ts
Normal file
17
src/features/location/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
var R = 6371; // Radius of the earth in km
|
||||
var dLat = deg2rad(lat2-lat1); // deg2rad below
|
||||
var dLon = deg2rad(lon2-lon1);
|
||||
var a =
|
||||
Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||||
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
|
||||
Math.sin(dLon/2) * Math.sin(dLon/2)
|
||||
;
|
||||
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||||
var d = R * c; // Distance in km
|
||||
return d;
|
||||
}
|
||||
|
||||
function deg2rad(deg: number) {
|
||||
return deg * (Math.PI/180)
|
||||
}
|
||||
144
src/features/planner/algorithm/build-graph.ts
Normal file
144
src/features/planner/algorithm/build-graph.ts
Normal 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 };
|
||||
40
src/features/planner/algorithm/construct-day.ts
Normal file
40
src/features/planner/algorithm/construct-day.ts
Normal 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 };
|
||||
146
src/features/planner/algorithm/get-next.ts
Normal file
146
src/features/planner/algorithm/get-next.ts
Normal 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 };
|
||||
38
src/features/planner/algorithm/utils.ts
Normal file
38
src/features/planner/algorithm/utils.ts
Normal 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;
|
||||
};
|
||||
|
||||
65
src/features/planner/hooks.ts
Normal file
65
src/features/planner/hooks.ts
Normal 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,
|
||||
}
|
||||
];
|
||||
}
|
||||
1
src/features/planner/index.ts
Normal file
1
src/features/planner/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './hooks';
|
||||
27
src/features/routines/context.ts
Normal file
27
src/features/routines/context.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { UserLocation } from "#/types/location";
|
||||
import { createContext } from "react"
|
||||
|
||||
type Routine = {
|
||||
id: string;
|
||||
title: string;
|
||||
required: boolean;
|
||||
priority: number;
|
||||
start: {
|
||||
min: Date;
|
||||
max: Date;
|
||||
};
|
||||
duration: number;
|
||||
location?: UserLocation[];
|
||||
days?: boolean[];
|
||||
}
|
||||
|
||||
type RoutinesContextValue = {
|
||||
routines: Routine[];
|
||||
remove: (id: string) => any;
|
||||
set: (routine: Routine) => any;
|
||||
}
|
||||
|
||||
const RoutinesContext = createContext<RoutinesContextValue>(undefined as any);
|
||||
|
||||
export type { Routine, RoutinesContextValue };
|
||||
export { RoutinesContext };
|
||||
36
src/features/routines/hooks.ts
Normal file
36
src/features/routines/hooks.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useCallback, useContext, useMemo } from "react"
|
||||
import { Routine, RoutinesContext } from "./context"
|
||||
|
||||
export const useRoutines = (day?: number) => {
|
||||
const { routines } = useContext(RoutinesContext);
|
||||
const current = useMemo(
|
||||
() => routines.filter(
|
||||
r => typeof day === undefined
|
||||
|| !r.days
|
||||
|| r.days[day!],
|
||||
),
|
||||
[routines],
|
||||
);
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
export const useSetRoutine = () => {
|
||||
const { set } = useContext(RoutinesContext);
|
||||
const setRoutine = useCallback(
|
||||
(routine: Routine) => set(routine),
|
||||
[set],
|
||||
);
|
||||
|
||||
return setRoutine;
|
||||
}
|
||||
|
||||
export const useRemoveRoutine = () => {
|
||||
const { remove } = useContext(RoutinesContext);
|
||||
const removeRoutine = useCallback(
|
||||
(id: string) => remove(id),
|
||||
[remove],
|
||||
);
|
||||
|
||||
return removeRoutine;
|
||||
}
|
||||
3
src/features/routines/index.ts
Normal file
3
src/features/routines/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { RoutinesProvider } from './provider';
|
||||
export { Routine } from './context';
|
||||
export * from './hooks';
|
||||
74
src/features/routines/provider.tsx
Normal file
74
src/features/routines/provider.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import React, { ReactNode, useMemo, useState } from "react";
|
||||
import { Routine, RoutinesContext } from "./context";
|
||||
|
||||
type RoutinesProviderProps = {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ROUTINES_STORAGE_KEY = 'routines-items';
|
||||
|
||||
const RoutinesProvider: React.FC<RoutinesProviderProps> = ({ children }) => {
|
||||
const [routineIndex, setRoutineIndex] = useState<{[id: string]: Routine}>({});
|
||||
const routines = useMemo(
|
||||
() => Object.values(routineIndex),
|
||||
[routineIndex]
|
||||
);
|
||||
|
||||
useAsync(
|
||||
async () => {
|
||||
const raw = await AsyncStorage.getItem(ROUTINES_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return;
|
||||
}
|
||||
const result = JSON.parse(raw) as {[name: string]: Routine};
|
||||
Object.values(result).forEach(item => {
|
||||
item.start.max = new Date(item.start.max);
|
||||
item.start.min = new Date(item.start.min);
|
||||
});
|
||||
|
||||
setRoutineIndex(result);
|
||||
},
|
||||
[setRoutineIndex],
|
||||
);
|
||||
|
||||
const [set] = useAsyncCallback(
|
||||
async (routine: Routine) => {
|
||||
const index = {
|
||||
...routineIndex,
|
||||
[routine.id]: routine,
|
||||
};
|
||||
setRoutineIndex(index);
|
||||
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
|
||||
},
|
||||
[setRoutineIndex, routineIndex],
|
||||
);
|
||||
|
||||
const [remove] = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
const index = {
|
||||
...routineIndex,
|
||||
};
|
||||
delete index[id];
|
||||
setRoutineIndex(index);
|
||||
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
|
||||
},
|
||||
[setRoutineIndex, routineIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<RoutinesContext.Provider
|
||||
value={{
|
||||
routines,
|
||||
set,
|
||||
remove,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RoutinesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export type { RoutinesProviderProps };
|
||||
export { RoutinesProvider };
|
||||
32
src/features/setup.tsx
Normal file
32
src/features/setup.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { GetTransition } from "#/types/location"
|
||||
import { ReactNode } from "react"
|
||||
import { AgendaContextProvider } from "./agenda-context"
|
||||
import { CalendarProvider } from "./calendar"
|
||||
import { LocationProvider } from "./location"
|
||||
import { RoutinesProvider } from "./routines"
|
||||
|
||||
type SetupProps = {
|
||||
day: Date;
|
||||
setDate: (date: Date) => void;
|
||||
children: ReactNode;
|
||||
getTransit: GetTransition;
|
||||
}
|
||||
const Setup: React.FC<SetupProps> = ({
|
||||
children,
|
||||
day,
|
||||
setDate,
|
||||
getTransit,
|
||||
}) => (
|
||||
<CalendarProvider date={day} setDate={setDate}>
|
||||
<RoutinesProvider>
|
||||
<LocationProvider getTransition={getTransit} lookup={() => []}>
|
||||
<AgendaContextProvider day={day}>
|
||||
{children}
|
||||
</AgendaContextProvider>
|
||||
</LocationProvider>
|
||||
</RoutinesProvider>
|
||||
</CalendarProvider>
|
||||
);
|
||||
|
||||
export type { SetupProps };
|
||||
export { Setup };
|
||||
76
src/features/tasks/hooks.ts
Normal file
76
src/features/tasks/hooks.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useMemo } from "react";
|
||||
import { useDate, useToday } from "#/features/calendar"
|
||||
import { useRoutines } from "#/features/routines";
|
||||
import { Task } from "#/types/task";
|
||||
import { set } from "date-fns";
|
||||
|
||||
const toToday = (today: Date, target: Date) => set(target, {
|
||||
year: today.getFullYear(),
|
||||
month: today.getMonth(),
|
||||
date: today.getDate(),
|
||||
})
|
||||
|
||||
export const useTasks = () => {
|
||||
const start = useDate();
|
||||
const day = useMemo(
|
||||
() => start.getDay(),
|
||||
[start],
|
||||
)
|
||||
const [fromCalendar = []] = useToday(start);
|
||||
const fromRoutines = useRoutines(day);
|
||||
|
||||
const tasksFromCalendar = useMemo<Task[]>(
|
||||
() => fromCalendar.filter(e => !e.allDay).map(task => {
|
||||
const start = new Date(task.startDate);
|
||||
const end = new Date(task.endDate);
|
||||
const duration = end.getTime() - start.getTime();
|
||||
return {
|
||||
id: task.id,
|
||||
name: task.title,
|
||||
external: true,
|
||||
required: true,
|
||||
start: {
|
||||
min: start,
|
||||
max: start,
|
||||
},
|
||||
priority: 100,
|
||||
duration: {
|
||||
min: duration,
|
||||
},
|
||||
};
|
||||
}),
|
||||
[fromCalendar],
|
||||
);
|
||||
|
||||
const tasksFromRoutines = useMemo<Task[]>(
|
||||
() => fromRoutines.map(task => ({
|
||||
id: task.id,
|
||||
name: task.title,
|
||||
locations: task.location,
|
||||
start: {
|
||||
min: toToday(start, task.start.min),
|
||||
max: toToday(start, task.start.max),
|
||||
},
|
||||
priority: task.priority,
|
||||
required: task.required,
|
||||
duration: {
|
||||
min: task.duration,
|
||||
},
|
||||
})),
|
||||
[fromRoutines, start],
|
||||
);
|
||||
|
||||
const tasks = useMemo(
|
||||
() => ({
|
||||
calendar: tasksFromCalendar,
|
||||
routines: tasksFromRoutines,
|
||||
all: [
|
||||
...tasksFromRoutines,
|
||||
...tasksFromCalendar,
|
||||
],
|
||||
}),
|
||||
[tasksFromCalendar, tasksFromRoutines],
|
||||
);
|
||||
|
||||
return tasks;
|
||||
}
|
||||
1
src/features/tasks/index.ts
Normal file
1
src/features/tasks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './hooks';
|
||||
Reference in New Issue
Block a user