AlexAegis/loreplotter

View on GitHub
src/app/service/lore.service.ts

Summary

Maintainability
C
1 day
Test Coverage
import { Enclosing, Node } from '@alexaegis/avl';
import { Offset } from '@angular-skyhook/core';
import { Injectable } from '@angular/core';
import { BaseDirective } from '@app/component/base-component.class';
import { normalizeFromWindow } from '@app/function';
import { enclosingProgress } from '@app/function/enclosing-progress.function';
import { refreshBlockOfActor } from '@app/function/refresh-block-component.function';
import { Actor, ActorDelta, Lore, Planet } from '@app/model/data';
import { UnixWrapper } from '@app/model/data/unix-wrapper.class';
import { tweenMap } from '@app/operator';
import { ActorService } from '@app/service/actor.service';
import { LoreDocumentMethods } from '@app/service/database';
import { DatabaseService } from '@app/service/database.service';
import { EngineService } from '@lore/engine/engine.service';
import { Axis } from '@lore/engine/helper';

import { ActorObject } from '@lore/engine/object';
import { StoreFacade } from '@lore/store/store-facade.service';
import { Easing } from '@tweenjs/tween.js';
import { RxAttachment, RxDocument } from 'rxdb';
import { BehaviorSubject, combineLatest, EMPTY, from, Observable, of, ReplaySubject, Subject, zip } from 'rxjs';
import { filter, flatMap, map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
import { Group, Math as ThreeMath, Vector3 } from 'three';

const DAY_IN_SECONDS = 86400;

/**
 * This service's goal is end consume the data coming start the database and the engine and then update both
 */
@Injectable()
export class LoreService extends BaseDirective {
    public spawnOnWorld = new Subject<{ point: ActorObject; position: Vector3 }>();
    public containerWidth = new ReplaySubject<number>(1);
    public easeCursorTo = new Subject<number>();
    public easeCursorToUnix = new Subject<number>();

    constructor(
        private engineService: EngineService,
        private databaseService: DatabaseService,
        private storeFacade: StoreFacade,
        private actorService: ActorService
    ) {
        super();

        // This subscription makes sure that always the current texture is shown
        this.teardown = this.databaseService.currentLore$
            .pipe(
                mergeMap(lore =>
                    lore.allAttachments$.pipe(
                        flatMap(attachments => attachments),
                        filter(attachment => attachment.id === 'texture'),
                        map(doc => (doc as any) as RxAttachment<Lore, LoreDocumentMethods>),
                        switchMap(doc => doc.getData()),
                        map(att => ({ lore: lore, att: att }))
                    )
                )
            )
            .subscribe(({ lore, att }) => {
                engineService.globe.radius = lore.planet.radius;
                engineService.globe.displacementTexture.loadFromBlob(att);
                engineService.refreshPopupPosition();
            });
        this.teardown = this.storeFacade.cursor$
            .pipe(
                filter(cursor => this.engineService.stage !== undefined && this.engineService.stage.sun !== undefined)
            )
            .subscribe(cursor => {
                this.engineService.stage.sun.resetPosition();
                this.engineService.stage.sun.position.applyAxisAngle(
                    Axis.y,
                    ((cursor % DAY_IN_SECONDS) / DAY_IN_SECONDS) * -360 * ThreeMath.DEG2RAD
                );
            });

        this.teardown = this.easeCursorTo
            .pipe(
                filter(to => to !== undefined),
                withLatestFrom(this.storeFacade.frame$, this.containerWidth),
                map(([position, { start, end }, containerWidth]) =>
                    Math.floor(ThreeMath.mapLinear(position, 0, containerWidth, start, end))
                )
            )
            .subscribe(next => this.easeCursorToUnix.next(next));

        const eastCursorFromUnixConvert$ = this.easeCursorToUnix.pipe(
            filter(to => to !== undefined),
            withLatestFrom(this.storeFacade.cursor$),
            map(([target, cursor]) => ({
                from: { cursor: cursor },
                to: { cursor: target }
            }))
        );

        this.teardown = eastCursorFromUnixConvert$ // , eastCursorFromUnixConvert$
            .pipe(
                tweenMap({
                    duration: 220,
                    easing: Easing.Exponential.Out,
                    pingpongInterrupt: true,
                    pingpongAfterFinish: false,
                    sendUndefined: true,
                    doOnNext: next => {
                        if (next) {
                            this.storeFacade.setCursorOverride(next.cursor);
                        }
                    },
                    doOnComplete: () => {
                        this.storeFacade.bakeCursorOverride();
                    }
                })
            )
            .subscribe();
        // This subscriber's job is end map each actors state end the map based on the current cursor
        this.teardown = combineLatest([
            this.databaseService.currentLoreActors$,
            this.storeFacade.cursor$,
            this.overrideNodePosition
        ])
            .pipe(
                flatMap(([actors, cursor, overrides]) =>
                    actors.map(actor => ({
                        actor: actor,
                        cursor: cursor,
                        overrideNodePositions: overrides && actor.id === overrides.actorId ? overrides : undefined
                    }))
                )
            )
            .subscribe(({ actor, cursor, overrideNodePositions }) => {
                const enclosure = actor._states.enclosingNodes(new UnixWrapper(cursor)) as Enclosing<
                    Node<UnixWrapper, ActorDelta>
                >;
                if (enclosure.last === undefined && enclosure.first !== undefined) {
                    enclosure.last = enclosure.first;
                } else if (enclosure.first === undefined && enclosure.last !== undefined) {
                    enclosure.first = enclosure.last;
                }
                if (overrideNodePositions !== undefined && overrideNodePositions.overrides.length > 0) {
                    for (const node of actor._states.nodes()) {
                        overrideNodePositions.overrides
                            .filter(ov => ov.previous === node.key.unix)
                            .forEach(ov => {
                                node.key.unix = ov.new;
                            });
                        if (
                            enclosure.first === undefined ||
                            (node.key.unix >= enclosure.first.key.unix && node.key.unix <= cursor)
                        ) {
                            enclosure.first = node;
                        }
                        if (
                            enclosure.last === undefined ||
                            (node.key.unix <= enclosure.last.key.unix && node.key.unix >= cursor)
                        ) {
                            enclosure.last = node;
                        }
                    }
                }

                const t = enclosingProgress(enclosure, cursor);
                let actorObject = this.engineService.globe.getObjectByName(actor.id) as ActorObject;
                let group: Group;
                if (actorObject) {
                    group = actorObject.parent as Group;
                } else {
                    group = new Group();
                    actorObject = new ActorObject(
                        actor,
                        this.storeFacade,
                        this,
                        this.actorService,
                        engineService.globe
                    );
                    group.add(actorObject);
                    this.engineService.globe.add(group);

                    this.engineService.control.zoomUpdate(this.engineService.stage.camera.position.length());
                    actorObject.updateHeightAndWorldPosAndScale();
                }

                if (
                    group.userData.override === undefined &&
                    enclosure.last !== undefined &&
                    enclosure.first !== undefined
                ) {
                    this.actorService.lookAtInterpolated(
                        enclosure.last.value.position,
                        enclosure.first.value.position,
                        t,
                        group
                    );

                    actorObject.updateHeight();
                } else if (group.userData.override === false) {
                    delete group.userData.override;
                }

                engineService.refreshPopupPosition();
            });

        // This subscriptions job is end create a brand new actor
        this.teardown = this.spawnActorOnClientOffset
            .pipe(
                filter(o => o !== undefined),
                withLatestFrom(
                    this.databaseService.currentLore$,
                    this.databaseService.nextActorId$,
                    this.storeFacade.cursor$
                ),
                switchMap(([offset, lore, nextId, cursor]) => {
                    const dropVector = this.engineService.intersection(normalizeFromWindow(offset.x, offset.y));
                    if (dropVector) {
                        dropVector.applyQuaternion(this.engineService.globe.quaternion.clone().inverse());
                        const actor = new Actor(nextId, lore.id);
                        actor._states.set(
                            new UnixWrapper(Math.floor(cursor)),
                            new ActorDelta(undefined, { x: dropVector.x, y: dropVector.y, z: dropVector.z })
                        );
                        return lore.collection.database.actor.upsert(actor);
                    } else {
                        return EMPTY;
                    }
                })
            )
            .subscribe();

        this.teardown = this.spawnOnWorld
            .pipe(
                filter(o => o !== undefined),
                withLatestFrom(this.storeFacade.cursor$),
                switchMap(async ([{ point, position }, cursor]) => {
                    point.applyQuaternion(this.engineService.globe.quaternion.clone().inverse());
                    const wrapper = new UnixWrapper(Math.floor(cursor));
                    const existingDelta = point.actor._states.get(wrapper);

                    if (existingDelta) {
                        existingDelta.position.x = position.x;
                        existingDelta.position.y = position.y;
                        existingDelta.position.z = position.z;
                    } else {
                        point.actor._states.set(
                            wrapper,
                            new ActorDelta(undefined, {
                                x: position.x,
                                y: position.y,
                                z: position.z
                            })
                        );
                    }
                    const updatedActor = await point.actor.atomicUpdate(a => (a._states = point.actor._states) && a);
                    point.parent.userData.override = false;
                    refreshBlockOfActor(point.actor);

                    return updatedActor;
                })
            )
            .subscribe();

        this.teardown = this.engineService.textureChange$
            .pipe(
                switchMap(texture => from(new Promise<Blob>(res => texture.canvas.toBlob(res, 'image/jpeg')))),
                withLatestFrom(this.databaseService.currentLore$),
                switchMap(([texture, loreDoc]) =>
                    loreDoc.putAttachment({
                        id: 'texture', // string, name of the attachment like 'cat.jpg'
                        data: texture, // (string|Blob|Buffer) data of the attachment
                        type: 'image/jpeg' // (string) type of the attachment-data like 'image/jpeg'
                    })
                )
            )
            .subscribe();
    }

    public spawnActorOnClientOffset = new Subject<Offset>();
    public overrideNodePosition = new BehaviorSubject<{
        actorId: string;
        overrides: Array<{ original: number; previous: number; new: number }>;
    }>(undefined);

    /**
     * Creates a new lore object in the database
     * @param lore from state be created, ! this parameter cant be modified since it's from the state !
     */
    public create(lore: Lore): Observable<RxDocument<Lore, LoreDocumentMethods>> {
        return zip(this.databaseService.database$, this.databaseService.nextLoreId$).pipe(
            map(([connection, nextId]) => ({
                connection,
                json: new Lore(nextId, lore.name, new Planet(lore.planet.name, lore.planet.radius))
            })),
            switchMap(({ connection, json }) => connection.lore.insert(json))
        );
    }

    /**
     * Creates a new lore object in the database
     * @param lore from state be created, ! this parameter cant be modified since it's from the state !
     */
    public update(lore: Partial<Lore>): Observable<RxDocument<Lore, LoreDocumentMethods>> {
        return this.databaseService.database$.pipe(
            map(connection => ({
                connection,
                json: new Lore(lore.id, lore.name, new Planet(lore.planet.name, lore.planet.radius))
            })),
            switchMap(({ connection, json }) => connection.lore.upsert(json))
        );
    }

    /**
     * Deletes a lore object from the database and also all the actors
     * @param id of the lore to be deleted
     */
    public delete(id: string): Observable<boolean> {
        return this.databaseService.database$.pipe(
            switchMap(connection =>
                connection.lore
                    .find({ id: id })
                    .$.pipe(
                        mergeMap(lores =>
                            connection.actor.find({ loreId: id }).$.pipe(map(actors => ({ lores, actors })))
                        )
                    )
            ),
            mergeMap(({ lores, actors }) =>
                zip(of(...lores).pipe(switchMap(l => l.remove())), of(...actors).pipe(switchMap(a => a.remove())))
            ),
            map(([l, a]) => l || a)
        );
    }
}