src/app/service/actor.service.ts
import { Node } from '@alexaegis/avl';
import { Injectable } from '@angular/core';
import { refreshBlockOfActor } from '@app/function';
import { enclosingProgress } from '@app/function/enclosing-progress.function';
import { EngineService } from '@app/lore/engine/engine.service';
import { Actor, ActorDelta, UnixWrapper, Vector3Serializable } from '@app/model/data';
import { DatabaseService } from '@app/service/database.service';
import { ActorFormResultData } from '@lore/component';
import { StoreFacade } from '@lore/store/store-facade.service';
import { RxDocument } from 'rxdb';
import { combineLatest, from, Observable, of, Subject } from 'rxjs';
import { filter, map, mergeMap, mergeScan, pairwise, scan, shareReplay, switchMap, tap, toArray } from 'rxjs/operators';
import { Group, Vector3 } from 'three';
import { Property } from '@app/model/data/property.class';
export const FORGET_TOKEN = '__FORGET__';
export interface Accumulator {
cursor: number;
actor: RxDocument<Actor>;
accumulator: ActorDeltaAccumulator;
firstEvent: Node<UnixWrapper, ActorDelta>;
lastEvent: Node<UnixWrapper, ActorDelta>;
previousEvent: Node<UnixWrapper, ActorDelta>;
nextEvent: Node<UnixWrapper, ActorDelta>;
}
export class AccumulatorField<T> {
appearedIn: Node<UnixWrapper, ActorDelta>;
value: T;
nextValue: T;
nextAppearance: Node<UnixWrapper, ActorDelta>;
}
export class ActorDeltaAccumulator {
id = new AccumulatorField<string>();
unix = new AccumulatorField<number>();
name = new AccumulatorField<string>();
maxSpeed = new AccumulatorField<number>();
color = new AccumulatorField<string>();
position = new AccumulatorField<Vector3Serializable>();
properties: Array<AccumulatorField<Property>> = [];
}
@Injectable()
export class ActorService {
public actorFormSave = new Subject<ActorFormResultData>();
// best name ever
public slerperHelper: Group;
public latestSlerpsWorldPositionHolder: Vector3;
public pseudoPoint: Group;
private _w = new UnixWrapper(0);
/**
* rotates the position t'th way between the enclosure
* Returns a new worldPosition at radius 1
*/
public lookAtInterpolated = (() => {
const _result = new Vector3();
const _norm = new Vector3();
const _from = new Vector3();
const _to = new Vector3();
return (a: Vector3Serializable, b: Vector3Serializable, t: number, target?: Group): Vector3 => {
_from.copy(a as Vector3);
_to.copy(b as Vector3);
if (t === Infinity || isNaN(t)) {
_result.copy(_from);
} else if (t === -Infinity) {
_result.copy(_to);
} else {
const ang = _from.angleTo(_to);
_norm
.copy(_from)
.cross(_to)
.normalize();
_result.copy(_from).applyAxisAngle(_norm, t * ang);
}
if (target) {
target.lookAt(_result);
}
return _result;
};
})();
private _va = new Vector3(); // Helper vector objects
public constructor(
private engineService: EngineService,
private storeFacade: StoreFacade,
private databaseService: DatabaseService
) {
this.slerperHelper = new Group();
this.pseudoPoint = new Group();
this.pseudoPoint.position.set(0, 0, 1);
this.slerperHelper.add(this.pseudoPoint);
this.latestSlerpsWorldPositionHolder = new Vector3();
}
// TODO: This makes a bunch of objects during run
public actorDeltasAtCursor$: Observable<Array<Accumulator>> = combineLatest([
this.databaseService.currentLoreActors$,
this.storeFacade.cursor$
]).pipe(
mergeMap(([actors, cursor]) =>
of(...actors).pipe(
map(actor => {
const accumulator = new ActorDeltaAccumulator();
const propertyMap = new Map<string, AccumulatorField<string>>();
let firstEvent: Node<UnixWrapper, ActorDelta>;
let previousEvent: Node<UnixWrapper, ActorDelta>;
let nextEvent: Node<UnixWrapper, ActorDelta>;
let lastEvent: Node<UnixWrapper, ActorDelta>;
accumulator.color.value = Actor.DEFAULT_COLOR;
accumulator.maxSpeed.value = Actor.DEFAULT_MAX_SPEED;
let reached = false;
for (const node of actor._states.nodes()) {
if (firstEvent === undefined) {
firstEvent = node;
}
if (node.key.unix < cursor) {
previousEvent = node;
}
lastEvent = node;
if (node.key.unix > cursor) {
reached = true;
if (nextEvent === undefined) {
nextEvent = node;
}
}
if (!reached) {
if (node.key.unix !== undefined) {
accumulator.unix.value = node.key.unix;
accumulator.unix.appearedIn = node;
}
if (node.value.name) {
accumulator.name.value = node.value.name;
accumulator.name.appearedIn = node;
}
if (node.value.maxSpeed !== undefined) {
accumulator.maxSpeed.value = node.value.maxSpeed;
accumulator.maxSpeed.appearedIn = node;
}
if (node.value.color) {
accumulator.color.value = node.value.color;
accumulator.color.appearedIn = node;
}
if (node.value.position !== undefined) {
accumulator.position.value = node.value.position;
accumulator.position.appearedIn = node;
}
if (node.value.properties) {
node.value.properties.forEach(({ key, value }) => {
const prop = propertyMap.get(key);
if (prop) {
if (value) {
if (value === FORGET_TOKEN) {
if (!prop.value.startsWith(FORGET_TOKEN)) {
prop.value = `${FORGET_TOKEN}.${prop.value}`;
}
} else {
prop.value = value;
}
prop.appearedIn = node;
}
} else {
const propField = new AccumulatorField<string>();
propField.value = value;
propField.appearedIn = node;
propertyMap.set(key, propField);
}
});
}
} else {
if (node.key.unix !== undefined && accumulator.unix.nextAppearance === undefined) {
accumulator.unix.nextValue = node.key.unix;
accumulator.unix.nextAppearance = node;
}
if (node.value.name && accumulator.name.nextAppearance === undefined) {
accumulator.name.nextValue = node.value.name;
accumulator.name.nextAppearance = node;
}
if (
node.value.maxSpeed !== undefined &&
accumulator.maxSpeed.nextAppearance === undefined
) {
accumulator.maxSpeed.nextValue = node.value.maxSpeed;
accumulator.maxSpeed.nextAppearance = node;
}
if (node.value.color && accumulator.color.nextAppearance === undefined) {
accumulator.color.nextValue = node.value.color;
accumulator.color.nextAppearance = node;
}
if (
node.value.position !== undefined &&
accumulator.position.nextAppearance === undefined
) {
accumulator.position.nextValue = node.value.position;
accumulator.position.nextAppearance = node;
}
if (node.value.properties) {
node.value.properties.forEach(({ key, value }) => {
const prop = propertyMap.get(key);
if (prop) {
if (value !== undefined && prop.nextAppearance === undefined) {
prop.nextValue = value;
prop.nextAppearance = node;
}
} else {
const propField = new AccumulatorField<string>();
propField.nextValue = value;
propField.nextAppearance = node;
propertyMap.set(key, propField);
}
});
}
}
}
for (const [key, value] of propertyMap.entries()) {
const accField = new AccumulatorField<Property>();
accField.nextValue = { key, value: value.nextValue };
accField.nextAppearance = value.nextAppearance;
accField.appearedIn = value.appearedIn;
accField.value = { key, value: value.value };
accumulator.properties.push(accField);
}
return { cursor, actor, accumulator, firstEvent, lastEvent, previousEvent, nextEvent };
}),
toArray()
)
),
shareReplay(1)
);
public selectedActorAccumulatorAtCursor$: Observable<Accumulator> = combineLatest([
this.actorDeltasAtCursor$,
this.engineService.selected.pipe(
filter(actorObject => actorObject !== undefined),
map(actorObject => actorObject.actor)
)
]).pipe(
map(([all, selected]) => all.find(acc => acc.actor.id === selected.id)),
filter(delta => delta !== undefined)
);
private _vb = new Vector3();
public actorDialogSubscription = this.actorFormSave
.pipe(
filter(data => data !== undefined),
switchMap(({ actor, name, maxSpeed, date, properties, newProperties, color }) => {
const wrapper = new UnixWrapper(Math.floor(date.unix()));
const finalPosition = this.actorPositionAt(actor, wrapper.unix);
const propertyMap = new Map<string, string>();
properties
.filter(e => e.value || e.forget)
.map(k => {
if (k.forget) {
k.value = FORGET_TOKEN;
}
return k;
})
.forEach(({ key, value }) => propertyMap.set(key, value));
newProperties.filter(({ value }) => !!value).forEach(({ key, value }) => propertyMap.set(key, value));
const finalProperties: Array<Property> = [];
for (const [key, value] of propertyMap.entries()) {
finalProperties.push(new Property(key, value));
}
const delta = new ActorDelta(name ? name : undefined, finalPosition, finalProperties, maxSpeed, color);
actor._states.set(wrapper, delta);
return from(
actor.atomicUpdate(a => {
a._states = actor._states;
return a;
})
).pipe(map(a => actor));
}),
tap(actor => refreshBlockOfActor(actor))
)
.subscribe();
public maxPossiblePlanetRadius$ = this.databaseService.currentLoreActors$.pipe(
mergeMap(actors =>
of(...actors).pipe(
mergeScan(
(maxAcc, actor) =>
of(...actor._states.nodes()).pipe(
pairwise(),
scan(
(acc, [a, b]) => {
if (a.value.maxSpeed !== undefined) {
acc.lastMaxSpeed = a.value.maxSpeed;
}
this._va.copy(a.value.position as Vector3);
this._vb.copy(b.value.position as Vector3);
const time = Math.abs(b.key.unix - a.key.unix); // s
const maxDistance = acc.lastMaxSpeed * (time / 3600); // km/h * h = km, arc-length
const angle = this._va.angleTo(this._vb); // radian
const maxRadius = maxDistance / angle; // km, radius
if (acc.maxRadius >= maxRadius) {
// Min search
acc.maxRadius = maxRadius;
}
return acc;
},
{ lastMaxSpeed: Actor.DEFAULT_MAX_SPEED, maxRadius: Infinity }
),
map(({ maxRadius }) => (maxAcc > maxRadius ? maxRadius : maxAcc)) // the smallest maximum
),
Infinity
)
)
),
shareReplay(1)
);
public actorPositionAt(actor: RxDocument<Actor>, unix: number): Vector3Serializable {
let finalPosition: Vector3Serializable;
this._w.unix = unix;
const enclosing = actor._states.enclosingNodes(this._w);
if (enclosing.first === undefined || enclosing.last === undefined) {
let node = enclosing.first;
if (!node) {
node = enclosing.last;
}
finalPosition = {
x: node.value.position.x,
y: node.value.position.y,
z: node.value.position.z
};
} else {
const progress = enclosingProgress(enclosing, unix);
let worldPos: Vector3Serializable;
if (enclosing.last) {
worldPos = enclosing.last.value.position;
}
if (enclosing.first) {
worldPos = enclosing.first.value.position;
}
if (enclosing.last && enclosing.first) {
worldPos = this.lookAtInterpolated(
enclosing.last.value.position,
enclosing.first.value.position,
progress
);
}
finalPosition = { x: worldPos.x, y: worldPos.y, z: worldPos.z };
}
return finalPosition;
}
public accumulatorOf(actor: RxDocument<Actor>): Observable<Accumulator> {
return this.actorDeltasAtCursor$.pipe(
map(actorAccs => actorAccs.find(actorAcc => actorAcc.actor.id === actor.id)),
filter(accumulator => accumulator !== undefined)
);
}
}