This commit is contained in:
Morten Olsen
2022-05-06 14:39:49 +02:00
parent d83a4aebc7
commit 7057e11e96
18 changed files with 257 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
import { Context, GraphNode } from "#/types/graph";
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
import { getNext } from "./get-next";
import { getImpossible, getNext } from "./get-next";
enum Strategies {
all = 'all',
@@ -13,6 +13,7 @@ type RunningStatus = {
current: 'running';
nodes: number;
start: Date;
strategy: Strategies,
cancel: () => void;
}
@@ -21,6 +22,7 @@ type CompletedStatus = {
start: Date;
end: Date;
nodes: number;
strategy: Strategies,
}
type Status = RunningStatus | CompletedStatus;
@@ -65,38 +67,62 @@ const buildGraph = async ({
sleepTime = 10,
}: BuildGraphOptions) => {
const start = new Date();
let leafs: GraphNode[] = [{
let nodeCount = 0;
let running = true;
const { remaining, impossible } = getImpossible(tasks, time);
let leafList: GraphNode[] = [{
location,
time: {
end: time,
start: time,
},
score: 0,
remainingTasks: tasks,
impossibeTasks: [],
remainingTasks: remaining,
impossibeTasks: impossible,
status: {
dead: false,
completed: false,
},
}];
let nodes = 0;
let running = true;
const final: GraphNode[] = [];
const completedList: GraphNode[] = [];
const deadList: GraphNode[] = [];
const complete = (nodes: GraphNode[]) => {
if (callback) {
callback({
current: 'completed',
nodes: nodeCount,
start,
end: new Date(),
strategy,
});
}
return nodes.sort((a, b) => b.score - a.score);
}
while (true) {
nodes++;
nodeCount++;
if (!running) {
return [];
}
const node = leafs.pop();
if (
leafList.length === 0
&& completedList.length === 0
&& strategy !== Strategies.all
) {
strategy = Strategies.all;
leafList.push(...deadList);
}
const node = leafList.pop();
if (!node) {
break;
}
if (nodes % batchSize === 1) {
if (nodeCount % batchSize === 0) {
if (callback) {
callback({
current: 'running',
nodes,
nodes: nodeCount,
strategy,
start,
cancel: () => {
running = false;
@@ -106,38 +132,28 @@ const buildGraph = async ({
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
const [alive, completed, dead] = fil([
n => (strategy === Strategies.all || !n.status.dead) && !n.status.completed,
n => !!n.status.completed && (strategy === Strategies.all || !n.status.dead),
n => n.status.dead,
], next);
leafs.push(...alive);
leafList.push(...alive);
if (strategy === Strategies.firstValid && completed.length > 0) {
if (callback) {
callback({ current: 'completed', nodes, start, end: new Date() })
}
return completed;
return complete(completed);
}
if (completed.length > 0) {
final.push(...completed)
completedList.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];
return complete([fullComplete]);
}
}
deadList.push(...dead);
}
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);
return complete(completedList);
}
export type { Status, BuildGraphOptions };

View File

@@ -13,7 +13,7 @@ type GetImpossibleResult = {
impossible: Task[];
}
const getImpossible = (
export const getImpossible = (
tasks: Task[],
time: Date,
) => {
@@ -50,15 +50,15 @@ const calculateScore = ({
score += task.priority * 10;
impossible.forEach((task) => {
if (task.required) {
score -= 1000;
score -= 10000 + (1 * task.priority);
} else {
score -= task.priority;
score -= 100 + (1 * task.priority);
}
});
});
if (transition) {
const minutes = transition.time / 1000 / 60
score -= minutes;
score -= 10 + (1 * minutes);
}
return score;
}

View File

@@ -0,0 +1,15 @@
import { createContext } from 'react';
import { Strategies } from "./algorithm/build-graph";
type PlannerOptions = {
strategy: Strategies;
}
type PlannerContextValue = {
options: PlannerOptions;
setOptions: (options: Partial<PlannerOptions>) => void;
}
const PlannerContext = createContext<PlannerContextValue>(undefined as any);
export type { PlannerContextValue, PlannerOptions };
export { PlannerContext };

View File

@@ -5,9 +5,10 @@ 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 { useContext, useMemo, useState } from "react";
import { PlanItem } from "#/types/plans";
import { Task } from "#/types/task";
import { PlannerContext } from "./context";
export type UsePlanOptions = {
location: UserLocation;
@@ -23,10 +24,21 @@ export type UsePlan = [
}
]
export const usePlanOptions = () => {
const { options } = useContext(PlannerContext);
return options;
}
export const useSetPlanOptions = () => {
const { setOptions } = useContext(PlannerContext);
return setOptions;
}
export const usePlan = ({
location,
}: UsePlanOptions): UsePlan => {
const today = useDate();
const planOptions = usePlanOptions();
const [status, setStatus] = useState<Status>();
const all = useTasksWithContext();
const enabled = useMemo(() => all.filter(f => f.enabled), [all])
@@ -37,7 +49,7 @@ export const usePlan = ({
location,
time: start || today,
tasks: enabled,
strategy: Strategies.firstComplet,
strategy: planOptions.strategy,
context: {
getTransition,
},
@@ -50,7 +62,7 @@ export const usePlan = ({
agenda: day,
};
},
[today, location, all, setStatus],
[today, location, all, setStatus, planOptions],
);
return [

View File

@@ -1 +1,4 @@
export { PlannerProvider } from './provider';
export type { PlannerOptions } from './context';
export { Strategies } from './algorithm/build-graph';
export * from './hooks';

View File

@@ -0,0 +1,32 @@
import { ReactNode, useCallback, useState } from 'react';
import { Strategies } from './algorithm/build-graph';
import { PlannerContext, PlannerOptions } from './context';
type PlannerProviderProps = {
children: ReactNode;
};
const PlannerProvider: React.FC<PlannerProviderProps> = ({ children }) => {
const [options, setOwnOptions] = useState<PlannerOptions>({
strategy: Strategies.firstComplet,
})
const setOptions = useCallback(
(next: Partial<PlannerOptions>) => {
setOwnOptions(current => ({
...current,
...next,
}))
},
[setOwnOptions],
);
return (
<PlannerContext.Provider value={{ options, setOptions }}>
{children}
</PlannerContext.Provider>
);
}
export type { PlannerProviderProps };
export { PlannerProvider };

View File

@@ -3,6 +3,7 @@ import { ReactNode } from "react"
import { AgendaContextProvider } from "./agenda-context"
import { CalendarProvider } from "./calendar"
import { LocationProvider } from "./location"
import { PlannerProvider } from "./planner"
import { RoutinesProvider } from "./routines"
type SetupProps = {
@@ -21,7 +22,9 @@ const Setup: React.FC<SetupProps> = ({
<RoutinesProvider>
<LocationProvider getTransition={getTransit} lookup={() => []}>
<AgendaContextProvider day={day}>
<PlannerProvider>
{children}
</PlannerProvider>
</AgendaContextProvider>
</LocationProvider>
</RoutinesProvider>

View File

@@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import styled from 'styled-components/native';
import { TouchableOpacity } from 'react-native';
import { IconNames, Icon } from '#/ui/components';
import { IconNames, Icon } from '../icon';
import { Theme } from '#/ui/theme';
import { Link } from '#/ui/typography';

View File

@@ -0,0 +1,56 @@
import { cmyk } from "chroma-js";
import { ReactNode, useCallback, useMemo, useState } from "react";
import { Modal } from "../../modal";
import { Row } from "../../row";
type SelectorProps<T> = {
label: string;
items: {
display: ReactNode;
value: T;
}[];
getId: (item: T) => string;
selected?: T;
setSelected: (item?: T) => void;
}
function Selector<T = any>({
items,
label,
getId,
selected,
setSelected,
}: SelectorProps<T>) {
const [visible, setVisible] = useState(false);
const selectedItem = useMemo(
() => selected ? items.find(i => getId(i.value) === getId(selected)) : undefined,
[selected, items],
);
const select = useCallback(
(item: T) => {
setSelected(item);
setVisible(false);
},
[setSelected, setVisible],
);
return (
<>
<Modal visible={visible} onClose={() => setVisible(false)}>
{items.map((item) => (
<Row
key={getId(item.value)}
onPress={() => select(item.value)}
title={item.display}
/>
))}
</Modal>
<Row
overline={label}
onPress={() => setVisible(true)}
title={selectedItem?.display || 'Select'}
/>
</>
)
}
export { Selector };

View File

@@ -1,4 +1,6 @@
export * from './icon';
export * from './modal';
export * from './icon';
export * from './form';
export * from './page';
export * from './popup';

View File

@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
import { Modal as Wrapper } from 'react-native';
import { Popup } from '../popup';
type ModalProps = {
visible: boolean;
onClose: () => void;
children: ReactNode;
}
const Modal: React.FC<ModalProps> = ({ visible, onClose, children }) => (
<Wrapper
transparent
visible={visible}
animationType="slide"
onRequestClose={onClose}
onDismiss={onClose}
>
<Popup onClose={onClose}>
{children}
</Popup>
</Wrapper>
);
export { Modal };

View File

@@ -51,4 +51,4 @@ const Popup: React.FC<Props> = ({ visible, children, onClose }) => {
);
};
export default Popup;
export { Popup };

View File

@@ -12,6 +12,7 @@ import { RoutineSetScreen } from '../screens/routines/set';
import { TaskListScreen } from '../screens/plan/tasks';
import { AgendaContextSetScreen } from '../screens/plan/set';
import { Icon } from '../components';
import { PlanSettingsScreen } from '../screens/plan/settings';
const MainTabsNvaigator = createBottomTabNavigator();
@@ -79,6 +80,7 @@ const Root: React.FC = () => (
<RootNavigator.Screen name="locationSet" component={LocationSetScreen} />
<RootNavigator.Screen name="routineSet" component={RoutineSetScreen} />
<RootNavigator.Screen name="agendaContextSet" component={AgendaContextSetScreen} />
<RootNavigator.Screen name="planSettings" component={PlanSettingsScreen} />
</RootNavigator.Group>
</RootNavigator.Navigator>
);

View File

@@ -1,9 +1,8 @@
import { useLocations, useSetLocation } from "#/features/location";
import { useNavigation, useRoute } from '@react-navigation/native';
import { Button, TextInput } from "#/ui/components";
import { Popup, Button, TextInput } from "#/ui/components";
import { useCallback, useEffect, useMemo, useState } from "react";
import { nanoid } from 'nanoid';
import Popup from "#/ui/components/popup";
const LocationSetScreen: React.FC = () => {
const { params = {} } = useRoute() as any;

View File

@@ -8,19 +8,19 @@ import { useCommit, useDate } from "#/features/calendar";
import { format, formatDistance, formatDistanceToNow, set } from "date-fns";
import styled from "styled-components/native";
import { Status } from "#/features/planner/algorithm/build-graph";
import { useNavigation } from "@react-navigation/native";
const Wrapper = styled.ScrollView`
`;
const getStats = (status: Status) => {
console.log('status', status);
if (status.current === 'running') {
const runTime = formatDistanceToNow(status.start, { includeSeconds: true })
return `calulated ${status.nodes} nodes in ${runTime}`;
return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`;
}
const runTime = formatDistance(status.start, status.end, { includeSeconds: true })
return `calulated ${status.nodes} nodes in ${runTime}`;
return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`;
};
const PlanDayScreen: React.FC = () => {
@@ -28,10 +28,11 @@ const PlanDayScreen: React.FC = () => {
const [location] = useCurrentLocation();
const [startTime, setStartTime] = useState('06:00');
const [commit] = useCommit();
const { navigate } = useNavigation();
const current = useMemo(
() => location || {
id: 'unknown',
title: 'foo',
title: 'Unknown',
},
[location]
)
@@ -70,6 +71,9 @@ const PlanDayScreen: React.FC = () => {
<Button onPress={() => commit(options.result?.agenda || [])} icon="download" />
</Cell>
)}
<Cell>
<Button onPress={() => navigate('planSettings')} icon="settings" />
</Cell>
</>
)}
/>

View File

@@ -1,11 +1,10 @@
import { useLocations, useSetLocation } from "#/features/location";
import { useNavigation, useRoute } from '@react-navigation/native';
import { Button, Checkbok, TextInput } from "#/ui/components";
import { Popup, Button, Checkbok, TextInput } from "#/ui/components";
import { useCallback, useEffect, useMemo, useState } from "react";
import { nanoid } from 'nanoid';
import { useAgendaContext, useSetAgendaContext } from "#/features/agenda-context";
import { format } from "date-fns";
import Popup from "#/ui/components/popup";
const AgendaContextSetScreen: React.FC = () => {
const { params = {} } = useRoute() as any;

View File

@@ -0,0 +1,40 @@
import { Strategies, usePlanOptions, useSetPlanOptions } from "#/features/planner"
import { Selector } from "#/ui/components/form/selector";
import { Popup } from "#/ui/components";
import { useNavigation } from "@react-navigation/native";
const items = [{
display: 'First valid',
value: Strategies.firstValid,
}, {
display: 'First complete',
value: Strategies.firstComplet,
}, {
display: 'All valid',
value: Strategies.allValid,
}, {
display: 'All',
value: Strategies.all,
}];
const PlanSettingsScreen: React.FC = () => {
const options = usePlanOptions();
const setOptions = useSetPlanOptions();
const { goBack } = useNavigation();
return (
<Popup onClose={goBack}>
<Selector
label="Strategy"
items={items}
getId={i => i}
selected={options.strategy}
setSelected={(strategy) => {
setOptions({ strategy: strategy || Strategies.firstComplet });
}}
/>
</Popup>
);
}
export { PlanSettingsScreen };

View File

@@ -1,11 +1,10 @@
import { useNavigation, useRoute } from '@react-navigation/native';
import { Button, Checkbok, TextInput } from "#/ui/components";
import { Popup, Button, Checkbok, TextInput } from "#/ui/components";
import { useCallback, useEffect, useMemo, useState } from "react";
import { nanoid } from 'nanoid';
import { useRoutines, useSetRoutine } from '#/features/routines';
import { format } from 'date-fns';
import { useLocations } from '#/features/location';
import Popup from '#/ui/components/popup';
const RoutineSetScreen: React.FC = () => {
const { params = {} } = useRoute() as any;