diff --git a/client/.gitignore b/client/.gitignore index 4d29575..fc81720 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.vscode \ No newline at end of file diff --git a/client/src/_components/alignment/canvas/default.canvas.alignment.js b/client/src/_components/alignment/canvas/default.canvas.alignment.js index 49c5bd3..cca3aea 100644 --- a/client/src/_components/alignment/canvas/default.canvas.alignment.js +++ b/client/src/_components/alignment/canvas/default.canvas.alignment.js @@ -71,7 +71,7 @@ const Canvas = forwardRef(function Canvas(props, ref) { // put image to canvas context.putImageData(imageData, x || 0, y || 0, 0, 0, w || imageData.width, h || imageData.height); }, - draw: (image, dims) => { + draw: (image, dims, clear=true) => { /** * Redraws image data to canvas @@ -96,7 +96,7 @@ const Canvas = forwardRef(function Canvas(props, ref) { // clear, resize and render image data to canvas const {source, view} = dims || {}; - context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); + if (clear) context.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height); // set canvas dimensions // canvasRef.current.width = view && view.w || canvasRef.current.width; // canvasRef.current.height = view && view.h || canvasRef.current.height; @@ -125,6 +125,12 @@ const Canvas = forwardRef(function Canvas(props, ref) { dims: () => { return {w: canvasRef.current.width, h: canvasRef.current.height}; }, + setWidth: (value) => { + canvasRef.current.width = value; + }, + setHeight: (value) => { + canvasRef.current.height = value; + }, bounds: () => { const rect = canvasRef.current.getBoundingClientRect(); return { diff --git a/client/src/_components/alignment/canvas/magnifier.canvas.alignment.js b/client/src/_components/alignment/canvas/magnifier.canvas.alignment.js index 8aefee2..fe4d25b 100644 --- a/client/src/_components/alignment/canvas/magnifier.canvas.alignment.js +++ b/client/src/_components/alignment/canvas/magnifier.canvas.alignment.js @@ -82,7 +82,6 @@ const MagnifierTool = forwardRef(function Canvas(props, ref) { panel.properties.render_dims ); - // const sourcePt = scalePoint(pos, panel.properties.magnified_dims, panel.properties.render_dims); let sourceX = sourcePt.x - scopeDims.w / 2; let sourceY = sourcePt.y - scopeDims.h / 2; @@ -97,6 +96,7 @@ const MagnifierTool = forwardRef(function Canvas(props, ref) { let viewY = destY; let drawMagImage = true; + // is the mouse over the image? if ( _x <= 0 || diff --git a/client/src/_components/alignment/tools/pointer.alignment.js b/client/src/_components/alignment/tools/pointer.alignment.js index b593ac5..3c08dae 100644 --- a/client/src/_components/alignment/tools/pointer.alignment.js +++ b/client/src/_components/alignment/tools/pointer.alignment.js @@ -55,9 +55,6 @@ export const getPos = (e, properties) => { (document && document.scrollTop || 0) - (document && document.clientTop || 0 ); - // DEBUG - // console.log(pageX, pageY, bounds) - // compute mouse position relative to canvas bounds const x = Math.max( Math.min( @@ -95,11 +92,12 @@ export function usePointer(properties, options) { /** * Set current pointer position coordinate + * * */ - const set = (e) => { + const set = (e, props = null) => { // compute current position of mouse - const pos = getPos(e, properties); + const pos = getPos(e, props ? props : properties); // set absolute client mouse coordinate setClient({x: e.clientX, y: e.clientY}); // set actual diff --git a/client/src/_components/alignment/tools/scaler.alignment.js b/client/src/_components/alignment/tools/scaler.alignment.js index 82af860..503f7f1 100644 --- a/client/src/_components/alignment/tools/scaler.alignment.js +++ b/client/src/_components/alignment/tools/scaler.alignment.js @@ -16,11 +16,13 @@ */ export const scaleToFit = (ix, iy, cx, cy) => { - const ri = ix / iy; - const rc = cx / cy; + // epsilon for stability + const eps = 1e-6; + const ri = ix / (iy + eps); + const rc = cx / (cy + eps); return { - w: Math.round(rc > ri ? ix * cy/iy : cx), - h: Math.round(rc > ri ? cy : iy * cx/ix) + w: Math.round(rc > ri ? ix * cy/(iy + eps) : cx), + h: Math.round(rc > ri ? cy : iy * cx/(ix + eps)) }; }; diff --git a/client/src/_components/common/comparator.js b/client/src/_components/common/comparator.js index 59cd8e9..82a7813 100644 --- a/client/src/_components/common/comparator.js +++ b/client/src/_components/common/comparator.js @@ -77,36 +77,47 @@ const ImagePair = ({ const Comparator = ({ images = [], - menu = true, autoslide=null, expandable=true }) => { + // slide panel reference + const slidePanel = React.useRef(); // selected slide state const [selectedIndex, setSelectedIndex] = React.useState(0); - const [pairToggle, setPairToggle] = React.useState(false); - const [viewerType, setViewerType] = React.useState('overlay'); - const [expandImage, setExpandImage] = React.useState(false); - const [panelWidth, setPanelWidth] = React.useState(0); - const [panelHeight, setPanelHeight] = React.useState(0); - let selectedImage = images[selectedIndex]; - const slidePanel = React.useRef(); + // const [expandImage, setExpandImage] = React.useState(false); + let selectedPair = images[selectedIndex]; + + // retrieve captures from pair + const { historic_captures={}, modern_captures={} } = selectedPair || {}; // get router const router = useRouter(); - // window dimensions - const [winWidth, winHeight] = useWindowSize(); + // extract metadata from capture object + const getMetadata = (capture) => { + const { refImage={} } = capture || {}; + const { label='', file={}, url='', title='' } = refImage; + const { file_type='', owner_id='', owner_type='' } = file; + return { + label : label || '', + title: title || '', + url: createNodeRoute(owner_type, 'show', owner_id) + } + } + const modernMetadata = getMetadata(modern_captures); + const historicMetadata = getMetadata(historic_captures); - // retrieve image metadata - const { historic_captures={}, modern_captures={} } = selectedImage || {}; - const selectedCapture = pairToggle ? historic_captures : modern_captures; - const { refImage={} } = selectedCapture || {}; - const { label='', file={}, url='', title='' } = refImage; - const { file_type='', owner_id='', owner_type='' } = file; - // create view link for selected image - const link = createNodeRoute(owner_type, 'show', owner_id) + // select image pair + React.useEffect(() => { + const timer = autoslide ? setTimeout(() => { + setSelectedIndex((selectedIndex + 1) % images.length); + }, autoslide) : null; + return () => { + clearTimeout(timer); + }; + }, [selectedIndex]); // auto-increment slideshow React.useEffect(() => { @@ -118,13 +129,6 @@ const Comparator = ({ }; }, [selectedIndex, images.length, setSelectedIndex, autoslide]); - // panel dimensions - React.useEffect(() => { - if (slidePanel.current) { - setPanelWidth(slidePanel.current.offsetWidth); - setPanelHeight(slidePanel.current.offsetHeight); - } - }, [winWidth, winHeight]); // increment/decrement index to make slide visible const prevPair = () => { @@ -136,97 +140,52 @@ const Comparator = ({ setSelectedIndex(nextIndex); }; - const getViewer = function() { - const viewers = { - slider: () => { - return - }, - default: () => { - return <> -
- -
-
- -
- - } - } - return viewers.hasOwnProperty(viewerType) ? viewers[viewerType]() : viewers.default(); - } - return (
- { images.length > 0 ? getViewer() : } + { + images.length > 0 ? + + : + + }
{ selectedIndex + 1 }/{images.length}
- { - expandable && viewerType === 'overlay' && + {/* { + expandable &&
- } + } */}
{ - label &&
+
  • +
  • Historic:
  • -
  • - { - viewerType === 'overlay' &&
  • - } +
  • Modern:
  • -
  • {label}
} - { - menu - ?
    { (images || []).map((imgPair, index) => { const { historic_captures={}, modern_captures={} } = imgPair || {}; - return (
- : -
- { - (images || []).map((image, index) => { - return ( - {setSelectedIndex(index)}} - /> - ); - }) - } -
- } - { - expandImage && - {setExpandImage(null)}} - > - - - }
); }; diff --git a/client/src/_components/common/icon.js b/client/src/_components/common/icon.js index ef2d597..c433950 100644 --- a/client/src/_components/common/icon.js +++ b/client/src/_components/common/icon.js @@ -10,7 +10,6 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { library } from '@fortawesome/fontawesome-svg-core'; import { - faMapMarker, faVectorSquare, faLayerGroup, @@ -289,12 +288,15 @@ const getIconClass = (iconType) => { externalLink: 'external-link-square-alt', align: 'crosshairs', overlay: 'layer-group', + slider: 'columns', + sideBySide: 'images', clustered: 'map-marker', boundaries: 'vector-square', filter: 'filter', filterNavigation: 'filter', list: 'list-alt', search: 'search', + magnify: 'search-plus', undo: 'undo', reset: 'undo', sync: 'sync', diff --git a/client/src/_components/common/slider.js b/client/src/_components/common/slider.js index 5fedef2..2817b68 100644 --- a/client/src/_components/common/slider.js +++ b/client/src/_components/common/slider.js @@ -13,17 +13,21 @@ * --------- * Revisions * - 14-07-2023 Redo of slider canvases. + * - 03-08-2024 Update slider to include image swap controls; centred view. */ -import React, {useLayoutEffect, useRef, useState} from 'react'; +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { schema } from '../../schema'; import Loading from './loading'; import { UserMessage } from './message'; -import {scaleToFit} from '../alignment/tools/scaler.alignment'; +import { scaleToFit } from '../alignment/tools/scaler.alignment'; import InputSelector from "../selectors/input.selector"; +import Button from './button'; import Canvas from "../alignment/canvas/default.canvas.alignment"; import styles from '../styles/slider.module.css'; -import {useWindowSize} from "../../_utils/events.utils.client"; +import { useWindowSize } from "../../_utils/events.utils.client"; +import {usePointer} from "../alignment/tools/pointer.alignment"; +import MagnifierTool from '../alignment/canvas/magnifier.canvas.alignment'; /** * Image slider component. @@ -32,10 +36,12 @@ import {useWindowSize} from "../../_utils/events.utils.client"; * @return */ -const Slider = ({ images = []}) => { +const Slider = ({ images = [] }) => { - const canvasWidth = 800; - const canvasHeight = 600; + // default canvas sizes and magnification factor + const DEFAULT_CANVAS_WIDTH = 900; + const DEFAULT_CANVAS_HEIGHT = 900; + const MAGNIFICATION = 2.0; // window dimensions const [winWidth, winHeight] = useWindowSize(); @@ -43,35 +49,266 @@ const Slider = ({ images = []}) => { // input image data const [image1, image2] = images || []; - // mounted status - const _isMounted = React.useRef(false); - // image data layers const renderLayer1 = useRef(null); const renderLayer2 = useRef(null); const imageLayer1 = useRef(null); const imageLayer2 = useRef(null); - const controlLayer = useRef(); + const defaultControlLayer = useRef(); + const sliderControlLayer = useRef(); + const magControlLayer = useRef(null); + const magnifiedImage1 = useRef(null); + const magnifiedImage2 = useRef(null); + const magnifierLayer = useRef(null); // create reference to panel resizer let sliding = false; let slideWidth = 0; let slideInit = 0; + const [canvasWidth, setCanvasWidth] = React.useState(DEFAULT_CANVAS_WIDTH); + const [canvasHeight, setCanvasHeight] = React.useState(DEFAULT_CANVAS_HEIGHT); + const [status1, setStatus1] = React.useState('empty'); const [status2, setStatus2] = React.useState('empty'); const [message, setMessage] = React.useState('empty'); const [viewWidth, setViewWidth] = React.useState(0); const [viewDims, setViewDims] = React.useState(null); + const [viewMode, setViewMode] = React.useState(null); + const [toggle, setToggle] = React.useState(false); + const [properties, setProperties] = React.useState({ + image_dims: {w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT}, + render_dims: {w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT}, + base_dims: {w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT}, + magnified_dims: {w: MAGNIFICATION*DEFAULT_CANVAS_WIDTH, h: MAGNIFICATION*DEFAULT_CANVAS_HEIGHT}, + bounds: {w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT} + }) // Image labels - const label1 = image1 && image1.hasOwnProperty('label') ? image1.label : 'Image 1'; - const label2 = image2 && image2.hasOwnProperty('label') ? image2.label : 'Image 2'; + const label1 = image1 && image1.hasOwnProperty('label') ? image1.label : 'Historic Image'; + const label2 = image2 && image2.hasOwnProperty('label') ? image2.label : 'Modern Image'; // top layer opacity state const [opacity, setOpacity] = useState(100); + // initialize canvas pointers + const pointer = usePointer(properties); + + /** + * Render image 1 data on canvas layer 1 + * + * @private + */ + + const _drawImage1 = () => { + + if (!imageLayer1.current || !imageLayer2.current) return; + + // compute scaled dimensions for image 1 to fit view canvas + const viewDims1 = scaleToFit( + imageLayer1.current.width, + imageLayer1.current.height, + DEFAULT_CANVAS_WIDTH, + DEFAULT_CANVAS_HEIGHT, + ); + + // compute scaled dimensions for image 2 to fit view canvas + const viewDims2 = scaleToFit( + imageLayer2.current.width, + imageLayer2.current.height, + DEFAULT_CANVAS_WIDTH, + DEFAULT_CANVAS_HEIGHT, + ); + + // Render image to canvas by view mode + if (viewMode === 'slider') { + // set canvas width to default; + setCanvasWidth(DEFAULT_CANVAS_WIDTH); + renderLayer1.current.setWidth(DEFAULT_CANVAS_WIDTH); + // compute image x offset + const _viewOffset1 = (DEFAULT_CANVAS_WIDTH - viewDims1.w) / 2; + renderLayer1.current.draw(imageLayer1.current, { + view: { x: _viewOffset1, y: 0, w: toggle ? viewDims1.w / 2 : viewDims1.w, h: viewDims1.h }, + source: { x: 0, y: 0, w: toggle ? imageLayer1.current.width / 2 : imageLayer1.current.width, h: imageLayer1.current.height } + }); + // draw magnified image to canvas + magnifiedImage1.current.setWidth(MAGNIFICATION * viewDims1.w); + magnifiedImage1.current.setHeight(MAGNIFICATION * viewDims1.h); + magnifiedImage1.current.draw(imageLayer1.current, { + view: { x: 0, y: 0, w: MAGNIFICATION * viewDims1.w, h: MAGNIFICATION * viewDims1.h }, + source: { x: 0, y: 0, w: imageLayer1.current.width, h: imageLayer1.current.height } + }); + // update properties + setProperties(prevState => ({...prevState, + bounds: renderLayer1.current.bounds(), + base_dims: { w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT }, + render_dims: { x: _viewOffset1, y: 0, w: viewDims1.w, h: viewDims1.h }, + magnified_dims: { x: 0, y: 0, w: MAGNIFICATION * viewDims1.w, h: MAGNIFICATION * viewDims1.h } + })); + + } + else if (viewMode === 'double') { + // set canvas width to double + const _combinedWidth = viewDims1.w + viewDims2.w; + setCanvasWidth(_combinedWidth); + // draw image to render canvas + renderLayer1.current.setWidth(_combinedWidth); + renderLayer1.current.draw(imageLayer1.current, { + view: { x: toggle ? 0 : viewDims1.w, y: 0, w: viewDims1.w, h: viewDims1.h }, + source: { x: 0, y: 0, w: imageLayer1.current.width, h: imageLayer1.current.height } + }, false); + // draw magnified image to canvas + magnifiedImage1.current.setWidth(MAGNIFICATION * _combinedWidth); + magnifiedImage1.current.setHeight(MAGNIFICATION * viewDims1.h); + magnifiedImage1.current.draw(imageLayer1.current, { + view: { x: toggle ? 0 : MAGNIFICATION * viewDims2.w, y: 0, w: MAGNIFICATION * viewDims1.w, h: MAGNIFICATION * viewDims1.h }, + source: { x: 0, y: 0, w: imageLayer1.current.width, h: imageLayer1.current.height } + }, false); + // update properties + setProperties(prevState => ({...prevState, + bounds: renderLayer1.current.bounds(), + base_dims: { w: _combinedWidth, h: viewDims1.h }, + render_dims: { x: 0, y: 0, w: _combinedWidth, h: viewDims1.h }, + magnified_dims: { x: 0, y: 0, w: MAGNIFICATION * _combinedWidth, h: MAGNIFICATION * viewDims1.h } + })); + } + else { + // set canvas width to default + setCanvasWidth(DEFAULT_CANVAS_WIDTH); + renderLayer1.current.setWidth(DEFAULT_CANVAS_WIDTH); + // compute image x offset + const _viewOffset1 = (DEFAULT_CANVAS_WIDTH - viewDims1.w) / 2; + // draw image to render canvas + renderLayer1.current.draw(imageLayer1.current, { + view: { x: _viewOffset1, y: 0, w: viewDims1.w, h: viewDims1.h }, + source: { x: 0, y: 0, w: imageLayer1.current.width, h: imageLayer1.current.height } + }); + // draw magnified image to canvas + magnifiedImage1.current.setWidth(MAGNIFICATION * viewDims1.w); + magnifiedImage1.current.setHeight(MAGNIFICATION * viewDims1.h); + magnifiedImage1.current.draw(imageLayer1.current, { + view: { x: 0, y: 0, w: MAGNIFICATION * viewDims1.w, h: MAGNIFICATION * viewDims1.h }, + source: { x: 0, y: 0, w: imageLayer1.current.width, h: imageLayer1.current.height } + }); + // update properties + setProperties(prevState => ({...prevState, + bounds: renderLayer1.current.bounds(), + base_dims: { w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT }, + render_dims: { x: _viewOffset1, y: 0, w: viewDims1.w, h: viewDims1.h }, + magnified_dims: { x: 0, y: 0, w: MAGNIFICATION * viewDims1.w, h: MAGNIFICATION * viewDims1.h } + })); + } + + + // reset canvas height + setCanvasHeight(viewDims1.h); + + // set initial view dims + if (toggle) { + setViewWidth(viewMode === 'slider' ? viewDims1.w / 2 : viewDims1.w); + setViewDims(viewDims1); + } + } + + /** + * Render image 2 data on canvas layer 2 + * + * @private + */ + + const _drawImage2 = () => { + + if (!imageLayer2.current || !imageLayer1.current) return; + + // compute scaled dimensions for image 1 to fit view canvas + const viewDims1 = scaleToFit( + imageLayer1.current.width, + imageLayer1.current.height, + DEFAULT_CANVAS_WIDTH, + DEFAULT_CANVAS_HEIGHT, + ); + + // compute scaled dimensions for image 2 to fit view canvas + const viewDims2 = scaleToFit( + imageLayer2.current.width, + imageLayer2.current.height, + DEFAULT_CANVAS_WIDTH, + DEFAULT_CANVAS_HEIGHT, + ); + + // Render image to canvas based on view mode + if (viewMode === 'slider') { + // set canvas width to default + setCanvasWidth(DEFAULT_CANVAS_WIDTH); + renderLayer2.current.setWidth(DEFAULT_CANVAS_WIDTH); + // compute image x offset + const _viewOffset2 = (DEFAULT_CANVAS_WIDTH - viewDims1.w) / 2; + // render image + renderLayer2.current.draw(imageLayer2.current, { + view: { x: _viewOffset2, y: 0, w: !toggle ? viewDims2.w / 2 : viewDims2.w, h: viewDims2.h }, + source: { x: 0, y: 0, w: !toggle ? imageLayer2.current.width / 2 : imageLayer2.current.width, h: imageLayer2.current.height } + }); + // draw magnified image to canvas + magnifiedImage2.current.setWidth(MAGNIFICATION * viewDims2.w); + magnifiedImage2.current.setHeight(MAGNIFICATION * viewDims2.h); + magnifiedImage2.current.draw(imageLayer2.current, { + view: { x: 0, y: 0, w: MAGNIFICATION * viewDims2.w, h: MAGNIFICATION * viewDims2.h }, + source: { x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height } + }); + } + else if (viewMode === 'double') { + // clear canvas 2 (image 2) + renderLayer2.current.clear(); + // draw image 2 to canvas 1 with offset + renderLayer1.current.draw(imageLayer2.current, { + view: { x: toggle ? viewDims1.w : 0, y: 0, w: viewDims2.w, h: viewDims2.h }, + source: { x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height } + }, false); + // clear magnified canvas 2 (image 2) + magnifiedImage2.current.clear(); + // draw magnified image 2 to canvas 1 + magnifiedImage1.current.draw(imageLayer2.current, { + view: { x: toggle ? MAGNIFICATION * viewDims1.w : 0, y: 0, w: MAGNIFICATION * viewDims2.w, h: MAGNIFICATION * viewDims2.h }, + source: { x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height } + }, false); + } + else { + // set canvas width to default + setCanvasWidth(DEFAULT_CANVAS_WIDTH); + renderLayer2.current.setWidth(DEFAULT_CANVAS_WIDTH); + // compute image x offset + const _viewOffset2 = (DEFAULT_CANVAS_WIDTH - viewDims1.w) / 2; + renderLayer2.current.draw(imageLayer2.current, { + view: { x: _viewOffset2, y: 0, w: viewDims2.w, h: viewDims2.h }, + source: { x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height } + }); + // draw magnified image to canvas + magnifiedImage2.current.setWidth(MAGNIFICATION * viewDims2.w); + magnifiedImage2.current.setHeight(MAGNIFICATION * viewDims2.h); + magnifiedImage2.current.draw(imageLayer2.current, { + view: { x: 0, y: 0, w: MAGNIFICATION * viewDims2.w, h: MAGNIFICATION * viewDims2.h }, + source: { x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height } + }); + // update properties + setProperties(prevState => ({...prevState, + bounds: renderLayer2.current.bounds(), + base_dims: { w: DEFAULT_CANVAS_WIDTH, h: DEFAULT_CANVAS_HEIGHT }, + render_dims: { x: _viewOffset2, y: 0, w: viewDims2.w, h: viewDims2.h }, + magnified_dims: { x: 0, y: 0, w: MAGNIFICATION * viewDims2.w, h: MAGNIFICATION * viewDims2.h } + })); + } + + // reset canvas height + setCanvasHeight(viewDims2.h); + + // set initial view dims + if (!toggle) { + setViewWidth(viewMode === 'slider' ? viewDims2.w / 2 : viewDims2.w); + setViewDims(viewDims2); + } + } + /** * Load image data in comparator panel @@ -79,68 +316,47 @@ const Slider = ({ images = []}) => { * @private */ - const load = () => { + const _load = () => { // status = image loading has started setStatus1('loading'); setStatus2('loading'); + // reset canvas size + setCanvasWidth(DEFAULT_CANVAS_WIDTH); + setCanvasHeight(DEFAULT_CANVAS_HEIGHT); + // set data urls for images const url1 = image1 && image1.hasOwnProperty('url') ? image1.url.medium : image1; const url2 = image2 && image2.hasOwnProperty('url') ? image2.url.medium : image2; - // set image sources + // set view image sources imageLayer1.current.src = url1; imageLayer2.current.src = url2; - // load image 1 (overlay) + // load image 1 + imageLayer1.current.onload = function () { + // load image data to canvas layer + _drawImage1(); + // update load status + setStatus1('loaded'); + }; imageLayer1.current.onerror = () => { setMessage({ msg: 'Error: Image could not be loaded.', type: 'error' }); imageLayer1.current.src = schema.errors.image.fallbackSrc; }; - imageLayer1.current.onload = function() { - - // compute scaled dimensions to fit view canvas - const viewDims1 = scaleToFit(imageLayer1.current.width, imageLayer1.current.height, canvasWidth, canvasHeight); - - // load data into canvas layer - // - initially show half of top layer image - const initWidth = viewDims1.w / 2; - renderLayer1.current.draw(imageLayer1.current, { - view: {x: 0, y: 0, w: viewDims1.w / 2, h: viewDims1.h}, - source: {x: 0, y: 0, w: imageLayer1.current.width / 2, h: imageLayer1.current.height} - }); - - // set initial view dims - setViewWidth(initWidth); - setViewDims(viewDims1); - + + // load image 2 + imageLayer2.current.onload = function () { + // load image data to canvas layer + _drawImage2(); // update load status - setStatus1('loaded'); - + setStatus2('loaded'); }; - - // load image 2 (underlay) imageLayer2.current.onerror = () => { setMessage({ msg: 'Error: Image could not be loaded.', type: 'error' }); imageLayer2.current.src = schema.errors.image.fallbackSrc; }; - imageLayer2.current.onload = function() { - // compute scaled dimensions to fit view canvas - const viewDims2 = scaleToFit( - imageLayer2.current.width, - imageLayer2.current.height, - canvasWidth, - canvasHeight, - ); - - // load data into canvas layer - renderLayer2.current.draw(imageLayer2.current, { - view: {x: 0, y: 0, w: viewDims2.w, h: viewDims2.h}, - source: {x: 0, y: 0, w: imageLayer2.current.width, h: imageLayer2.current.height} - }); - }; - setStatus2('loaded'); } /** @@ -154,7 +370,29 @@ const Slider = ({ images = []}) => { const { value = 100 } = target; setOpacity(value); // set canvas layer alpha value for opacity - renderLayer1.current.alpha( value / 100 ); + toggle ? renderLayer1.current.alpha(value / 100) : renderLayer2.current.alpha(value / 100); + } + + /** + * reset opacity on both layers + * + * @private + */ + + const _resetOpacity = () => { + setOpacity(100); + renderLayer1.current.alpha(100); + renderLayer2.current.alpha(100); + } + + /** + * toggle image magnifier tool + * + * @private + */ + + const _toggleMagnifier = () => { + pointer.magnify ? pointer.magnifyOff() : pointer.magnifyOn(); } /** @@ -165,19 +403,71 @@ const Slider = ({ images = []}) => { * @private */ - useLayoutEffect(()=>{ - if (status1 === 'empty' || status2 === 'empty') { - load(); - } - return ()=>{_isMounted.current = true;} - + useLayoutEffect(() => { + if (status1 === 'empty' || status2 === 'empty') _load(); }, []); - useLayoutEffect(()=>{ - load(); - return ()=>{_isMounted.current = true;} + /** + * Reload images on image source and window size change. + * + * @private + */ + + useLayoutEffect(() => { + _load(); + }, [winWidth, winHeight, images]); - }, [winWidth, winHeight]); + + /** + * Redraw image to canvas. + * + * @private + */ + + useLayoutEffect(() => { + _drawImage1(); + _drawImage2(); + _resetOpacity(); + }, [viewMode, toggle]); + + /** + * Apply magnification to image. + * + * @private + */ + + useEffect(() => { + + // compare current with previously set pointer bounds + const _compareBounds = (bounds1, bounds2) => { + return bounds1.top === bounds2.top + && bounds1.left === bounds2.left + && bounds1.width === bounds2.width + && bounds1.height === bounds2.height; + } + + // get current canvas bounds + const bounds = toggle || viewMode === 'double' ? renderLayer1.current.bounds() : renderLayer2.current.bounds(); + + // apply magnification if enabled + if (pointer.magnify) { + // get current magnified image + const _magnifiedImage = toggle || viewMode === 'double' ? magnifiedImage1.current.canvas() : magnifiedImage2.current.canvas(); + + // check pointer bounds to detect canvas translation (e.g., scrolling) + if(_compareBounds(bounds, properties.bounds)) { + magnifierLayer.current.magnify(_magnifiedImage, {pointer, properties}); + } + else { + // update pointer bounds (update properties state) + setProperties(prevState => ({...prevState, bounds: bounds })); + magnifierLayer.current.magnify(_magnifiedImage, {pointer, properties: {...properties, bounds: bounds}}); + } + } + else { + magnifierLayer.current.clear(); + } + }, [pointer.x, pointer.y, pointer.magnify, viewMode]); /* Initialize panel resize */ function _resizeStart(e) { @@ -199,18 +489,27 @@ const Slider = ({ images = []}) => { /* Position the slider and resize panel */ function _resize(e) { + /* if slider is no longer engaged, exit this function: */ if (!sliding) return false; // compute adjusted widths const _viewWidth = viewWidth - Math.round(slideInit - e.pageX); - const _imageWidth = Math.round(_viewWidth * ( imageLayer1.current.width / viewDims.w)); + const _imageFullWidth = toggle ? imageLayer1.current.width : imageLayer2.current.width; + const _imageWidth = Math.round(_viewWidth * (_imageFullWidth / viewDims.w)); + const _viewOffset = (canvasWidth - viewDims.w) / 2; // load data into canvas layer // - initially show half of top layer image + toggle ? renderLayer1.current.draw(imageLayer1.current, { - view: {x: 0, y: 0, w: _viewWidth, h: viewDims.h}, - source: {x: 0, y: 0, w: _imageWidth, h: imageLayer1.current.height} + view: { x: _viewOffset, y: 0, w: _viewWidth, h: viewDims.h }, + source: { x: 0, y: 0, w: _imageWidth, h: imageLayer1.current.height } + }) + : + renderLayer2.current.draw(imageLayer2.current, { + view: { x: _viewOffset, y: 0, w: _viewWidth, h: viewDims.h }, + source: { x: 0, y: 0, w: _imageWidth, h: imageLayer2.current.height } }); // set slide width @@ -218,37 +517,90 @@ const Slider = ({ images = []}) => { } + /* Show magnifier viewer at pointer position */ + function _setPointer(e) { + e.preventDefault(); + pointer.set(e, properties); + } + return <>
- { ( status1 !== 'loaded' || status2 !== 'loaded' ) && } - { message && } -
- } + {message && } +
+ { + pointer.magnify && + } + { + viewMode === 'slider' && + } + + + + { alt={label2} />
- +
+
    +
  • +
  • +
  • +
  • +
  • +
  • + +
  • +
+
+
; diff --git a/client/src/_components/styles/slider.module.css b/client/src/_components/styles/slider.module.css index 7b1aadc..9ba4cb2 100644 --- a/client/src/_components/styles/slider.module.css +++ b/client/src/_components/styles/slider.module.css @@ -4,6 +4,7 @@ min-height: 500px; width: auto; background-color: transparent; + cursor: default; } .container { position: relative; @@ -11,6 +12,7 @@ background-color: transparent; margin: auto; height: auto; /*should be the same height as the images*/ + /* overflow-x: scroll; */ } .slider figure { margin: 0; @@ -27,15 +29,17 @@ position: absolute; z-index: 30; background-color: transparent; - cursor: col-resize; + cursor: inherit; } .image1 { margin: auto; position: absolute; z-index: 20; + cursor: default; } .image2 { margin: auto; position: absolute; z-index: 10; + cursor: default; } \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css index 5b129a2..8c2d01f 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1079,6 +1079,10 @@ nav.breadcrumb ul li a:hover { background-color: #444444; color: #EEEEEE; } +.capture-button.active { + background-color: coral; + color: #EEEEEE; +} .comparator .prev, .comparator .next, .carousel .prev, diff --git a/src/controllers/metadata.controller.js b/src/controllers/metadata.controller.js index fd096a3..4d76ecb 100644 --- a/src/controllers/metadata.controller.js +++ b/src/controllers/metadata.controller.js @@ -198,8 +198,6 @@ export default function MetadataController(metadataType) { const ownerID = this.getOwnerId(req); let ownerType = null; - console.log(req.body) - // get owner ID from parameters if (ownerID) { // get owner metadata record (if exists) diff --git a/src/services/files.services.js b/src/services/files.services.js index 5174435..a448747 100644 --- a/src/services/files.services.js +++ b/src/services/files.services.js @@ -637,7 +637,6 @@ export const compress = async (files={}, version) => { export const getFilePath = (file, version='medium' ) => { const { fs_path = '', secure_token = '', file_type='' } = file || {}; - console.log(file, '****') const lowResPath = process.env.LOWRES_PATH; const defaultPath = process.env.UPLOAD_DIR; diff --git a/src/services/import.services.js b/src/services/import.services.js index 5cb7b03..950cf5c 100644 --- a/src/services/import.services.js +++ b/src/services/import.services.js @@ -50,37 +50,6 @@ export const receive = (req, owner_id=null, owner_type=null) => { // close request pipeline req.on('close', cleanup); - // initialize busboy - busboy - .on('field', onField.bind(null, metadata.data)) - .on('file', onFile.bind(null, filePromises, metadata, onError)) - .on('error', onError) - .on('end', onEnd) - .on('finish', onEnd); - - busboy.on('partsLimit', function() { - const err = new Error('Reach parts limit'); - err.code = 'Request_parts_limit'; - err.status = 413; - onError(err); - }); - - busboy.on('filesLimit', () => { - const err = new Error('Reach files limit'); - err.code = 'Request_files_limit'; - err.status = 413; - onError(err); - }); - - busboy.on('fieldsLimit', () => { - const err = new Error('Reach fields limit'); - err.code = 'Request_fields_limit'; - err.status = 413; - onError(err); - }); - - req.pipe(busboy); - function onError(err) { console.error(err); cleanup(); @@ -92,6 +61,7 @@ export const receive = (req, owner_id=null, owner_type=null) => { console.error(err); return reject(err); } + Promise.all(filePromises) .then(() => { cleanup(); @@ -105,14 +75,46 @@ export const receive = (req, owner_id=null, owner_type=null) => { function cleanup() { busboy.removeListener('field', onField); busboy.removeListener('file', onFile); - busboy.removeListener('close', cleanup); + busboy.removeListener('close', onEnd); busboy.removeListener('end', cleanup); busboy.removeListener('error', onEnd); busboy.removeListener('partsLimit', onEnd); busboy.removeListener('filesLimit', onEnd); busboy.removeListener('fieldsLimit', onEnd); - busboy.removeListener('finish', onEnd); } + + + // initialize busboy + busboy + .on('field', onField.bind(null, metadata.data)) + .on('file', onFile.bind(null, filePromises, metadata, onError)) + .on('error', onError) + .on('end', onEnd) + .on('close', onEnd); + + busboy.on('partsLimit', function() { + const err = new Error('Reach parts limit'); + err.code = 'Request_parts_limit'; + err.status = 413; + onError(err); + }); + + busboy.on('filesLimit', () => { + const err = new Error('Reach files limit'); + err.code = 'Request_files_limit'; + err.status = 413; + onError(err); + }); + + busboy.on('fieldsLimit', () => { + const err = new Error('Reach fields limit'); + err.code = 'Request_fields_limit'; + err.status = 413; + onError(err); + }); + + req.pipe(busboy); + }); } @@ -183,7 +185,7 @@ export const onFile = (filePromises, metadata, onError, fieldname, file, filenam .on('open', () => file .pipe(writeStream) .on('error', reject) - .on('finish', () => { + .on('close', () => { // attach file data to global metadata // - for multiple files on same type, use key index (e.g. 'file_type[2]') // - for multiple files on different type, use file index diff --git a/src/services/model.services.js b/src/services/model.services.js index 3cddc64..9a731e2 100644 --- a/src/services/model.services.js +++ b/src/services/model.services.js @@ -220,7 +220,6 @@ export default function ModelServices(model) { // [3] process model data query (if provided) if (stmts.model) { const { sql, data } = stmts.model(item); - console.log(sql) res = await client.query(sql, data); }