diff options
author | eeng5 <eleanor.eng5@gmail.com> | 2019-11-19 17:47:13 -0500 |
---|---|---|
committer | eeng5 <eleanor.eng5@gmail.com> | 2019-11-19 17:47:13 -0500 |
commit | 7d89d10a3d43755c267fd48e18701d457ae7aa1c (patch) | |
tree | 059312d4d2dfeaa96b5e272abc7608b0d68b96b4 /src/client/views/nodes/ImageBox.tsx | |
parent | ab285371f6fb2a4f1e64888bafbc84b602f23416 (diff) | |
parent | 667d196a2cbc8e237de5be9a6f7ec4ecca9d2bb5 (diff) |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into server_refactor
Diffstat (limited to 'src/client/views/nodes/ImageBox.tsx')
-rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 267 |
1 files changed, 77 insertions, 190 deletions
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 07fd832be..3341a1bf1 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -2,10 +2,8 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEye } from '@fortawesome/free-regular-svg-icons'; import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, runInAction } from 'mobx'; +import { action, computed, observable, runInAction, trace } from 'mobx'; import { observer } from "mobx-react"; -import Lightbox from 'react-image-lightbox'; -import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc'; import { List } from '../../../new_fields/List'; import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; @@ -21,12 +19,13 @@ import { ContextMenu } from "../../views/ContextMenu"; import { ContextMenuProps } from '../ContextMenuItem'; import { DocAnnotatableComponent } from '../DocComponent'; import { InkingControl } from '../InkingControl'; -import { documentSchema } from './DocumentView'; import FaceRectangles from './FaceRectangles'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { documentSchema } from '../../../new_fields/documentSchemas'; +import { Id } from '../../../new_fields/FieldSymbols'; var requestImageSize = require('../../util/request-image-size'); var path = require('path'); const { Howl } = require('howler'); @@ -38,7 +37,10 @@ library.add(faFileAudio, faAsterisk); export const pageSchema = createSchema({ curPage: "number", - fitWidth: "boolean" + fitWidth: "boolean", + rotation: "number", + googlePhotosUrl: "string", + googlePhotosTags: "string" }); interface Window { @@ -55,29 +57,15 @@ const ImageDocument = makeInterface(pageSchema, documentSchema); @observer export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) { - - public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(ImageBox, "data", fieldExt); } - @observable static _showControls: boolean; + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); } private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); - private _downX: number = 0; - private _downY: number = 0; - private _lastTap: number = 0; - @observable private _isOpen: boolean = false; - private dropDisposer?: DragManager.DragDropDisposer; - @observable private hoverActive = false; + private _dropDisposer?: DragManager.DragDropDisposer; + @observable private _audioState = 0; + @observable static _showControls: boolean; protected createDropTarget = (ele: HTMLDivElement) => { - if (this.dropDisposer) { - this.dropDisposer(); - } - if (ele) { - this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); - } - } - onDrop = (e: React.DragEvent) => { - e.stopPropagation(); - e.preventDefault(); - console.log("IMPLEMENT ME PLEASE"); + this._dropDisposer && this._dropDisposer(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); } @undoBatch @@ -89,61 +77,18 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum e.stopPropagation(); } de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => { - Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); + this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop); e.stopPropagation(); })); } } - onPointerDown = (e: React.PointerEvent): void => { - if (e.shiftKey && e.ctrlKey) { - e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only - } - // if (Date.now() - this._lastTap < 300) { - // if (e.buttons === 1) { - // this._downX = e.clientX; - // this._downY = e.clientY; - // document.removeEventListener("pointerup", this.onPointerUp); - // document.addEventListener("pointerup", this.onPointerUp); - // } - // } else { - // this._lastTap = Date.now(); - // } - } - @action - onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointerup", this.onPointerUp); - if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { - this._isOpen = true; - } - e.stopPropagation(); - } - - @action - lightbox = (images: string[]) => { - if (this._isOpen) { - return (<Lightbox - mainSrc={images[this.Document.curPage || 0]} - nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]} - prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]} - onCloseRequest={action(() => - this._isOpen = false - )} - onMovePrevRequest={action(() => - this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length - )} - onMoveNextRequest={action(() => - this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length - )} - />); - } - } - recordAudioAnnotation = () => { let gumStream: any; let recorder: any; let self = this; - navigator.mediaDevices.getUserMedia({ + const extensionDoc = this.extensionDoc; + extensionDoc && navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) { gumStream = stream; @@ -156,13 +101,13 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum body: formData }); const files = await res.json(); - const url = Utils.prepend(files[0]); + const url = Utils.prepend(files[0].path); // upload to server with known URL - let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 }); + let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 }); audioDoc.treeViewExpandedView = "layout"; - let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc)); + let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc)); if (audioAnnos === undefined) { - self.extensionDoc.audioAnnotations = new List([audioDoc]); + extensionDoc.audioAnnotations = new List([audioDoc]); } else { audioAnnos.push(audioDoc); } @@ -179,25 +124,22 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum @undoBatch rotate = action(() => { - let proto = Doc.GetProto(this.props.Document); - let nw = this.props.Document.nativeWidth; - let nh = this.props.Document.nativeHeight; - let w = this.props.Document.width; - let h = this.props.Document.height; - proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360; - proto.nativeWidth = nh; - proto.nativeHeight = nw; - this.props.Document.width = h; - this.props.Document.height = w; + let nw = this.Document.nativeWidth; + let nh = this.Document.nativeHeight; + let w = this.Document.width; + let h = this.Document.height; + this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360; + this.Document.nativeWidth = nh; + this.Document.nativeHeight = nw; + this.Document.width = h; + this.Document.height = w; }); specificContextMenu = (e: React.MouseEvent): void => { - let field = Cast(this.Document[this.props.fieldKey], ImageField); + const field = Cast(this.Document[this.props.fieldKey], ImageField); if (field) { - let url = field.url.href; let funcs: ContextMenuProps[] = []; - funcs.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" }); - funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" }); + funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" }); funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" }); let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers..."); @@ -213,12 +155,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum extractFaces = () => { let converter = (results: any) => { let faceDocs = new List<Doc>(); - results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!)); + results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>()); return faceDocs; }; - if (this.url) { - CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); - } + this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter); } generateMetadata = (threshold: Confidence = Confidence.Excellent) => { @@ -230,36 +170,19 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum let sanitized = tag.name.replace(" ", "_"); tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`); }); - this.extensionDoc.generatedTags = tagsList; + this.extensionDoc && (this.extensionDoc.generatedTags = tagsList); tagDoc.title = "Generated Tags Doc"; tagDoc.confidence = threshold; return tagDoc; }; - if (this.url) { - CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); - } + this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter); } - @action - onDotDown(index: number) { - this.Document.curPage = index; - } @computed private get url() { - let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField); + let data = Cast(this.dataDoc[this.props.fieldKey], ImageField); return data ? data.url.href : undefined; } - dots(paths: string[]) { - let nativeWidth = FieldValue(this.Document.nativeWidth, 1); - let dist = Math.min(nativeWidth / paths.length, 40); - let left = (nativeWidth - paths.length * dist) / 2; - return paths.map((p, i) => - <div className="imageBox-placer" key={i} > - <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> - </div> - ); - } - choosePath(url: URL) { const lower = url.href.toLowerCase(); if (url.protocol === "data") { @@ -290,32 +213,28 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum } _curSuffix = "_m"; - resize(srcpath: string, layoutdoc: Doc) { + resize = (srcpath: string) => { requestImageSize(srcpath) .then((size: any) => { let rotation = NumCast(this.dataDoc.rotation) % 180; let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size; let aspect = realsize.height / realsize.width; - if (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) { + if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) { setTimeout(action(() => { - layoutdoc.height = layoutdoc[WidthSym]() * aspect; - layoutdoc.nativeHeight = realsize.height; - layoutdoc.nativeWidth = realsize.width; + this.Document.height = this.Document[WidthSym]() * aspect; + this.Document.nativeHeight = realsize.height; + this.Document.nativeWidth = realsize.width; }), 0); } }) - .catch((err: any) => { - console.log(err); - }); + .catch((err: any) => console.log(err)); } - @observable _audioState = 0; - @action onPointerEnter = () => { let self = this; - let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations); - if (audioAnnos.length && this._audioState === 0) { + let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations); + if (audioAnnos && audioAnnos.length && this._audioState === 0) { let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; anno.data instanceof AudioField && new Howl({ src: [anno.data.url.href], @@ -329,70 +248,41 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum }); this._audioState = 1; } - // else { - // if (this._audioState === 0) { - // this._audioState = 1; - // new Howl({ - // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"], - // autoplay: true, - // loop: false, - // volume: 0.5, - // onend: function () { - // runInAction(() => self._audioState = 0); - // } - // }); - // } - // } } - @action - audioDown = () => { - this.recordAudioAnnotation(); - } + audioDown = () => this.recordAudioAnnotation(); considerGooglePhotosLink = () => { - const remoteUrl = StrCast(this.props.Document.googlePhotosUrl); - if (remoteUrl) { - return ( - <img - id={"google-photos"} - src={"/assets/google_photos.png"} - style={{ opacity: this.hoverActive ? 1 : 0 }} - onClick={() => window.open(remoteUrl)} - /> - ); - } - return (null); + const remoteUrl = this.Document.googlePhotosUrl; + return !remoteUrl ? (null) : (<img + id={"google-photos"} + src={"/assets/google_photos.png"} + onClick={() => window.open(remoteUrl)} + />); } considerGooglePhotosTags = () => { - const tags = StrCast(this.props.Document.googlePhotosTags); - if (tags) { - return ( - <img - id={"google-tags"} - src={"/assets/google_tags.png"} - /> - ); - } - return (null); + const tags = this.Document.googlePhotosTags; + return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />); } - @computed - get content() { + @computed get content() { + trace(); + const extensionDoc = this.extensionDoc; + if (!extensionDoc) return (null); // let transform = this.props.ScreenToLocalTransform().inverse(); let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; // var [sptX, sptY] = transform.transformPoint(0, 0); // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); // let w = bptX - sptX; - let nativeWidth = FieldValue(this.Document.nativeWidth, pw); - let nativeHeight = FieldValue(this.Document.nativeHeight, 0); - let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; + let nativeWidth = (this.Document.nativeWidth || pw); + let nativeHeight = (this.Document.nativeHeight || 0); + let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")]; // this._curSuffix = ""; // if (w > 20) { - let alts = DocListCast(this.extensionDoc.Alternates); - let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); + let alts = DocListCast(extensionDoc.Alternates); + let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url)); let field = this.dataDoc[this.props.fieldKey]; // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m"; @@ -400,21 +290,17 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum if (field instanceof ImageField) paths = [this.choosePath(field.url)]; paths.push(...altpaths); // } - let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive"; - let rotation = NumCast(this.dataDoc.rotation, 0); - let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1; + let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive"; + let rotation = NumCast(this.Document.rotation, 0); + let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1; let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; - let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)]; + let srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))]; let fadepath = paths[Math.min(paths.length - 1, 1)]; - if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document); + !this.Document.ignoreAspect && this.resize(srcpath); return ( - <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }} - onPointerDown={this.onPointerDown} - onPointerEnter={action(() => this.hoverActive = true)} - onPointerLeave={action(() => this.hoverActive = false)} - onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <div className={`imageBox-cont${interactive}`} key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <div id="cf"> <img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys @@ -431,31 +317,32 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum ref={this._imgRef} onError={this.onError} /></div>} </div> - {paths.length > 1 ? this.dots(paths) : (null)} <div className="imageBox-audioBackground" onPointerDown={this.audioDown} onPointerEnter={this.onPointerEnter} style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }} > <FontAwesomeIcon className="imageBox-audioFont" - style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" /> + style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" /> </div> {this.considerGooglePhotosLink()} - {/* {this.lightbox(paths)} */} - <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> + <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} /> </div>); } + contentFunc = () => [this.content]; render() { - Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey); - return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}> + return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu} + style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} > <CollectionFreeFormView {...this.props} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth} + annotationsKey={this.annotationsKey} + isAnnotationOverlay={true} focus={this.props.focus} isSelected={this.props.isSelected} select={emptyFunction} - active={this.active} + active={this.annotationsActive} ContentScaling={returnOne} whenActiveChanged={this.whenActiveChanged} removeDocument={this.removeDocument} @@ -467,7 +354,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc} chromeCollapsed={true}> - {() => [this.content]} + {this.contentFunc} </CollectionFreeFormView> </div >); } |