diff --git a/.idea/dictionaries/awilkey.xml b/.idea/dictionaries/awilkey.xml
new file mode 100644
index 00000000..6912849b
--- /dev/null
+++ b/.idea/dictionaries/awilkey.xml
@@ -0,0 +1,80 @@
+
${names.join('\n')}
`; + m.redraw(); + } else if (this.info.display !== 'none') { + this.info.display = 'none'; + m.redraw(); + } + + return true; + } + + /** + * Handle start of pan events on canvas, box zooms if ruler is part of selected or + * mass select if not. + * + * @param evt - tap event + * @returns {boolean} return true to stop event prorogation further down layers + * @private + */ + + _onPanStart(evt) { + // TODO: send pan events to the scenegraph elements which compose the biomap + // (don't scale the canvas element itself) + this.zoomP = { + start: 0, + end: 0, + pStart: true, + ruler: false, + delta: 0, + corner: 0 + }; + this.zoomP.pStart = true; + console.warn('BioMap -> onPanStart -- vertically; implement me', evt); + let globalPos = pageToCanvas(evt, this.canvas); + let left = this.ruler.globalBounds.left; + // scroll view vs box select + if (left < (globalPos.x - evt.deltaX) && + (globalPos.x - evt.deltaX) < (left + this.ruler.bounds.width)) { + this.zoomP.ruler = true; + this._moveRuler(evt); + } else { + this.zoomP.ruler = false; + this.zoomP.start = this._pixelToCoordinate(globalPos.y - this.ruler.globalBounds.top - evt.deltaY); + if (this.zoomP.start < this.model.view.base.start) { + this.zoomP.start = this.model.view.base.start; + } + let ctx = this.context2d; + this.zoomP.corner = {top: globalPos.y - evt.deltaY, left: globalPos.x - evt.deltaX}; + ctx.lineWidth = 1.0; + ctx.strokeStyle = 'black'; + // noinspection JSSuspiciousNameCombination + ctx.strokeRect( + Math.floor(globalPos.x - evt.deltaX), + Math.floor(globalPos.y - evt.deltaY), + Math.floor(evt.deltaX), + Math.floor(evt.deltaY) + ); + } + return true; + } + + /** + * Pans ruler's position box on click-and-pan + * + * @param evt - pan event + * @private + */ + + _moveRuler(evt) { + if (this.model.config.invert) { + evt.deltaY = -evt.deltaY; + } + + let delta = (evt.deltaY - this.zoomP.delta) / this.model.view.pixelScaleFactor; + if (this.model.view.visible.start + delta < this.model.view.base.start) { + delta = this.model.view.base.start - this.model.view.visible.start; + } else if (this.model.view.visible.stop + delta > this.model.view.base.stop) { + delta = this.model.view.base.stop - this.model.view.visible.stop; + } + this.model.view.visible.start += delta; + this.model.view.visible.stop += delta; + this._redrawViewport({start: this.model.view.visible.start, stop: this.model.view.visible.stop}); + this.zoomP.delta = evt.deltaY; + } + + /** + * Continue pre-exiting pan event + * + * @param evt - continuation of pan event + * @returns {boolean} return true to stop event prorogation further down layers + * @private + */ + + _onPan(evt) { + // block propagation if pan hasn't started + if (!this.zoomP || !this.zoomP.pStart) return true; + if (this.zoomP && this.zoomP.ruler) { + this._moveRuler(evt); + } else { + let globalPos = pageToCanvas(evt, this.canvas); + this.draw(); + let ctx = this.context2d; + ctx.lineWidth = 1.0; + ctx.strokeStyle = 'black'; + // noinspection JSSuspiciousNameCombination + ctx.strokeRect( + Math.floor(this.zoomP.corner.left), + Math.floor(this.zoomP.corner.top), + Math.floor(globalPos.x - this.zoomP.corner.left), + Math.floor(globalPos.y - this.zoomP.corner.top) + ); + } + return true; + } + + /** + * Finalize pan event + * + * @param evt - tap event + * @returns {boolean} return true to stop event propagation further down layers + * @private + */ + + _onPanEnd(evt) { + // TODO: send pan events to the scenegraph elements which compose the biomap + // (don't scale the canvas element itself) + console.warn('BioMap -> onPanEnd -- vertically; implement me', evt, this.model.view.base); + // block propagation if pan hasn't started + if (!this.zoomP || !this.zoomP.pStart) return true; + if (this.zoomP && this.zoomP.ruler) { + this._moveRuler(evt); + } else { + let globalPos = pageToCanvas(evt, this.canvas); + + // test if any part of the box select is in the ruler zone + let rLeft = this.ruler.globalBounds.left; + let rRight = this.ruler.globalBounds.right; + let lCorner = this.zoomP.corner.left < globalPos.x ? this.zoomP.corner.left : globalPos.x; + let rCorner = lCorner === this.zoomP.corner.left ? globalPos.x : this.zoomP.corner.left; + // if zoom rectangle contains the ruler, zoom, else populate popover + if (((lCorner <= rLeft) && (rCorner >= rLeft)) || ((lCorner <= rRight && rCorner >= rRight))) { + this.model.view.visible = this.model.view.base; + + this.zoomP.start = this._pixelToCoordinate(this.zoomP.corner.top - this.ruler.globalBounds.top); + this.zoomP.stop = this._pixelToCoordinate(globalPos.y - this.ruler.globalBounds.top); + let swap = this.zoomP.start < this.zoomP.stop; + let zStart = swap ? this.zoomP.start : this.zoomP.stop; + let zStop = swap ? this.zoomP.stop : this.zoomP.start; + + if (zStart < this.model.view.base.start) { + zStart = this.model.view.base.start; + } + if (zStop > this.model.view.base.stop) { + zStop = this.model.view.base.stop; + } + + this._redrawViewport({start: zStart, stop: zStop}); + } else { + + this._loadHitMap(); + let hits = []; + let swap = this.zoomP.corner.left < globalPos.x; + let swapV = this.zoomP.corner.top < globalPos.y; + this.hitMap.search({ + minX: swap ? this.zoomP.corner.left : globalPos.x, + maxX: swap ? globalPos.x : this.zoomP.corner.left, + minY: swapV ? this.zoomP.corner.top : globalPos.y, + maxY: swapV ? globalPos.y : this.zoomP.corner.top + }).forEach(hit => { + // temp fix, find why hit map stopped updating properly + if(!hit.data.model) return; + if ((hit.data.model.coordinates.start >= this.model.view.visible.start) && + (hit.data.model.coordinates.start <= this.model.view.visible.stop)) { + hits.push(hit.data); + } else if ((hit.data.model.coordinates.stop >= this.model.view.visible.start) && + (hit.data.model.coordinates.stop <= this.model.view.visible.stop)) { + hits.push(hit.data); + } + }); + if (hits.length > 0) { + hits.sort((a, b) => { + return a.model.coordinates.start - b.model.coordinates.start; + }); + this.info.display = 'inline-block'; + this.info.top = this.ruler.globalBounds.top; + this.info.left = 0; + this.info.data = hits; + let names = hits.map(hit => { + return hit.model.name; + }); + //@awilkey: is this obsolete? + this.info.innerHTML = `${names.join('\n')}
`; + m.redraw(); + } else if (this.info.display !== 'none') { + this.info.display = 'none'; + m.redraw(); + } + } + } + this.draw(); + this.zoomP = { + start: 0, + end: 0, + pStart: false, + ruler: false, + delta: 0, + corner: { + top: 0, + left: 0 + } + }; +// this.zoomP.ruler = false; +// this.zoomP.pStart = false; + return true; // do not stop propagation + } + + + /** + * Converts a pixel position to the canvas' backbone coordinate system. + * + * @param {number} point - pixel position on screen + * @return {number} backbone position + * + * @private + */ + + _pixelToCoordinate(point) { + let coord = this.model.view.base; + let visc = this.model.view.visible; + let psf = this.model.view.pixelScaleFactor; + return ((visc.start * (coord.stop * psf - point) + visc.stop * (point - coord.start * psf)) / (psf * (coord.stop - coord.start))) - (coord.start * -1); + } + + /** + * perform layout of backbone, feature markers, and feature labels. + * + * @param {object} layoutBounds - bounds object representing bounds of this canvas + * + * @private + */ + + _layout(layoutBounds) { + // TODO: calculate width based on # of SNPs in layout, and width of feature + // labels + // Setup Canvas + //const width = Math.floor(100 + Math.random() * 200); + this.lb = this.lb || layoutBounds; + console.log('BioMap -> layout'); + const width = Math.floor(this.lb.width / this.appState.bioMaps.length); + this.children = []; + this.domBounds = this.domBounds || new Bounds({ + left: this.lb.left, + top: this.lb.top, + width: width > 300 ? width : 300, + height: this.lb.height + }); + + this.bounds = this.bounds || new Bounds({ + left: 0, + top: this.lb.top + 40, + width: this.domBounds.width, + height: Math.floor(this.domBounds.height - 140) // set to reasonably re-size for smaller windows + }); + //Add children tracks + this.bbGroup = new Group({parent: this}); + this.bbGroup.bounds = new Bounds({ + top: this.bounds.top, + left: this.model.config.ruler.labelSize * 10, + width: 10, + height: this.bounds.height + }); + this.bbGroup.model = this.model; + this.backbone = new MapTrack({parent: this}); + this.bbGroup.addChild(this.backbone); + this.model.view.backbone = this.backbone.backbone.globalBounds; + this.ruler = new Ruler({parent: this, bioMap: this.model, config: this.model.config.ruler}); + this.bbGroup.addChild(this.ruler); + this.backbone.children.forEach(child => { + if (child.globalBounds.left < this.bbGroup.bounds.left) { + this.bbGroup.bounds.left = child.globalBounds.left; + } + if (child.globalBounds.right > this.bbGroup.bounds.right) { + this.bbGroup.bounds.right = child.globalBounds.right; + } + }); + this.children.push(this.bbGroup); + + this.tracksRight =[]; + this.tracksLeft = []; + if(this.model.tracks) { + this.model.tracks.forEach((track,order) => { + track.tracksIndex = order; + if (track.position === -1) { + this.tracksRight.push(track); + } else { + this.tracksLeft.push(track); + } + }); + } + + let qtlRight = new FeatureTrack({parent:this,position:1}); + let qtlLeft = new FeatureTrack({parent:this,position:-1}); + // let qtlRight = {}; + //let qtlRight = new QtlTrack({parent: this , position: 1}); + //let qtlLeft = new QtlTrack({parent: this, position: -1}); + this.addChild(qtlRight); + this.addChild(qtlLeft); + + if (qtlLeft && qtlLeft.bounds.right > this.bbGroup.bounds.left) { + const bbw = this.bbGroup.bounds.width; + this.bbGroup.bounds.left = qtlLeft.globalBounds.right + 100; + this.bbGroup.bounds.width = bbw; + const qrw = qtlRight.bounds.width; + qtlRight.bounds.left += qtlLeft.globalBounds.right; + qtlRight.bounds.right = qtlRight.bounds.left + qrw; + } + + if (this.domBounds.width < qtlRight.globalBounds.right + 30) { + this.domBounds.width = qtlRight.globalBounds.right + 50; + } + + //load local rBush tree for hit detection + this._loadHitMap(); + //let layout know that width has changed on an element; + //m.redraw(); + this.dirty = true; + } + + /** + * Adds children nodes to the R-tree + * + * @private + */ + + _loadHitMap() { + let hits = []; + let childrenHits = this.children.map(child => { + return child.hitMap; + }); + childrenHits.forEach(child => { + hits = hits.concat(child); + }); + this.locMap.clear();// = rbush(); + this.locMap.load(hits); + } + + /** + * Redraw restricted view + * + * @param coordinates + * @private + */ + + _redrawViewport(coordinates) { + this.model.view.visible = { + start: coordinates.start, + stop: coordinates.stop + }; + this.backbone.loadLabelMap(); + this.draw(); + + let cMaps = document.getElementsByClassName('cmap-correspondence-map'); + [].forEach.call(cMaps, el => { + el.mithrilComponent.draw(); + }); + // move top of popover if currently visible + if (this.info.display !== 'none') { + this.info.top = this.info.data[0].globalBounds.top; + } + m.redraw(); + } +} diff --git a/src/canvas/layout/CorrespondenceMap.js b/src/canvas/canvas/CorrespondenceMap.js similarity index 69% rename from src/canvas/layout/CorrespondenceMap.js rename to src/canvas/canvas/CorrespondenceMap.js index 8031f25a..8b6513e9 100644 --- a/src/canvas/layout/CorrespondenceMap.js +++ b/src/canvas/canvas/CorrespondenceMap.js @@ -1,8 +1,10 @@ /** - * CorrespondenceMap - * Mithril component for correspondence lines between 2 or more BioMaps with an - * html5 canvas element. - */ + * Mithril component for correspondence lines between 2 or more BioMaps with an + * html5 canvas element. + * + * @extends SceneGraphNodeCanvas + * + */ import m from 'mithril'; import {Bounds} from '../../model/Bounds'; import {SceneGraphNodeCanvas} from '../node/SceneGraphNodeCanvas'; @@ -10,9 +12,10 @@ import {SceneGraphNodeGroup} from '../node/SceneGraphNodeGroup'; import {CorrespondenceMark} from '../geometry/CorrespondenceMark'; import {featuresInCommon} from '../../model/Feature'; -export class CorrespondenceMap extends SceneGraphNodeCanvas{ +export class CorrespondenceMap extends SceneGraphNodeCanvas { constructor({bioMapComponents, appState, layoutBounds}) { super({}); + console.log('CorrespondenceMap -> constructor'); this.bioMapComponents = bioMapComponents; this.appState = appState; this.verticalScale = 1; @@ -25,12 +28,14 @@ export class CorrespondenceMap extends SceneGraphNodeCanvas{ */ draw() { let ctx = this.context2d; - if(! ctx) return; - if(! this.domBounds) return; + if (!ctx) return; + if (!this.domBounds) return; ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); let gb = this.globalBounds || {}; ctx.save(); ctx.globalAlpha = 0; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(gb.top), @@ -51,17 +56,18 @@ export class CorrespondenceMap extends SceneGraphNodeCanvas{ let leftFeatures = this.bioMapComponents[0].model.features; let rightFeatures = this.bioMapComponents[1].model.features; //let leftFeatures = this.bioMapComponents[0].backbone.filteredFeatures; - //let rightFeatures = this.bioMapComponents[1].backbone.filteredFeatures; - let common = featuresInCommon(leftFeatures, rightFeatures); - return common; + //let rightFeatures = this.bioMapComponents[1].backbone.filteredFeat + return featuresInCommon(leftFeatures, rightFeatures); + //return common; } /** - * + * mithril component render callback + * mithril component render callback * */ + view() { - if(this.domBounds && ! this.domBounds.isEmptyArea) { + if (this.domBounds && !this.domBounds.isEmptyArea) { this.lastDrawnMithrilBounds = this.domBounds; } let b = this.domBounds || {}; @@ -74,12 +80,18 @@ export class CorrespondenceMap extends SceneGraphNodeCanvas{ }); } + /** + * Lay out correspondence lines between features + * @param layoutBounds - bounds of the linked canvas + * @private + */ + _layout(layoutBounds) { this.domBounds = layoutBounds; // this.bounds (scenegraph) has the same width and height, but zero the // left/top because we are the root node in a canvas sceneGraphNode - // heirarchy. - let gb1 = this.bioMapComponents[0].backbone.backbone.globalBounds; + // hierarchic. + let gb1 = this.bioMapComponents[0].backbone.markerGroup.globalBounds; this.bounds = new Bounds({ allowSubpixel: false, left: 1, @@ -87,9 +99,9 @@ export class CorrespondenceMap extends SceneGraphNodeCanvas{ width: this.domBounds.width, height: this.domBounds.height }); - + let corrData = []; - let coorGroup = new SceneGraphNodeGroup({parent:this}); + let coorGroup = new SceneGraphNodeGroup({parent: this}); coorGroup.bounds = new Bounds({ allowSubpixel: false, top: gb1.top, @@ -98,33 +110,35 @@ export class CorrespondenceMap extends SceneGraphNodeCanvas{ height: gb1.height, }); this.addChild(coorGroup); - console.log('childBounds', this.globalBounds, coorGroup.globalBounds); let bioMapCoordinates = [ - this.bioMapComponents[0].mapCoordinates, + this.bioMapComponents[0].mapCoordinates, this.bioMapComponents[1].mapCoordinates ]; - this.commonFeatures.forEach( feature => { + this.commonFeatures.forEach(feature => { let corrMark = new CorrespondenceMark({ parent: coorGroup, featurePair: feature, - mapCoordinates:bioMapCoordinates, - bioMap : this.bioMapComponents + mapCoordinates: bioMapCoordinates, + bioMap: this.bioMapComponents }); coorGroup.addChild(corrMark); corrData.push({ - minX:this.bounds.left, - maxX:this.bounds.right , - minY:feature[0].coordinates.start, + minX: this.bounds.left, + maxX: this.bounds.right, + minY: feature[0].coordinates.start, maxY: feature[1].coordinates.start, data: corrMark }); }); - this.locMap.load(corrData); - console.log('bioMap', this.locMap.all()); + this.locMap.load(corrData); } - get visible(){ + /** + * Return visible elements in R-Tree + */ + + get visible() { return this.locMap.all(); } } diff --git a/src/canvas/geometry/CorrespondenceMark.js b/src/canvas/geometry/CorrespondenceMark.js index 671f2dd1..a27a9c69 100644 --- a/src/canvas/geometry/CorrespondenceMark.js +++ b/src/canvas/geometry/CorrespondenceMark.js @@ -1,55 +1,83 @@ /** - * FeatureMarker - * A SceneGraphNode representing a feature on a Map with a line or hash mark. - */ + * FeatureMarker + * A SceneGraphNode representing a feature on a Map with a line or hash mark. + * + * @extends SceneGraphNodeBase + */ + import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; import {translateScale} from '../../util/CanvasUtil'; export class CorrespondenceMark extends SceneGraphNodeBase { + /** + * Construct the CorrespondenceMark layer + * @param parent - parent scene graph node + * @param featurePair - array of features being compared + * @param mapCoordinates - current zoom level of each feature maps + * @param bioMap - array of both sets of map data + */ + + /** + * TODO: Allow configuration as part of config file + */ + constructor({parent, featurePair, mapCoordinates, bioMap}) { super({parent}); this.model = featurePair; this.mapCoordinates = mapCoordinates; this.lineWidth = 1.0; this.bioMap = bioMap; - + this.invert = [bioMap[0].model.config.invert, bioMap[1].model.config.invert]; + this.pixelScaleFactor = [ - bioMap[0].model.view.pixelScaleFactor, - bioMap[1].model.view.pixelScaleFactor, + bioMap[0].model.view.pixelScaleFactor, + bioMap[1].model.view.pixelScaleFactor, ]; + let leftY = translateScale( - this.model[0].coordinates.start, - bioMap[0].model.view.base, - bioMap[0].model.view.visible) * this.pixelScaleFactor[0]; + this.model[0].coordinates.start, + bioMap[0].model.view.base, + bioMap[0].model.view.visible, + this.invert[0]) * this.pixelScaleFactor[0]; + let rightY = translateScale( - this.model[1].coordinates.start, - bioMap[1].model.view.base, - bioMap[1].model.view.visible) * this.pixelScaleFactor[1]; + this.model[1].coordinates.start, + bioMap[1].model.view.base, + bioMap[1].model.view.visible, + this.invert[1]) * this.pixelScaleFactor[1]; this.bounds = new Bounds({ allowSubpixel: false, top: leftY, left: parent.bounds.left, - height: leftY-rightY, + height: leftY - rightY, width: parent.bounds.width }); } + /** + * Draw the correspondence marks + * @param {object} ctx - canvas context 2D + */ + draw(ctx) { - var bioMap = this.bioMap; + let bioMap = this.bioMap; let leftYStart = translateScale( this.model[0].coordinates.start, bioMap[0].model.view.base, - bioMap[0].model.view.visible) * this.pixelScaleFactor[0]; + bioMap[0].model.view.visible, + this.invert[0]) * this.pixelScaleFactor[0]; + let rightYStart = translateScale( this.model[1].coordinates.start, bioMap[1].model.view.base, - bioMap[1].model.view.visible) * this.pixelScaleFactor[1]; + bioMap[1].model.view.visible, + this.invert[1]) * this.pixelScaleFactor[1]; if (this.model[0].coordinates.start === this.model[0].coordinates.stop - && this.model[1].coordinates.start === this.model[1].coordinates.stop) { + && this.model[1].coordinates.start === this.model[1].coordinates.stop) { // correspondence line this.bounds.top = leftYStart; this.bounds.bottom = rightYStart; @@ -58,7 +86,9 @@ export class CorrespondenceMark extends SceneGraphNodeBase { ctx.lineWidth = this.lineWidth; ctx.strokeStyle = '#CAA91E'; ctx.globalAlpha = 0.7; + // noinspection JSSuspiciousNameCombination ctx.moveTo(Math.floor(gb.left), Math.floor(gb.top)); + // noinspection JSSuspiciousNameCombination ctx.lineTo(Math.floor(gb.right), Math.floor(gb.bottom)); ctx.stroke(); } @@ -67,11 +97,13 @@ export class CorrespondenceMark extends SceneGraphNodeBase { let leftYStop = translateScale( this.model[0].coordinates.stop, bioMap[0].model.view.base, - bioMap[0].model.view.visible) * this.pixelScaleFactor[0]; + bioMap[0].model.view.visible, + this.invert[0]) * this.pixelScaleFactor[0]; let rightYStop = translateScale( this.model[1].coordinates.stop, bioMap[1].model.view.base, - bioMap[1].model.view.visible) * this.pixelScaleFactor[1]; + bioMap[1].model.view.visible, + this.invert[1]) * this.pixelScaleFactor[1]; this.bounds.top = leftYStart; this.bounds.bottom = leftYStop; @@ -87,10 +119,14 @@ export class CorrespondenceMark extends SceneGraphNodeBase { ctx.beginPath(); ctx.lineWidth = this.lineWidth; ctx.globalAlpha = 0.2; - ctx.fillStyle = '#7C6400';//'#A4870C'; + ctx.fillStyle = '#7C6400'; //'#A4870C'; + // noinspection JSSuspiciousNameCombination ctx.moveTo(Math.floor(gbLeft.left), Math.floor(gbLeft.top)); + // noinspection JSSuspiciousNameCombination ctx.lineTo(Math.floor(gbLeft.left), Math.floor(gbLeft.bottom)); + // noinspection JSSuspiciousNameCombination ctx.lineTo(Math.floor(gbRight.right), Math.floor(gbRight.bottom)); + // noinspection JSSuspiciousNameCombination ctx.lineTo(Math.floor(gbRight.right), Math.floor(gbRight.top)); ctx.fill(); } diff --git a/src/canvas/geometry/Dot.js b/src/canvas/geometry/Dot.js new file mode 100644 index 00000000..178cc614 --- /dev/null +++ b/src/canvas/geometry/Dot.js @@ -0,0 +1,82 @@ +/** + * + * A SceneGraphNode representing a circular mark. + * + * @extends SceneGraphNodeBase + */ + +import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; +import {Bounds} from '../../model/Bounds'; +import {translateScale} from '../../util/CanvasUtil'; + +export class Dot extends SceneGraphNodeBase { + /** + * Constructor + * + * @param parent - parent scene graph node + * @param bioMap - map data + * @param featureModel - feature data + * @param config - configuration information object + */ + + constructor({parent, bioMap, featureModel, config}) { + super({parent, tags: [featureModel.name]}); + //setup config + this.config = config; + this.model = featureModel; + this.view = bioMap.view; + this.pixelScaleFactor = this.view.pixelScaleFactor; + this.invert = bioMap.view.invert; + this.start = this.model.coordinates.start; + this.radius = config.width; + this.depth = 0; + + // setup initial placement + if (this.model.coordinates.depth) { + this.depth = translateScale(this.model.coordinates.depth, { + start: 0, + stop: config.displayWidth + }, config.view, false); + } + this.bounds = new Bounds({ + top: 0, + left: 0, + width: 2 * this.radius, //this.fontSize*(this.model.name.length), + height: 2 * this.radius, + allowSubpixel: false + }); + } + + /** + * Draw label on cmap canvas context + * @param ctx + */ + + draw(ctx) { + //Setup a base offset based on parent track + if (this.start < this.view.visible.start || this.start > this.view.visible.stop) return; + if (!this.offset) { + const left = this.globalBounds.left; + const top = this.globalBounds.top; + this.offset = {top: top, left: left}; + } + let config = this.config; + let y = translateScale(this.start, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; + let x = this.depth; + + // Draw dot + ctx.beginPath(); + ctx.fillStyle = config.fillColor; + ctx.arc(x + this.offset.left, y + this.offset.top, this.radius, 0, 2 * Math.PI, false); + ctx.fill(); + ctx.lineWidth = config.lineWeight; + ctx.strokeStyle = config.lineColor; + ctx.stroke(); + + //update bounding box + this.bounds.top = y - this.radius; + this.bounds.left = x - this.radius; + this.bounds.width = 2 * this.radius; + this.bounds.height = 2 * this.radius; + } +} \ No newline at end of file diff --git a/src/canvas/geometry/FeatureLabel.js b/src/canvas/geometry/FeatureLabel.js index a0617d0a..09679920 100644 --- a/src/canvas/geometry/FeatureLabel.js +++ b/src/canvas/geometry/FeatureLabel.js @@ -1,41 +1,57 @@ /** - * FeatureLabel - * A SceneGraphNode representing a text label for a feature on a Map. - */ + * + * A SceneGraphNode representing a text label for a feature on a Map. + * + * @extends SceneGraphNodeBase + */ + import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; import {translateScale} from '../../util/CanvasUtil'; export class FeatureLabel extends SceneGraphNodeBase { + /** + * Constructor + * + * @param parent - parent scene graph node + * @param bioMap - map data + * @param featureModel - feature data + */ - constructor({parent, bioMap, featureModel}) { + constructor({parent, bioMap, featureModel,config}) { super({parent, tags: [featureModel.name]}); + this.config = config; this.model = featureModel; this.view = bioMap.view; - this.fontSize = bioMap.config.markerLabelSize; - this.fontFace = bioMap.config.markerLabelFace; - this.fontColor = bioMap.config.markerLabelColor; this.pixelScaleFactor = this.view.pixelScaleFactor; + this.invert = bioMap.view.invert; + this.start = this.model.coordinates.start; this.bounds = new Bounds({ - allowSubpixel: false, top: 0, left: 5, - width: parent.bounds.width, - height: 12 + width: 200, //this.fontSize*(this.model.name.length), + height: 12, + allowSubpixel: false }); } + /** + * Draw label on cmap canvas context + * @param ctx + */ + draw(ctx) { - let y = translateScale(this.model.coordinates.start,this.view.base,this.view.visible) * this.pixelScaleFactor; + let config = this.config; + let y = translateScale(this.start, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; this.bounds.top = y; - this.bounds.bottom = y + this.fontSize; + this.bounds.bottom = y + config.labelSize; let gb = this.globalBounds || {}; - ctx.font = `${this.fontSize}px ${this.fontFace}`; + ctx.font = `${config.labelSize}px ${config.labelFace}`; ctx.textAlign = 'left'; - ctx.fillStyle = this.fontColor; - ctx.fillText(this.model.name,gb.left, gb.top); + ctx.fillStyle = config.labelColor; + ctx.fillText(this.model.name, gb.left, gb.top); // reset bounding box to fit the new stroke location/width - this.bounds.right = this.bounds.left + Math.floor(ctx.measureText(this.model.name).width)+1; - if(this.parent.bounds.width < this.bounds.width) this.parent.bounds.width = this.bounds.width; + this.bounds.width = this.bounds.left + Math.floor(ctx.measureText(this.model.name).width) + 1; + if (this.parent.bounds.width < this.bounds.width) this.parent.bounds.width = this.bounds.width; } } diff --git a/src/canvas/geometry/FeatureMark.js b/src/canvas/geometry/FeatureMark.js index 7dc34087..ee50675a 100644 --- a/src/canvas/geometry/FeatureMark.js +++ b/src/canvas/geometry/FeatureMark.js @@ -1,21 +1,31 @@ /** - * FeatureMarker - * A SceneGraphNode representing a feature on a Map with a line or hash mark. - */ + * FeatureMarker + * A SceneGraphNode representing a feature on a Map with a line or hash mark. + * + * @extends SceneGraphNodeBase + */ import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; import {translateScale} from '../../util/CanvasUtil'; export class FeatureMark extends SceneGraphNodeBase { - constructor({parent, bioMap, featureModel}) { + /** + * Constructor + * @param parent - parent scene graph node + * @param bioMap - map data + * @param featureModel - feature data + */ + + constructor({parent, bioMap, featureModel,config}) { super({parent, tags: [featureModel.name]}); this.model = featureModel; this.featureMap = bioMap; + this.config = config; - this.offset = this.featureMap.view.base.start*-1; - this.lineWidth = bioMap.config.markerWeight; - this.strokeStyle = bioMap.config.markerColor; + this.offset = this.featureMap.view.base.start * -1; + this.invert = this.featureMap.view.invert; + this.start = this.model.coordinates.start; this.pixelScaleFactor = this.featureMap.view.pixelScaleFactor; this.bounds = new Bounds({ @@ -23,23 +33,31 @@ export class FeatureMark extends SceneGraphNodeBase { top: 0, left: 0, width: parent.bounds.width, - height: this.lineWidth + height: this.lineWeight }); } + /** + * Draw the marker + * @param ctx - active canvas2D context + */ + draw(ctx) { - let y = translateScale(this.model.coordinates.start, this.featureMap.view.base, this.featureMap.view.visible) * this.pixelScaleFactor; + let config = this.config; + let y = translateScale(this.start, this.featureMap.view.base, this.featureMap.view.visible, this.invert) * this.pixelScaleFactor; this.bounds.top = y; let gb = this.globalBounds || {}; ctx.beginPath(); - ctx.strokeStyle = this.strokeStyle; - ctx.lineWidth = this.lineWidth; + ctx.strokeStyle = config.lineColor; + ctx.lineWidth = config.lineWeight; + // noinspection JSSuspiciousNameCombination ctx.moveTo(Math.floor(gb.left), Math.floor(gb.top)); + // noinspection JSSuspiciousNameCombination ctx.lineTo(Math.floor(gb.right), Math.floor(gb.top)); ctx.stroke(); // reset bounding box to fit the new stroke location/width // lineWidth adds equal percent of passed width above and below path - this.bounds.top = Math.floor(y - this.lineWidth/2); - this.bounds.bottom = Math.floor( y + this.lineWidth/2); + this.bounds.top = Math.floor(y - config.lineWeight / 2); + this.bounds.bottom = Math.floor(y + config.lineWeight / 2); } } diff --git a/src/canvas/geometry/MapBackbone.js b/src/canvas/geometry/MapBackbone.js index b65797e7..dceaf04d 100644 --- a/src/canvas/geometry/MapBackbone.js +++ b/src/canvas/geometry/MapBackbone.js @@ -1,38 +1,67 @@ /** - * MapBackbone - * A SceneGraphNode representing a backbone, simply a rectangle representing - * the background. - */ + * MapBackbone + * A SceneGraphNode representing a backbone, simply a rectangle enclosing the upper and + * lower bounds of the map of the current feature, providing a delineated region to draw + * features of interest + * + * @extends SceneGraphNodeBase + */ + import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; export class MapBackbone extends SceneGraphNodeBase { - constructor({parent, bioMap}) { + /** + * Constructor + * @param parent - Parent scene graph node + * @param bioMap - Map data + */ + + constructor({parent, bioMap,config}) { super({parent}); + this.config = config; const b = parent.bounds; - const config = bioMap.config; - const backboneWidth = config.backboneWidth; - this.fillStyle = config.backboneColor; + const backboneWidth = config.width; this.bounds = new Bounds({ allowSubpixel: false, top: 0, left: b.width * 0.5 - backboneWidth * 0.5, - width: backboneWidth, + width: backboneWidth + config.lineWeight, height: b.height }); bioMap.view.backbone = this.globalBounds; } + /** + * Draw the map backbone, then iterate through and draw its children + * @param ctx - currently active canvas2D context + */ + draw(ctx) { + let config = this.config; let gb = this.globalBounds || {}; - ctx.fillStyle = this.fillStyle; + ctx.fillStyle = config.fillColor; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(gb.top), Math.floor(gb.width), Math.floor(gb.height) ); - this.children.forEach( child => child.draw(ctx)); + + if(this.lineWidth > 0) { + ctx.strokeStyle = config.lineColor; + ctx.lineWidth = config.lineWeight; + ctx.strokeRect( + Math.floor(gb.left), + Math.floor(gb.top), + Math.floor(gb.width), + Math.floor(gb.height) + ); + } + + this.children.forEach(child => child.draw(ctx)); } } diff --git a/src/canvas/geometry/QTL.js b/src/canvas/geometry/QTL.js index f10a165e..da301ef9 100644 --- a/src/canvas/geometry/QTL.js +++ b/src/canvas/geometry/QTL.js @@ -1,50 +1,68 @@ /** - * QTL - A feature with a length and width drawn as part of a group of similar - * features - * - */ + * QTL - A feature with a length and width drawn as part of a group of similar + * features + * + * @extends SceneGraphNodeBase + */ + import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; import {translateScale} from '../../util/CanvasUtil'; export class QTL extends SceneGraphNodeBase { - constructor({parent, bioMap, featureModel, initialConfig}) { + /** + * Construct the QTL feature + * @param parent - parent scene graph node + * @param bioMap - map data + * @param featureModel - feature data + * @param initialConfig - configuration object for display variables + */ + + constructor({parent, bioMap, featureModel, initialConfig,config}) { super({parent, tags: [featureModel.name]}); - let config = bioMap.config; this.model = featureModel; this.featureMap = bioMap; this.view = this.featureMap.view; this.lineWidth = 1.0; //min and max location in pixels this.pixelScaleFactor = this.featureMap.view.pixelScaleFactor; - this.fill = initialConfig.trackColor[initialConfig.filters.indexOf(this.model.tags[0])]||initialConfig.trackColor[0] || config.trackColor ; - this.width = initialConfig.trackWidth || config.trackWidth; - this.trackSpacing = initialConfig.trackSpacing || config.trackSpacing; - this.labelColor = config.trackLabelColor; - this.labelSize = config.trackLabelSize; - this.labelFace = config.trackLabelFace; - this.offset = this.trackSpacing + this.labelSize; + this.fill = config.fillColor; + if(initialConfig.fillColor) { + this.fill = initialConfig.fillColor[initialConfig.filters.indexOf(this.model.tags[0])] || initialConfig.fillColor[0]; + } + + this.width = initialConfig.width || config.width; + this.trackSpacing = initialConfig.internalPadding || config.internalPadding; + this.labelColor = initialConfig.labelColor || config.labelColor; + this.labelSize = initialConfig.labelSize || config.labelSize; + this.labelFace = initialConfig.labelFace || config.labelFace; + this.offset = this.trackSpacing + this.labelSize; + this.invert = this.view.invert; + this.start = this.invert ? this.model.coordinates.stop : this.model.coordinates.start; + this.stop = this.invert ? this.model.coordinates.start : this.model.coordinates.stop; // Calculate start/end position, then // Iterate across QTLs in group and try to place QTL region where it can // minimize stack width in parent group - let y1 = translateScale(this.model.coordinates.start,this.view.base, this.view.visible) * this.pixelScaleFactor; - let y2 = translateScale(this.model.coordinates.stop, this.view.base, this.view.visible) * this.pixelScaleFactor; + let y1 = translateScale(this.start, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; + let y2 = translateScale(this.stop, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; let leftLoc = 0; - let leftArr = []; + let leftArr; leftArr = this.parent.locMap.search({ minY: this.model.coordinates.start, maxY: this.model.coordinates.stop, minX: 0, - maxX:10000 + maxX: 10000 + }); + leftArr = leftArr.sort((a, b) => { + return a.data.bounds.right - b.data.bounds.right; }); - leftArr = leftArr.sort((a,b)=>{return a.data.bounds.right-b.data.bounds.right;}); let stepOffset = this.width + this.offset; let stackEnd = leftArr.length; - for( let i = 0; i <= stackEnd; ++i){ - leftLoc = i*(stepOffset); - if( leftArr[i] && leftArr[i].data.bounds.left !== leftLoc){ + for (let i = 0; i <= stackEnd; ++i) { + leftLoc = i * (stepOffset); + if (leftArr[i] && leftArr[i].data.bounds.left !== leftLoc) { break; } } @@ -54,25 +72,37 @@ export class QTL extends SceneGraphNodeBase { top: y1, left: leftLoc, width: this.width, - height: y2-y1 + height: y2 - y1 }); } + /** + * + * @param ctx + */ + draw(ctx) { // Get start and stop of QTL on current region, if it isn't located in // current view, don't draw, else cutoff when it gets to end of currently // visible region. - if( this.model.coordinates.stop < this.view.visible.start || - this.model.coordinates.start > this.view.visible.stop) return; - var y1pos = this.model.coordinates.start > this.view.visible.start ? this.model.coordinates.start : this.view.visible.start; - var y2pos = this.model.coordinates.stop < this.view.visible.stop ? this.model.coordinates.stop : this.view.visible.stop; - let y1 = translateScale(y1pos,this.view.base,this.view.visible) * this.pixelScaleFactor; - let y2 = translateScale(y2pos,this.view.base,this.view.visible) * this.pixelScaleFactor; + if (this.model.coordinates.stop < this.view.visible.start || + this.model.coordinates.start > this.view.visible.stop) return; + let y1pos = this.model.coordinates.start > this.view.visible.start ? this.model.coordinates.start : this.view.visible.start; + let y2pos = this.model.coordinates.stop < this.view.visible.stop ? this.model.coordinates.stop : this.view.visible.stop; + this.start = y1pos; + this.stop = y2pos; + if (this.invert) { + this.start = y2pos; + this.stop = y1pos; + } + + let y1 = translateScale(this.start, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; + let y2 = translateScale(this.stop, this.view.base, this.view.visible, this.invert) * this.pixelScaleFactor; //setup bounds and draw this.bounds = new Bounds({ top: y1, - height: y2-y1, + height: y2 - y1, left: this.bounds.left, width: this.width }); @@ -82,30 +112,32 @@ export class QTL extends SceneGraphNodeBase { let fontStyle = this.labelFace; ctx.font = `${fontSize}px ${fontStyle}`; ctx.fillStyle = this.fill; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(gb.top), Math.floor(this.width), Math.floor(qtlHeight) ); - let textWidth = ctx.measureText(this.model.name).width + (ctx.measureText('M').width*6); - let textStop = this.model.coordinates.stop - (translateScale(textWidth/this.pixelScaleFactor,this.view.base,this.view.visible)+this.view.base.start); + let textWidth = ctx.measureText(this.model.name).width + (ctx.measureText('M').width * 6); + let textStop = this.stop - (translateScale(textWidth / this.pixelScaleFactor, this.view.base, this.view.visible) + this.view.base.start); let overlap = this.parent.locMap.search({ minY: textStop > this.view.visible.start ? textStop : this.view.visible.start, - maxY: this.model.coordinates.stop, + maxY: this.stop, minX: gb.left, maxX: gb.right }); - if(overlap.length <=1 || textWidth <= gb.height){ + if (overlap.length <= 1 || textWidth <= gb.height) { ctx.save(); - ctx.translate(gb.left,gb.top); + ctx.translate(gb.left, gb.top); ctx.fillStyle = this.labelColor; - ctx.rotate(-Math.PI /2); - ctx.fillText(this.model.name,-gb.height,this.width+fontSize+1); + ctx.rotate(-Math.PI / 2); + ctx.fillText(this.model.name, -gb.height, this.width + fontSize + 1); ctx.restore(); } // Draw any children - this.children.forEach( child => child.draw(ctx)); + this.children.forEach(child => child.draw(ctx)); } } diff --git a/src/canvas/geometry/Ruler.js b/src/canvas/geometry/Ruler.js index 07c3bb97..fb62598a 100644 --- a/src/canvas/geometry/Ruler.js +++ b/src/canvas/geometry/Ruler.js @@ -1,75 +1,118 @@ /** - * ruler - * A SceneGraphNode representing ruler and zoom position for a given backbone - * - */ + * ruler + * A SceneGraphNode representing ruler and zoom position for a given backbone + * + * @extends SceneGraphNodeBase + */ + import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; import {Bounds} from '../../model/Bounds'; +import {translateScale} from '../../util/CanvasUtil'; export class Ruler extends SceneGraphNodeBase { - constructor({parent, bioMap}) { + /** + * Constructor + * @param parent - parent scene graph node + * @param bioMap - map data + * @param config - ruler configuration object + */ + + constructor({parent, bioMap, config}) { super({parent}); - let config = bioMap.config; this.config = config; this.mapCoordinates = bioMap.view; - this.offset = this.mapCoordinates.base.start*-1; + this.offset = this.mapCoordinates.base.start * -1; this.pixelScaleFactor = this.mapCoordinates.pixelScaleFactor; - this.fillColor = config.rulerColor; - this.textFace = config.rulerLabelFace; - this.textSize = config.rulerLabelSize; - this.textColor = config.rulerLabelColor; - this.rulerPrecision = config.rulerPrecision; + this.invert = this.mapCoordinates.invert; + this.fillColor = config.fillColor; + this.textFace = config.labelFace; + this.textSize = config.labelSize; + this.textColor = config.labelColor; + this.rulerPrecision = config.precision; + this.rulerWidth = config.width; + this.rulerPadding = config.padding; + this.innerSize = config.innerLineWeight; + this.innerColor = config.innerLineColor; const b = this.parent.backbone.bounds; this.bounds = new Bounds({ allowSubpixel: false, - top: this.parent.bounds.top, - left: b.left- config.rulerWidth - config.rulerSpacing , //arbritray spacing to look goo - width: config.rulerWidth, - height: b.height + top: 0, + left: b.left - config.width - config.padding - config.lineWeight, //arbitrary spacing to look goo + width: config.width, + height: b.height }); } - draw(ctx) { + /** + * Draw ruler and zoom bar + * @param ctx - linked canvas2D context + */ - let start = (this.mapCoordinates.visible.start+this.offset) * this.pixelScaleFactor; - let stop = (this.mapCoordinates.visible.stop+this.offset) * this.pixelScaleFactor; - let text = [this.mapCoordinates.base.start.toFixed(this.rulerPrecision),this.mapCoordinates.base.stop.toFixed(this.rulerPrecision)]; + draw(ctx) { + let config = this.config; + let vStart = this.invert ? this.mapCoordinates.visible.stop : this.mapCoordinates.visible.start; + let vStop = this.invert ? this.mapCoordinates.visible.start : this.mapCoordinates.visible.stop; + let start = translateScale(vStart, this.mapCoordinates.base, this.mapCoordinates.base, this.invert) * this.pixelScaleFactor; + let stop = translateScale(vStop, this.mapCoordinates.base, this.mapCoordinates.base, this.invert) * this.pixelScaleFactor; + let text = [this.mapCoordinates.base.start.toFixed(config.precision), this.mapCoordinates.base.stop.toFixed(config.precision)]; - let w = ctx.measureText(text[0]).width > ctx.measureText(text[1]).width ? ctx.measureText(text[0]).width : ctx.measureText(text[1]).width; - this.textWidth = w; + this.textWidth = ctx.measureText(text[0]).width > ctx.measureText(text[1]).width ? ctx.measureText(text[0]).width : ctx.measureText(text[1]).width; let gb = this.globalBounds || {}; // draw baseline labels - ctx.font = `${this.textSize}px ${this.textFace}`; + ctx.font = `${config.labelSize}px ${config.labelFace}`; + ctx.fillStyle = config.labelColor; ctx.textAlign = 'left'; - ctx.fillStyle = this.textColor; - ctx.fillText(text[0],gb.left - ctx.measureText(text[0]).width - (gb.width/2),Math.floor(gb.top - this.textSize/2)); - ctx.fillText(text[1],gb.left - ctx.measureText(text[1]).width - (gb.width/2),Math.floor(gb.bottom+this.textSize)); + if (this.invert) { + ctx.fillText(text[1], gb.left - ctx.measureText(text[1]).width - (gb.width / 2), Math.floor(gb.top - config.labelSize / 2)); + ctx.fillText(text[0], gb.left - ctx.measureText(text[0]).width - (gb.width / 2), Math.floor(gb.bottom + config.labelSize)); + } else { + ctx.fillText(text[0], gb.left - ctx.measureText(text[0]).width - (gb.width / 2), Math.floor(gb.top - config.labelSize / 2)); + ctx.fillText(text[1], gb.left - ctx.measureText(text[1]).width - (gb.width / 2), Math.floor(gb.bottom + config.labelSize)); + } // Draw zoom position labels - text = [this.mapCoordinates.visible.start.toFixed(this.rulerPrecision),this.mapCoordinates.visible.stop.toFixed(this.rulerPrecision)]; - - ctx.fillText(text[0],gb.left + this.config.rulerWidth + this.config.rulerSpacing , Math.floor(gb.top - this.textSize/2)); - ctx.fillText(text[1],gb.left + this.config.rulerWidth + this.config.rulerSpacing ,(gb.bottom + this.textSize)); + text = [this.mapCoordinates.visible.start.toFixed(config.precision), this.mapCoordinates.visible.stop.toFixed(config.precision)]; + if (this.invert) { + ctx.fillText(text[1], gb.left + config.width + config.padding, Math.floor(gb.top - config.labelSize / 2)); + ctx.fillText(text[0], gb.left + config.width + config.padding, (gb.bottom + config.labelSize)); + } else { + ctx.fillText(text[0], gb.left + config.width + config.padding, Math.floor(gb.top - config.labelSize / 2)); + ctx.fillText(text[1], gb.left + config.width + config.padding, (gb.bottom + config.labelSize)); + } //Draw baseline ruler - ctx.beginPath(); - ctx.lineWidth = 1.0; - ctx.strokeStyle = 'black'; - ctx.moveTo(Math.floor(gb.left + gb.width/2), Math.floor(gb.top)); - ctx.lineTo(Math.floor(gb.left + gb.width/2), Math.floor(gb.bottom)); + ctx.beginPath(); + ctx.lineWidth = config.innerLineWeight; + ctx.strokeStyle = config.innerLineColor; + // noinspection JSSuspiciousNameCombination + ctx.moveTo(Math.floor(gb.left + gb.width / 2), Math.floor(gb.top)); + // noinspection JSSuspiciousNameCombination + ctx.lineTo(Math.floor(gb.left + gb.width / 2), Math.floor(gb.bottom)); ctx.stroke(); // Draw "zoom box" - ctx.fillStyle = this.fillColor;//'aqua'; - var height = stop - start > 1 ? stop-start : 1.0; + ctx.fillStyle = config.fillColor;//'aqua'; + let height = stop - start > 1 ? stop - start : 1.0; + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(start + gb.top), Math.floor(gb.width), Math.floor(height) ); + //draw border if asked for + if(config.lineWeight > 0) { + ctx.strokeStyle = config.lineColor; + ctx.lineWidth = config.lineWeight; + ctx.strokeRect( + Math.floor(gb.left), + Math.floor(start + gb.top), + Math.floor(gb.width), + Math.floor(height) + ); + } ////debugging rectangle to test group bounds //ctx.fillStyle = 'red'; //ctx.fillRect( @@ -79,10 +122,16 @@ export class Ruler extends SceneGraphNodeBase { // Math.floor(gb.height) //); - this.children.forEach( child => child.draw(ctx)); + this.children.forEach(child => child.draw(ctx)); } - get visible(){ - return {data:this}; + /** + * Return the ruler as data for an scenegraph visibility check. (Ruler by definition is + * always visible, and does own logic for the position bar) + * @returns {{data: Ruler}} + */ + + get visible() { + return {data: this}; } } diff --git a/src/canvas/geometry/manhattanRuler.js b/src/canvas/geometry/manhattanRuler.js new file mode 100644 index 00000000..7554ccbf --- /dev/null +++ b/src/canvas/geometry/manhattanRuler.js @@ -0,0 +1,104 @@ +/** + * QTL - A feature with a length and width drawn as part of a group of similar + * features + * + * @extends SceneGraphNodeBase + */ + +import {SceneGraphNodeBase} from '../node/SceneGraphNodeBase'; +import {Bounds} from '../../model/Bounds'; +import {translateScale} from '../../util/CanvasUtil'; + +export class manhattanRuler extends SceneGraphNodeBase { + + /** + * Construct the QTL feature + * @param parent - parent scene graph node + * @param bioMap - map data + * @param featureModel - feature data + * @param initialConfig - configuration object for display variables + */ + + constructor({parent, featureModel,config}) { + super({parent}); + this.config = config; + this.manhattanPlot = featureModel; + this.bounds = new Bounds({ + allowSubpixel: false, + top:0, + left:0, + width: config.displayWidth, + height: this.parent.bounds.height + }); + } + + /** + * + * @param ctx + */ + + draw(ctx) { + let config = this.config; + ctx.save(); + ctx.globalAlpha = .5; + let cb = this.globalBounds; + let depth = 0; + if (this.manhattanPlot) { + //Draw "ruler" + ctx.strokeStyle = config.rulerColor; + ctx.lineWidth = config.rulerWeight; + + //Baseline marks + ctx.beginPath(); + + ctx.moveTo(cb.left, cb.top); + ctx.lineTo(cb.right, cb.top); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(cb.left, cb.bottom); + ctx.lineTo(cb.right, cb.bottom); + ctx.stroke(); + + //Ruler + for (let i = 0; i <= this.manhattanPlot.view.stop; i++) { + if (i % config.rulerMinorMark === 0 || i % config.rulerMajorMark === 0) { + depth = translateScale(i, { + start: 0, + stop: config.displayWidth + }, this.manhattanPlot.view, false); + ctx.beginPath(); + ctx.moveTo(cb.left + depth, cb.top); + ctx.lineTo(cb.left + depth, cb.top - 10); + ctx.stroke(); + if (i % config.rulerMajorMark === 0) { + ctx.font = config.labelSize; + ctx.fillStyle = config.labelColor; + ctx.textAlign = 'center'; + ctx.fillText(String(i), cb.left + depth, cb.top - 11); + } + } + } + ctx.fillText('-log10(p)', cb.left + config.displayWidth / 2, cb.top - 25); + + // Reference lines + + if (this.manhattanPlot.lines) { + this.manhattanPlot.lines.forEach(line => { + depth = translateScale(line.value, { + start: 0, + stop: config.displayWidth + }, this.manhattanPlot.view, false); + ctx.strokeStyle = line.lineColor; + ctx.lineWidth = line.lineWeight; + ctx.beginPath(); + ctx.moveTo(cb.left + depth, cb.top); + ctx.lineTo(cb.left + depth, cb.bottom); + ctx.stroke(); + }); + } + } + ctx.restore(); + + this.children.forEach(child => child.draw(ctx)); + } +} diff --git a/src/canvas/layout/BioMap.js b/src/canvas/layout/BioMap.js deleted file mode 100644 index 49d21918..00000000 --- a/src/canvas/layout/BioMap.js +++ /dev/null @@ -1,423 +0,0 @@ -/** - this.info.top = this.info.data.globalBounds.top; - m.redraw(); - * BioMap - * - * SceneGraphNodeCanvas representing a biological map and its associated tracks - * - */ -import m from 'mithril'; -import PubSub from 'pubsub-js'; - -import {featureUpdate,dataLoaded} from '../../topics'; - -import {Bounds} from '../../model/Bounds'; -import {SceneGraphNodeCanvas} from '../node/SceneGraphNodeCanvas'; -import {SceneGraphNodeGroup as Group} from '../node/SceneGraphNodeGroup'; -import {MapTrack} from './MapTrack'; -import {QtlTrack} from './QtlTrack'; -import {Ruler} from '../geometry/Ruler'; -import {pageToCanvas} from '../../util/CanvasUtil'; - -export class BioMap extends SceneGraphNodeCanvas { - - constructor({bioMapModel, appState, layoutBounds, bioMapIndex}) { - super({model:bioMapModel}); - this.bioMapIndex = bioMapIndex; - this.model.visible = { - start: this.model.coordinates.start, - stop: this.model.coordinates.stop - }; - this.model.view = { - base: { - start: this.model.coordinates.start, - stop: this.model.coordinates.stop - }, - visible: { - start: this.model.coordinates.start, - stop: this.model.coordinates.stop - } - }; - this.zoomDelta = (this.model.view.base.stop - this.model.view.base.start)/this.model.config.rulerSteps; - // set up coordinate bounds for view scaling - this.appState = appState; - this.verticalScale = 0; - this.backbone = null; - this.featureMarks = []; - this.featureLabels = []; - this.info = { - top:0, - left:0, - display:'none' - }; - - // create some regular expressions for faster dispatching of events - this._gestureRegex = { - pan: new RegExp('^pan'), - pinch: new RegExp('^pinch'), - tap: new RegExp('^tap'), - wheel: new RegExp('^wheel') - }; - this._layout(layoutBounds); - - } - - oncreate(vnode) { - super.oncreate(vnode); - PubSub.subscribe(featureUpdate, () => { - this._layout(this.lb); - this._redrawViewport(this.model.view.visible); - }); - } - /** - * culls elements to draw down to only those visible within the view - * bounds - */ - get visible(){ - let vis = []; - let cVis = this.children.map(child => { - return child.visible; - }); - cVis.forEach(item => { - vis = vis.concat(item); - }); - return vis; - } - /** - * children.visible() culls hits based on map coordiantes - * the hitMap is based on canvas global coordinates. - * */ - get hitMap(){ - return this.locMap; - } - - /** - * - * Re-implement lifecycle/gestrue components as needed to appease - * the items on the canvas. - * - */ - - // mousewheel event - _onZoom(evt) { - // TODO: send zoom event to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) - console.warn('BioMap -> onZoom', evt); - // normalise scroll delta - this.verticalScale = evt.deltaY < 0 ? -this.zoomDelta : this.zoomDelta; - let mcv = this.model.view.base; - let zStart = (this.model.view.visible.start + this.verticalScale); - let zStop = (this.model.view.visible.stop - this.verticalScale); - if(zStop - zStart < .01){ - this.verticalScale -=0.5; - return true; - } - if(zStart < mcv.start) { - zStart = mcv.start; - } else if ( zStart > zStop ){ - zStart = zStop; - } - - if(zStop > mcv.stop) { - zStop = mcv.stop; - } else if ( zStop < zStart ){ - zStop = zStart; - } - this._redrawViewport({start:zStart,stop:zStop}); - return true; // stop event propagation - } - - // return hits in case of tap/click event - _onTap(evt) { - console.log('BioMap -> onTap', evt, this); - let globalPos = pageToCanvas(evt,this.canvas); - this._loadHitMap(); - let hits = []; - - this.hitMap.search({ - minX: globalPos.x, - maxX: globalPos.x, - minY: globalPos.y-2, - maxY: globalPos.y+2 - }).forEach(hit => { - // temp fix, find why hit map stopped updating properly - if((hit.data.model.coordinates.start >= this.model.view.visible.start) && - (hit.data.model.coordinates.start <= this.model.view.visible.stop)){ - hits.push(hit.data); - } else if((hit.data.model.coordinates.stop >= this.model.view.visible.start) && - (hit.data.model.coordinates.stop <= this.model.view.visible.stop)){ - hits.push(hit.data); - } - }); - if(hits.length > 0){ - hits.sort((a,b) => { return a.model.coordinates.start - b.model.coordinates.start;}); - this.info.display = 'inline-block'; - this.info.top = hits[0].globalBounds.top; - this.info.left = hits[0].globalBounds.right; - this.info.data = hits; - let names = hits.map(hit => { return hit.model.name; }); - //@awilkey: is this obsolete? - this.info.innerHTML= `${names.join('\n')} <\p>`; - m.redraw(); - } else if(this.info.display !== 'none'){ - this.info.display = 'none'; - m.redraw(); - } - - return true; - } - - // Setup selection context for pan event - _onPanStart(evt) { - // TODO: send pan events to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) - this.zoomP = { - start:0, - end:0, - pStart: true, - ruler: false, - delta:0, - corner: 0 - }; - this.zoomP.pStart = true; - console.warn('BioMap -> onPanStart -- vertically; implement me', evt); - let globalPos = pageToCanvas(evt, this.canvas); - let left = this.ruler.globalBounds.left; - // scroll view vs box select - if(left < (globalPos.x-evt.deltaX) && - (globalPos.x-evt.deltaX) < (left+this.ruler.bounds.width)){ - this.zoomP.ruler = true; - this._moveRuler(evt); - } else { - this.zoomP.ruler = false; - this.zoomP.start = this._pixelToCoordinate(globalPos.y-this.ruler.globalBounds.top-evt.deltaY); - if(this.zoomP.start < this.model.view.base.start){ - this.zoomP.start = this.model.view.base.start; - } - let ctx = this.context2d; - this.zoomP.corner = {top:globalPos.y-evt.deltaY,left:globalPos.x-evt.deltaX}; - ctx.lineWidth = 1.0; - ctx.strokeStyle = 'black'; - ctx.strokeRect( - Math.floor(globalPos.x-evt.deltaX), - Math.floor(globalPos.y-evt.deltaY), - Math.floor(evt.deltaX), - Math.floor(evt.deltaY) - ); - } - return true; - } - - // Moves ruler position on drag event - _moveRuler(evt){ - let delta = (evt.deltaY - this.zoomP.delta) / this.model.view.pixelScaleFactor; - if(this.model.view.visible.start+delta < this.model.view.base.start){ - delta = this.model.view.base.start - this.model.view.visible.start; - } else if(this.model.view.visible.stop+delta > this.model.view.base.stop){ - delta = this.model.view.base.stop - this.model.view.visible.stop; - } - this.model.view.visible.start += delta; - this.model.view.visible.stop += delta; - this._redrawViewport({start:this.model.view.visible.start, stop:this.model.view.visible.stop}); - this.zoomP.delta = evt.deltaY; - } - _onPan(evt){ - // block propegation if pan hasn't started - if (!this.zoomP || !this.zoomP.pStart) return true; - if(this.zoomP && this.zoomP.ruler){ - this._moveRuler(evt); - } else { - let globalPos = pageToCanvas(evt,this.canvas); - this.draw(); - let ctx = this.context2d; - ctx.lineWidth = 1.0; - ctx.strokeStyle = 'black'; - ctx.strokeRect( - Math.floor(this.zoomP.corner.left), - Math.floor(this.zoomP.corner.top), - Math.floor(globalPos.x - this.zoomP.corner.left), - Math.floor(globalPos.y -this.zoomP.corner.top ) - ); - } - return true; - } - _onPanEnd(evt) { - // TODO: send pan events to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) - console.warn('BioMap -> onPanEnd -- vertically; implement me', evt,this.model.view.base); - // block propegation if pan hasn't started - if (!this.zoomP || !this.zoomP.pStart) return true; - if(this.zoomP && this.zoomP.ruler){ - this._moveRuler(evt); - } else { - let globalPos = pageToCanvas(evt,this.canvas); - - // test if any part of the box select is in the ruler zone - let rLeft = this.ruler.globalBounds.left; - let rRight = this.ruler.globalBounds.right; - let lCorner = this.zoomP.corner.left < globalPos.x ? this.zoomP.corner.left : globalPos.x; - let rCorner = lCorner == this.zoomP.corner.left ? globalPos.x : this.zoomP.corner.left; - // if zoom rectangle contains the ruler, zoom, else populate popover - if(((lCorner <= rLeft) && (rCorner >= rLeft)) || ((lCorner <= rRight && rCorner >= rRight))){ - this.model.view.visible = this.model.view.base; - - this.zoomP.start = this._pixelToCoordinate(this.zoomP.corner.top-this.ruler.globalBounds.top); - this.zoomP.stop = this._pixelToCoordinate(globalPos.y-this.ruler.globalBounds.top); - let swap = this.zoomP.start < this.zoomP.stop; - let zStart = swap ? this.zoomP.start: this.zoomP.stop; - let zStop = swap ? this.zoomP.stop: this.zoomP.start; - - if(zStart < this.model.view.base.start){ - zStart = this.model.view.base.start; - } - if(zStop > this.model.view.base.stop){ - zStop = this.model.view.base.stop; - } - - this._redrawViewport({start:zStart, stop:zStop}); - } else { - - this._loadHitMap(); - let hits = []; - let swap = this.zoomP.corner.left < globalPos.x; - let swapV = this.zoomP.corner.top < globalPos.y; - this.hitMap.search({ - minX: swap ? this.zoomP.corner.left: globalPos.x, - maxX: swap ? globalPos.x : this.zoomP.corner.left, - minY: swapV ? this.zoomP.corner.top : globalPos.y, - maxY: swapV ? globalPos.y : this.zoomP.corner.top - }).forEach(hit => { - // temp fix, find why hit map stopped updating properly - if((hit.data.model.coordinates.start >= this.model.view.visible.start) && - (hit.data.model.coordinates.start <= this.model.view.visible.stop)){ - hits.push(hit.data); - } else if((hit.data.model.coordinates.stop >= this.model.view.visible.start) && - (hit.data.model.coordinates.stop <= this.model.view.visible.stop)){ - hits.push(hit.data); - } - }); - if(hits.length > 0){ - hits.sort((a,b) => { return a.model.coordinates.start - b.model.coordinates.start;}); - this.info.display = 'inline-block'; - this.info.top = this.ruler.globalBounds.top; - this.info.left = 0; - this.info.data = hits; - let names = hits.map(hit => { return hit.model.name; }); - //@awilkey: is this obsolete? - this.info.innerHTML= `
${names.join('\n')} <\p>`; - m.redraw(); - } else if(this.info.display !== 'none'){ - this.info.display = 'none'; - m.redraw(); - } - } - } - this.draw(); - this.zoomP = { - start:0, - end:0, - pStart: false, - ruler: false, - delta:0, - corner: { - top: 0, - left: 0 - } - }; -// this.zoomP.ruler = false; -// this.zoomP.pStart = false; - return true; // do not stop propagation - } - /** - * Converts a pixel position to the canvas' backbone coordinate system. - * - */ - _pixelToCoordinate(point){ - let coord = this.model.view.base; - let visc = this.model.view.visible; - let psf = this.model.view.pixelScaleFactor; - return ((visc.start*(coord.stop*psf - point) + visc.stop*(point - coord.start* psf))/(psf*(coord.stop - coord.start)))-(coord.start*-1); - } - - /** - * perform layout of backbone, feature markers, and feature labels. - */ - - _layout(layoutBounds) { - // TODO: calculate width based on # of SNPs in layout, and width of feature - // labels - // Setup Canvas - //const width = Math.floor(100 + Math.random() * 200); - this.lb = layoutBounds; - console.log('BioMap -> layout'); - const width = Math.floor(layoutBounds.width/this.appState.bioMaps.length); - this.children = []; - this.domBounds = new Bounds({ - left:layoutBounds.left, - top: layoutBounds.top, - width: width > 300 ? width:300, - height: layoutBounds.height - }); - - this.bounds = new Bounds({ - left: 0, - top: layoutBounds.top + 40, - width: this.domBounds.width, - height: Math.floor(this.domBounds.height - 140) // set to reasonably re-size for smaller windows - }); - //Add children tracks - this.bbGroup = new Group({parent:this}); - this.bbGroup.bounds = new Bounds({ - top:0, - left:0, - width:10 - }); - this.bbGroup.model = this.model; - this.backbone = new MapTrack({parent:this}); - this.bbGroup.addChild(this.backbone); - this.model.view.backbone = this.backbone.backbone.globalBounds; - this.ruler = new Ruler({parent:this, bioMap:this.model}); - this.bbGroup.addChild(this.ruler); - this.children.push(this.bbGroup); - let qtl = new QtlTrack({parent:this}); - if(this.domBounds.width < qtl.globalBounds.right+30){ - this.domBounds.width = qtl.globalBounds.right + 50; - } - this.children.push(qtl); - //load local rBush tree for hit detection - this._loadHitMap(); - //let layout know that width has changed on an element; - m.redraw(); - } - - _loadHitMap(){ - let hits = []; - let childrenHits = this.children.map(child => { - return child.hitMap; - }); - childrenHits.forEach(child =>{ - hits = hits.concat(child); - }); - this.locMap.clear();// = rbush(); - this.locMap.load(hits); - } - - _redrawViewport(coordinates){ - this.model.view.visible = { - start: coordinates.start, - stop: coordinates.stop - }; - this.backbone.loadLabelMap(); - this.draw(); - - let cMaps = document.getElementsByClassName('cmap-correspondence-map'); - [].forEach.call(cMaps, el =>{ - el.mithrilComponent.draw(); - }); - // move top of popover if currently visible - if(this.info.display !== 'none'){ - this.info.top = this.info.data[0].globalBounds.top; - } - m.redraw(); - } -} diff --git a/src/canvas/layout/FeatureTrack.js b/src/canvas/layout/FeatureTrack.js new file mode 100644 index 00000000..4265a902 --- /dev/null +++ b/src/canvas/layout/FeatureTrack.js @@ -0,0 +1,145 @@ +/** + * FeatureTrack + * A SceneGraphNode representing a collection of tracks. + * + * @extends SceneGraphNodeTrack + */ + +import {Bounds} from '../../model/Bounds'; + +import {SceneGraphNodeGroup} from '../node/SceneGraphNodeGroup'; +import {SceneGraphNodeTrack} from '../node/SceneGraphNodeTrack'; +import {ManhattanPlot} from './ManhattanPlot'; +import {QtlTrack} from './QtlTrack'; + +export class FeatureTrack extends SceneGraphNodeTrack { + + /** + * Constructor - sets up a track that's a group of QTL rectangles + * @param params + */ + + constructor(params) { + super(params); + this.model = this.parent.model; + const b = this.parent.bounds; + this.trackPos = params.position || 1; + + let left = this.trackPos < 0 ? 10 : this.parent.bbGroup.bounds.right; + this.bounds = new Bounds({ + allowSubpixel: false, + top: b.top, + left: left, + width: 0, + height: b.height + }); + if(this.parent.model.tracks) { + let tracks = this.trackPos === 1 ? this.parent.tracksLeft : this.parent.tracksRight; + tracks.forEach((track, order) => { + // newFeatureTrack is a group with two components, the feature data track, and the feature label track + //track.appState = this.parent.appState; + let newFeatureTrack = new SceneGraphNodeGroup({parent:this}); + newFeatureTrack.model = this.model; + newFeatureTrack.config = track; + newFeatureTrack.order = order; + + let trackLeft = order === 0 ? 0 : this.children[order-1].bounds.right; + trackLeft += this.model.config.qtl.padding; + + newFeatureTrack.bounds = new Bounds({ + allowSubpixel: false, + top: 0, + left: trackLeft, + width: this.model.config.qtl.trackMinWidth, + height: b.height + }); + + + let featureData = {}; + if(track.type === 'qtl') { + newFeatureTrack.title = track.title || this.model.config.qtl.title || track.filters[0]; + featureData = new QtlTrack({parent:newFeatureTrack , config: track}); + } else if( track.type === 'manhattan') { + newFeatureTrack.sources = this.parent.appState.sources; + newFeatureTrack.title = track.title || this.model.config.manhattan.title || 'Manhattan'; + featureData = new ManhattanPlot({parent:newFeatureTrack, config: track}); + } + + newFeatureTrack.addChild(featureData); + if(featureData.globalBounds.right > newFeatureTrack.globalBounds.right){ + newFeatureTrack.bounds.right += featureData.bounds.right; + } + + if(newFeatureTrack.globalBounds.right > this.globalBounds.right){ + this.bounds.right = this.bounds.left + (newFeatureTrack.globalBounds.right - this.globalBounds.left); + } + + this.addChild(newFeatureTrack); + }); + } else { + this.parent.model.tracks = []; + } + + } + + /** + * + */ + + get visible() { + let visible = []; + this.children.forEach(child => { + visible = visible.concat(child.visible); + }); + return visible; + //return visible.concat([{data:this}]); // debugging statement to test track width bounds + } + + /** + * Debug draw to check track positioning + * @param ctx + */ + + draw(ctx) { + ctx.save(); + ctx.globalAlpha = .5; + ctx.fillStyle = '#ADD8E6'; + this.children.forEach(child => { + let cb = child.globalBounds; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination + ctx.fillRect( + Math.floor(cb.left), + Math.floor(cb.top), + Math.floor(cb.width), + Math.floor(cb.height) + ); + }); + //ctx.fillStyle = 'red'; + //let cb = this.globalBounds; + //ctx.fillRect( + // Math.floor(cb.left), + // Math.floor(cb.top), + // Math.floor(cb.width), + // Math.floor(cb.height) + //); + ctx.restore(); + } + + /** + * Get RTree children that are visible in the canvas' current zoom bounds + * @returns {Array} + */ + + get hitMap() { + //return []; + // console.log('hits child',child); + let hits = []; + this.children.forEach(child => { + return child.children.map(qtlGroup => { + hits = hits.concat(qtlGroup.hitMap); + }); + }); + return hits; + } +} diff --git a/src/canvas/layout/ManhattanPlot.js b/src/canvas/layout/ManhattanPlot.js new file mode 100644 index 00000000..e569bc67 --- /dev/null +++ b/src/canvas/layout/ManhattanPlot.js @@ -0,0 +1,175 @@ +/** + * ManhattanPlot + * A SceneGraphNodeTrack representing a Manhattan Plot. + * + * @extends SceneGraphNodeTrack + */ +import {Bounds} from '../../model/Bounds'; + +import {SceneGraphNodeTrack} from '../node/SceneGraphNodeTrack'; +import {Dot} from '../geometry/Dot'; +import {manhattanRuler} from '../geometry/manhattanRuler'; + +export class ManhattanPlot extends SceneGraphNodeTrack { + + /** + * Constructor - sets up a track that's a group of QTL rectangles + * @param params + */ + + constructor(params) { + super(params,); + console.log('manhattan -> constructor', params); + let manhattanPlot = params.config; + const b = this.parent.bounds; + this.trackPos = params.position || 1; + this.bounds = new Bounds({ + allowSubpixel: false, + top: 0, + left: 0, + width: 0, + height: b.height + }); + if (manhattanPlot !== null) { + let manhattanInfo = manhattanPlot; + //merge configuration information with default config + for( let key in this.parent.model.config.manhattan){ + if(!manhattanInfo.hasOwnProperty(key)){ + manhattanInfo[key] = this.parent.model.config.manhattan[key]; + } + } + manhattanInfo.lines.forEach(line => { + if(!line.lineWeight){ + line.lineWeigth = manhattanInfo.featureLineWeight; + } + if(!line.lineColor){ + line.lineColor = manhattanInfo.featureLineColor; + } + }); + + // If data hasn't been attached to this map to plot, filter and attach it. + if (manhattanInfo.data === undefined) { + manhattanInfo.view = { + start: 0, + stop: manhattanInfo.maxValue || 0 + }; + let baseData = this.parent.sources.filter(model => { + return model.id === manhattanInfo.dataId; + }); + + let prefix = manhattanInfo.prefix || ''; + manhattanInfo.data = baseData[0].parseResult.data.filter(mdata => { + if (prefix + mdata[manhattanInfo.targetField] === this.parent.model.name) { + if (manhattanInfo.max === undefined && -Math.log10(mdata[manhattanInfo.pField]) >= manhattanInfo.view.stop) { //determine max value while filtering data + manhattanInfo.view.stop = Math.ceil(-Math.log10(mdata[manhattanInfo.pField])); + + } + return true; + } + return false; + }); + } + + //Draw manhattan plot + //let left = this.parent.bbGroup.bounds.right; + + this.bounds = new Bounds({ + allowSubpixel: false, + top: 0, + left: 0, + width: manhattanInfo.displayWidth || 0, + height: b.height + }); + + let fmData = []; + let locData = []; + this.fmData = fmData; + + this.manhattanMarks = manhattanInfo.data.map(model => { + model.coordinates = { + start: model[manhattanInfo.posField], + depth: -Math.log10(model[manhattanInfo.pField]) + }; + if((model.coordinates.start > this.parent.model.view.base.stop) || + (model.coordinates.start < this.parent.model.view.base.start) ){ return; } + + let fm = new Dot({ + featureModel: model, + parent: this, + bioMap: this.parent.model, + config: manhattanInfo + }); + fmData.push(fm); + + let loc = { + minY: model.coordinates.start, + maxY: model.coordinates.start, + minX: fm.globalBounds.left, + maxX: fm.globalBounds.right, + data: fm + }; + + locData.push(loc); + return fm; + }); + + this.ruler ={ + minY: 0, + maxY: 100000000, + minX: this.globalBounds.left, + maxX: this.globalBounds.right, + data: new manhattanRuler({ + featureModel : manhattanInfo, + parent: this, + config: manhattanInfo + }) + }; + + this.locMap.load(locData); + this.tags = ['manhattan']; + } + } + + /** + * + */ + + get visible() { + return this.locMap.all().concat(this.ruler); + } + + // /** + // * Debug draw to check track positioning + // * @param ctx + // */ + + draw(ctx) { + // ctx.save(); + // ctx.globalAlpha = .5; + // ctx.fillStyle = 'green'; + // let cb = this.globalBounds; + // ctx.fillRect(cb.left,cb.top,cb.width,cb.height); + // ctx.restore(); + + this.children.forEach(child => child.draw(ctx)); + } + + // /** + // * Get RTree children that are visible in the canvas' current zoom bounds +// * @returns {Array} +// */ +// + get hitMap() { + //return this.locMap.all(); + return this.children.map(child => { + return { + minY: child.globalBounds.top, + maxY: child.globalBounds.bottom, + minX: child.globalBounds.left, + maxX: child.globalBounds.right, + data: child + }; + }); + + } +} \ No newline at end of file diff --git a/src/canvas/layout/MapTrack.js b/src/canvas/layout/MapTrack.js index dc4fa951..0e0182bb 100644 --- a/src/canvas/layout/MapTrack.js +++ b/src/canvas/layout/MapTrack.js @@ -1,8 +1,8 @@ /** - * MapTrack - * A SceneGraphNode representing a backbone, simply a rectangle representing - * the background. - */ + * MapTrack + * A SceneGraphNode representing a backbone, simply a rectangle representing + * the background. + */ import knn from 'rbush-knn'; import {SceneGraphNodeTrack} from '../node/SceneGraphNodeTrack'; @@ -12,65 +12,72 @@ import {FeatureMark} from '../geometry/FeatureMark'; import {MapBackbone} from '../geometry/MapBackbone'; import {FeatureLabel} from '../geometry/FeatureLabel'; -export class MapTrack extends SceneGraphNodeTrack { +export class MapTrack extends SceneGraphNodeTrack { + + /** + * + * @param params + */ constructor(params) { - console.log("MapTrack-> Constructing Map"); + console.log('MapTrack-> Constructing Map'); super(params); const b = this.parent.bounds; this.model = this.parent.model; //const backboneWidth = b.width * 0.2; - const backboneWidth = this.model.config.backboneWidth; + const backboneWidth = this.model.config.backbone.width; this.bounds = new Bounds({ allowSubpixel: false, - top: b.top, - left: this.model.config.rulerLabelSize * 10,//b.width * 0.5 - backboneWidth * 0.5, + top: 0, + left: 0, width: backboneWidth, height: b.height }); this.mC = this.parent.mapCoordinates; - this.backbone = new MapBackbone({ parent: this, bioMap: this.model}); + this.backbone = new MapBackbone({parent: this, bioMap: this.model,config: this.model.config.backbone}); this.addChild(this.backbone); // calculate scale factor between backbone coordinates in pixels - this.model.view.pixelScaleFactor = this.backbone.bounds.height/this.model.length; + this.model.view.pixelScaleFactor = this.backbone.bounds.height / this.model.length; this.model.view.backbone = this.globalBounds; // Setup groups for markers and labels - let markerGroup = new SceneGraphNodeGroup({parent:this}); + let markerGroup = new SceneGraphNodeGroup({parent: this}); this.addChild(markerGroup); this.markerGroup = markerGroup; markerGroup.bounds = this.backbone.bounds; this.addChild(markerGroup); - let labelGroup = new SceneGraphNodeGroup({parent:this}); + let labelGroup = new SceneGraphNodeGroup({parent: this}); this.addChild(labelGroup); this.labelGroup = labelGroup; labelGroup.bounds = new Bounds({ top: 0, left: this.backbone.bounds.right + 1, height: this.bounds.height, - width: 20 + width: 0 }); // Filter features for drawing - this.filteredFeatures = this.model.features.filter( model => { + this.filteredFeatures = this.model.features.filter(model => { return model.length <= 0.00001; }); //Place features and their labels, prepare to add to rtree let fmData = []; let lmData = []; - this.featureMarks = this.filteredFeatures.map( model => { + this.featureMarks = this.filteredFeatures.map(model => { let fm = new FeatureMark({ featureModel: model, parent: this.backbone, - bioMap: this.model + bioMap: this.model, + config: this.model.config.marker }); let lm = new FeatureLabel({ featureModel: model, parent: this.labelGroup, - bioMap: this.parent.model + bioMap: this.parent.model, + config: this.model.config.marker }); markerGroup.addChild(fm); labelGroup.addChild(lm); @@ -79,28 +86,35 @@ export class MapTrack extends SceneGraphNodeTrack { maxY: model.coordinates.stop, minX: fm.globalBounds.left, maxX: fm.globalBounds.right, - data:fm + data: fm }); lmData.push({ minY: model.coordinates.start, maxY: model.coordinates.stop, minX: lm.globalBounds.left, - maxX: lm.globalBounds.right, + maxX: lm.globalBounds.left + this.labelGroup.bounds.width, data: lm }); + if (lm.bounds.right > this.labelGroup.bounds.right) this.labelGroup.bounds.right = lm.bounds.right; return fm; }); - // Load group rtrees for markers and labels + // Load group rTrees for markers and labels markerGroup.locMap.load(fmData); labelGroup.locMap.load(lmData); // load this rtree with markers (elements that need hit detection) this.locMap.load(fmData); } - get visible(){ + /** + * + * @returns {*[]} + */ + + get visible() { let coord = this.parent.model.view.base; let visc = this.parent.model.view.visible; + let vis = [{ minX: this.bounds.left, maxX: this.bounds.right, @@ -117,57 +131,69 @@ export class MapTrack extends SceneGraphNodeTrack { let labels = []; let start = visc.start; let stop = visc.stop; - let psf = this.labelGroup.children[0].pixelScaleFactor; - let step =((visc.start*(coord.stop*psf - 12) + visc.stop*(12 - coord.start* psf))/(psf*(coord.stop - coord.start)) - start) - (coord.start*-1); - for(let i = start; i < stop; i+=step){ - - let item = knn( this.labelGroup.locMap, this.labelGroup.children[0].globalBounds.left,i,1)[0]; - if(labels.length === 0){ - labels.push(item); - continue; - } - let last = labels[labels.length-1]; - if(item != last && (item.minY > (last.maxY + step))){ - labels.push(item); - } + let psf = this.labelGroup.children[0].pixelScaleFactor; + let step = ((visc.start * (coord.stop * psf - 12) + visc.stop * (12 - coord.start * psf)) / (psf * (coord.stop - coord.start)) - start) - (coord.start * -1); + for (let i = start; i < stop; i += step) { + + let item = knn(this.labelGroup.locMap, this.labelGroup.children[0].globalBounds.left, i, 1)[0]; + if (labels.length === 0) { + labels.push(item); + continue; + } + let last = labels[labels.length - 1]; + if (item !== last && (item.minY > (last.maxY + step))) { + labels.push(item); + } } vis = vis.concat(labels); - //vis = vis.concat([{data:this}]); return vis; } - get hitMap(){ + /** + * + */ + + get hitMap() { let bbGb = this.backbone.globalBounds; - return this.markerGroup.children.map( child =>{ + return this.markerGroup.children.map(child => { return { - minY: child.globalBounds.bottom+1, - maxY: child.globalBounds.top-1, - minX: bbGb.left , - maxX: bbGb.right , + minY: child.globalBounds.bottom + 1, + maxY: child.globalBounds.top - 1, + minX: bbGb.left, + maxX: bbGb.right, data: child }; }); } - draw(ctx){ + /** + * + * @param ctx + */ + + draw(ctx) { let gb = this.globalBounds || {}; ctx.fillStyle = 'blue'; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(gb.top), Math.floor(gb.width), Math.floor(gb.height) - ); + ); ctx.fillStyle = 'green'; gb = this.labelGroup.globalBounds || {}; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(gb.left), Math.floor(gb.top), Math.floor(gb.width), Math.floor(gb.height) - ); + ); } - loadLabelMap(){ + loadLabelMap() { } } diff --git a/src/canvas/layout/QtlTrack.js b/src/canvas/layout/QtlTrack.js index 6f567d45..45d8f3f1 100644 --- a/src/canvas/layout/QtlTrack.js +++ b/src/canvas/layout/QtlTrack.js @@ -1,110 +1,122 @@ /** - * QtlTrack - * A SceneGraphNode representing a collection of QTLs. - */ + * QtlTrack + * A SceneGraphNode representing a collection of QTLs. + * + * @extends SceneGraphNodeTrack + */ import {SceneGraphNodeTrack} from '../node/SceneGraphNodeTrack'; -import {SceneGraphNodeGroup} from '../node/SceneGraphNodeGroup'; import {Bounds} from '../../model/Bounds'; import {QTL} from '../geometry/QTL'; -export class QtlTrack extends SceneGraphNodeTrack { +export class QtlTrack extends SceneGraphNodeTrack { + + /** + * Constructor - sets up a track that's a group of QTL rectangles + * @param params + */ + + /** + * TODO: Allow for subtracks + */ constructor(params) { super(params); - console.log('QtlTrack -> constructor',this.parent.domBounds); - const b = this.parent.bounds; + + this.filteredFeatures = []; + let b = this.parent.bounds; this.bounds = new Bounds({ allowSubpixel: false, - top: this.parent.bounds.top, - left: this.parent.backbone.bounds.right + 100, - width: 50, + top: 0, + left: 0, + width: this.parent.model.config.qtl.trackMinWidth, height: b.height }); - if(this.parent.model.qtlGroups && this.parent.model.qtlGroups.length > 0){ - console.log('qtlGroups', this.parent.model.qtlGroups); - let qtlGroups = this.parent.model.qtlGroups; - for( let i = 0 ; i < qtlGroups.length; i++){ - let qtlConf = qtlGroups[i]; - if (typeof qtlConf.filters === 'string'){ qtlConf.filters = [qtlConf.filters];} - if (typeof qtlConf.trackColor === 'string'){ qtlConf.trackColor = [qtlConf.trackColor];} - let qtlGroup = new SceneGraphNodeGroup({parent:this, tags:qtlConf.filters.slice(0)}); - this.addChild(qtlGroup); - let offset = this.qtlGroup !== undefined ? this.qtlGroup.bounds.right + 20 : 0; - this.qtlGroup = qtlGroup; - qtlGroup.bounds = new Bounds({ - top:0, - left: offset, - width:20, - height: b.height - }); - - this.mapCoordinates = this.parent.mapCoordinates; - this.filteredFeatures = []; - qtlConf.filters.forEach( (filter,order) => { - var test = this.parent.model.features.filter( model => { - return model.tags[0].match(filter) !== null; - }) - if(test.length === 0){ - // get rid of any tags that don't actually get used - qtlConf.filters.splice(order,1); - } else { - this.filteredFeatures = this.filteredFeatures.concat(test); - } - }); - this.filteredFeatures.sort((a,b)=>{return a.coordinates.start - b.coordinates.start;}); - let fmData = []; - this.maxLoc = 0; - this.qtlMarks = this.filteredFeatures.map( model => { - let fm = new QTL ({ - featureModel: model, - parent: this.qtlGroup, - bioMap: this.parent.model, - initialConfig:this.parent.model.qtlGroups[i] - }); - qtlGroup.addChild(fm); - let loc = { - minY: model.coordinates.start, - maxY: model.coordinates.stop, - minX: fm.globalBounds.left, - maxX: fm.globalBounds.right, - data:fm - }; - qtlGroup.locMap.insert(loc); - fmData.push(loc); - if(fm.globalBounds.right > this.globalBounds.right){ - this.maxLoc = this.globalBounds.right; - this.bounds.right = this.globalBounds.left + (fm.globalBounds.right - this.globalBounds.left); - qtlGroup.bounds.right = qtlGroup.bounds.left + (fm.globalBounds.right - qtlGroup.globalBounds.left) + fm.offset; //set to fm.textWidth - } - return fm; - }); - this.locMap.load(fmData); + + let qtlConf = params.config; + for( let key in this.parent.model.config.qtl){ + if(!qtlConf.hasOwnProperty(key)){ + qtlConf[key] = this.parent.model.config.qtl[key]; } - } else { // TODO: Rewrite so that this isn't required to be here - let qtlGroup = new SceneGraphNodeGroup({parent:this}); - this.addChild(qtlGroup); - this.qtlGroup = qtlGroup; - - qtlGroup.bounds = new Bounds({ - top:0, - left:0, - width:0, - height: b.height - }); } + + qtlConf.filters.forEach( (filter,order) => { + var test = this.parent.model.features.filter( model => { + return model.tags[0].match(filter) !== null; + }); + if(test.length === 0){ + // get rid of any tags that don't actually get used + qtlConf.filters.splice(order,1); + } else { + this.filteredFeatures = this.filteredFeatures.concat(test); + } + }); + + this.filteredFeatures.sort((a,b)=>{return a.coordinates.start - b.coordinates.start;}); + let fmData = []; + + + this.maxLoc = 0; + this.qtlMarks = this.filteredFeatures.map( model => { + let fm = new QTL ({ + featureModel: model, + parent: this, + bioMap: this.parent.model, + initialConfig: qtlConf, + config: this.parent.model.config.qtl + }); + + this.addChild(fm); + + let loc = { + minY: model.coordinates.start, + maxY: model.coordinates.stop, + minX: fm.globalBounds.left, + maxX: fm.globalBounds.right, + data:fm + }; + + this.locMap.insert(loc); + + fmData.push(loc); + + if(fm.globalBounds.right > this.globalBounds.right){ + this.bounds.right = fm.globalBounds.right - this.globalBounds.left; + } + + return fm; + }); + this.locMap.clear(); + this.locMap.load(fmData); } - get visible(){ + /** + * + */ + + get visible() { + // let visible = []; + // this.children.forEach(child => { + // visible = visible.concat(child.locMap.all()); + // }); + // + // return visible; return this.locMap.all(); //return this.locMap.all().concat([{data:this}]); // debugging statement to test track width bounds - } - - draw(ctx){ + } + + /** + * Debug draw to check track positioning + * @param ctx + */ + + draw(ctx) { ctx.save(); ctx.globalAlpha = .5; ctx.fillStyle = '#ADD8E6'; - this.children.forEach( child => { + this.children.forEach(child => { let cb = child.globalBounds; + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination ctx.fillRect( Math.floor(cb.left), Math.floor(cb.top), @@ -115,31 +127,21 @@ export class QtlTrack extends SceneGraphNodeTrack { ctx.restore(); } - get hitMap(){ - //return []; - let hits = []; - let childPos = this.children.map(child => { - return child.children.map( qtlGroup =>{ - return { - minY: qtlGroup.globalBounds.top, - maxY: qtlGroup.globalBounds.bottom, - minX: qtlGroup.globalBounds.left, - maxX: qtlGroup.globalBounds.right , - data: qtlGroup - }; - }); - }); - childPos.forEach( childArray =>{ - hits = hits.concat(childArray); + /** + * Get RTree children that are visible in the canvas' current zoom bounds + * @returns {Array} + */ + + get hitMap() { + //return this.locMap.all(); + return this.children.map(child => { + return { + minY: child.globalBounds.top, + maxY: child.globalBounds.bottom, + minX: child.globalBounds.left, + maxX: child.globalBounds.right, + data: child + }; }); - return hits; - // return { - // minY: child.globalBounds.top, - // maxY: child.globalBounds.bottom, - // minX: child.globalBounds.left, - // maxX: child.globalBounds.right , - // data: child - // }; - //}); } } diff --git a/src/canvas/node/SceneGraphNodeBase.js b/src/canvas/node/SceneGraphNodeBase.js index e15f2583..b3810c3f 100644 --- a/src/canvas/node/SceneGraphNodeBase.js +++ b/src/canvas/node/SceneGraphNodeBase.js @@ -1,27 +1,27 @@ /** - * SceneGraphNodeBase - * Base Class representing a drawable element in canvas scenegraph - */ + * SceneGraphNodeBase + * Base Class representing a drawable element in canvas scenegraph + */ -import rbush from 'rbush'; +import rbush from 'rbush'; -import { Bounds } from '../../model/Bounds'; +import {Bounds} from '../../model/Bounds'; export class SceneGraphNodeBase { /** - * Create a SceneGraphNode. - * Constructor uses ES6 destructuring of parameters from an object. - * e.g. new SceneGraphNode({param: .., param2, etc.}) - * - * @param {Object} params - having the following properties: - * @param {String} tag - an label or slug - * @param {Object} parent - the parent node - * @param {Object} bounds - local Canvas bounds, relative to our parent. - This is not the same as DOM bounds of the canvas element! - * @param {Number} rotation - degrees, default 0. - * @returns {Object} - */ + * Create a SceneGraphNode. + * Constructor uses ES6 destructuring of parameters from an object. + * e.g. new SceneGraphNode({param: .., param2, etc.}) + * + * @param {Object} params - having the following properties: + * @param {Array} tags - an label or slug + * @param {Object} parent - the parent node + * @param {Object} bounds - local Canvas bounds, relative to our parent. + * This is not the same as DOM bounds of the canvas element! + * @param {Number} rotation - degrees, default 0. + */ + constructor({parent, bounds, rotation = 0, tags = []}) { this.parent = parent; this._rotation = rotation; @@ -32,29 +32,56 @@ export class SceneGraphNodeBase { this._visble = []; } - /* getters and setters */ + /* getters and setters */ /* define getters for our properties; note subclasses can override setters, e.g. to perform layout or calculations based on new state */ + /* getters */ - get children() { return this._children; } - get bounds() { return this._bounds; } - get rotation() { return this._rotation; } - get tags() { return this._tags; } - /* setters */ - set children(b) { this._children = b;} - set bounds(b) { this._bounds = b; } - set rotation(degrees) { this._rotation = degrees; } - set tags(tags) { this._tags = tags; } + /** + * Children scene graph nodes + * @returns {Array|*} any child nodes this node has + */ + + get children() { + return this._children; + } + + /** + * Local bounds + * @returns {*} local bounds + */ + + get bounds() { + return this._bounds; + } + + /** + * Rotation applied on this and subsequent children + * @returns {*} rotation + */ + + get rotation() { + return this._rotation; + } /** - * Traverse all parents bounds to calculate self Bounds on Canvas. - * - * @returns {Object} - Bounds instance - */ + * Info tags + * @returns {*} tags + */ + + get tags() { + return this._tags; + } + + /** + * Traverse all parents bounds to calculate self Bounds on Canvas. + * @returns {Object} - Bounds instance + */ + get globalBounds() { console.assert(this.bounds, 'bounds missing'); - if(! this.parent) return this.bounds; + if (!this.parent) return this.bounds; let gb = this.parent.globalBounds; return new Bounds({ top: this.bounds.top + gb.top, @@ -67,46 +94,90 @@ export class SceneGraphNodeBase { } /** - * Use rbush to returni children nodes that may be visible. + * Use rbush to return children nodes that may be visible. * At this level, it is assumed that there is no viewport * constraints to the filter. * - * @retrun {Array} - array of rbush nodes + * @return {Array} - array of rbush nodes */ - get visible(){ + + get visible() { let vis = []; - let childVisible = this.children.map( child => { + let childVisible = this.children.map(child => { return child.locMap.all(); }); - childVisible.forEach(item =>{ vis = vis.concat(item);}); + childVisible.forEach(item => { + vis = vis.concat(item); + }); return vis; } /** * Traverse children, returning hitmap - * * @returns {Array} - array of rbush entries */ - get hitMap(){ + get hitMap() { let hits = []; - let childMap = this.children.map( child => { + let childMap = this.children.map(child => { return child.hitMap; }); - childMap.forEach(item =>{ hits = hits.concat(item);}); + childMap.forEach(item => { + hits = hits.concat(item); + }); return hits; } - /* public methods/* + /* setters */ + + /** + * Child scene graph nodes + * @param {Array|*} b + */ + + set children(b) { + this._children = b; + } + + /** + * Nodes local bounds + * @param b - bounds object + */ + + set bounds(b) { + this._bounds = b; + } + + /** + * Rotation + * @param {number} degrees - rotation in degrees + */ + + set rotation(degrees) { + this._rotation = degrees; + } + + /** + * Tags + * @param {array} tags - object's descriptive tags + */ + + set tags(tags) { + this._tags = tags; + } + + /* public methods */ + /** * Translate coordinates to canvas space. When an element wants to draw on * canvas, it requires translating into global coordinates for the canvas. * * @param {Object} params - object with following properties: - * @param {Number} x - * @param {Number} y - * @returns {Object} - { x, y } + * @param {Number} x - x location + * @param {Number} y - y location + * @returns {Object} - { x, y } x,y location in global terms */ + translatePointToGlobal({x, y}) { let gb = this.globalBounds; return {x: x + gb.left, y: y + gb.top}; @@ -118,35 +189,38 @@ export class SceneGraphNodeBase { * * @param {object} node - SceneGraphNode derived item to insert as a child **/ - addChild(node){ - if(node.parent){ + + addChild(node) { + if (node.parent) { node.parent.removeChild(node); } node.parent = this; - if(this._children.indexOf(node) === -1) this._children.push(node); + if (this._children.indexOf(node) === -1) this._children.push(node); } /** * Removes a child node from the _children array - * and changes child node's parent to undefined + * and changes child node's parent to undefined * * @param {object} node - SceneGraphNode derived node to remove **/ - removeChild(node){ + + removeChild(node) { //TODO: May need to use a indexOf polyfill if targeting IE < 9 let index = this._children.indexOf(node); - if(index > -1){ - this._children.splice(index,1); + if (index > -1) { + this._children.splice(index, 1); } node.parent = null; } + /** * Traverse children and call their draw on the provided context * - * #param {object} ctx - canvas context - * + * @param {object} ctx - canvas context */ - draw(ctx){ + + draw(ctx) { this.children.forEach(child => child.draw(ctx)); } } diff --git a/src/canvas/node/SceneGraphNodeCanvas.js b/src/canvas/node/SceneGraphNodeCanvas.js index d44b1a0f..c16e7709 100644 --- a/src/canvas/node/SceneGraphNodeCanvas.js +++ b/src/canvas/node/SceneGraphNodeCanvas.js @@ -1,8 +1,8 @@ /** - * SceneGraphNodeCanvas - * Mithril component representing a html5 canvas element. - * - */ + * SceneGraphNodeCanvas + * Mithril component representing a html5 canvas element. + * + */ import m from 'mithril'; import PubSub from 'pubsub-js'; @@ -17,10 +17,15 @@ import {selectedMap} from '../../topics'; import {Bounds} from '../../model/Bounds'; import {SceneGraphNodeBase} from './SceneGraphNodeBase'; +export class SceneGraphNodeCanvas + extends mix(SceneGraphNodeBase) + .with(DrawLazilyMixin, RegisterComponentMixin) { -export class SceneGraphNodeCanvas - extends mix(SceneGraphNodeBase) - .with(DrawLazilyMixin, RegisterComponentMixin) { + /** + * constructor + * @param model - data model for canvas + * @param appState - app state model + */ constructor({model, appState}) { super({}); @@ -28,26 +33,32 @@ export class SceneGraphNodeCanvas this.appState = appState; this.verticalScale = 1; this.info = { - visible:false, - top:0, - left:0 + visible: false, + top: 0, + left: 0 }; this._gestureRegex = { - pan: new RegExp('^pan'), + pan: new RegExp('^pan'), pinch: new RegExp('^pinch'), - tap: new RegExp('^tap'), + tap: new RegExp('^tap'), wheel: new RegExp('^wheel') }; } + /** + * Getter if canvas has focus + * @returns {boolean} + */ + get selected() { return this.appState.selection.bioMaps.indexOf(this) !== -1; } - /** * mithril lifecycle method + * @param vnode */ + oncreate(vnode) { super.oncreate(vnode); this.canvas = this.el = vnode.dom; @@ -57,12 +68,14 @@ export class SceneGraphNodeCanvas /** * mithril lifecycle method + * @param vnode - current virtual dom node */ + onupdate(vnode) { // TODO: remove this development assistive method console.assert(this.el === vnode.dom); let b = new Bounds(this.el.getBoundingClientRect()); - console.log('BioMap.onupdate', b.width, b.height, this.el); + console.log('BioMap.onupdate', this.el.mithrilComponent, b); } /** @@ -70,28 +83,29 @@ export class SceneGraphNodeCanvas */ view() { // store these bounds, for checking in drawLazily() - if(this.domBounds && ! this.domBounds.isEmptyArea) { + if (this.domBounds && !this.domBounds.isEmptyArea) { this.lastDrawnMithrilBounds = this.domBounds; } let b = this.domBounds || {}; let selectedClass = this.selected ? 'selected' : ''; - return m('canvas', { - class: `cmap-canvas cmap-biomap ${selectedClass}`, - style: `left: ${b.left}px; top: ${b.top}px; + return m('canvas', { + class: `cmap-canvas cmap-biomap ${selectedClass}`, + style: `left: ${b.left}px; top: ${b.top}px; width: ${b.width}px; height: ${b.height}px; transform: rotate(${this.rotation}deg);`, - width: b.width, - height: b.height - }); + width: b.width, + height: b.height + }); } /** - * draw our scenegraph children our canvas element + * draw our scene graph children on canvas element */ + draw() { let ctx = this.context2d; - if(! ctx) return; - if(! this.domBounds) return; + if (!ctx) return; + if (!this.domBounds) return; ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.save(); //ctx.translate(0.5, 0.5); // prevent subpixel rendering of 1px lines @@ -99,44 +113,63 @@ export class SceneGraphNodeCanvas ctx.restore(); // store these bounds, for checking in drawLazily() this.lastDrawnCanvasBounds = this.bounds; + this.dirty = false; } - /** + + /** * custom gesture event dispatch listener; see LayoutContainer + * @param evt + * @returns {boolean} Don't stop event propagation */ + handleGesture(evt) { - if(evt.type.match(this._gestureRegex.tap)) { + if (evt.type.match(this._gestureRegex.tap)) { return this._onTap(evt); } else if (evt.type.match(this._gestureRegex.pinch)) { return this._onZoom(evt); } - else if(evt.type.match(this._gestureRegex.wheel)) { + else if (evt.type.match(this._gestureRegex.wheel)) { return this._onZoom(evt); } - else if(evt.type.match(this._gestureRegex.pan)) { - if(evt.type === 'panend'){ + else if (evt.type.match(this._gestureRegex.pan)) { + if (evt.type === 'panend') { return this._onPanEnd(evt); - } else if ( evt.type === 'panstart'){ + } else if (evt.type === 'panstart') { return this._onPanStart(evt); } else { return this._onPan(evt); } } - return false; // dont stop evt propagation + return false; // don't stop evt propagation } + /** + * Handle zoom event + * @param evt - zoom event (mousewheel or gesture) + * @returns {boolean} don't stop event propagation + * @private + */ + _onZoom(evt) { // TODO: send zoom event to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) + // (don't scale the canvas element itself) console.warn('BioMap -> onZoom -- implement me', evt); return false; // stop event propagation } + /** + * Tap/Click event + * @param evt + * @returns {boolean} Don't stop event propagation by default + * @private + */ + _onTap(evt) { let sel = this.appState.selection.bioMaps; let i = sel.indexOf(this); - if(i === -1) { + if (i === -1) { sel.push(this); } else { @@ -149,24 +182,48 @@ export class SceneGraphNodeCanvas }); return false; } + + /** + * Pan event that isn't first or last in sequence + * @param evt + * @returns {boolean} + * @private + */ + _onPan(evt) { // TODO: send pan events to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) - if(evt.direction & Hammer.DIRECTION_VERTICAL) { + // (don't scale the canvas element itself) + if (evt.direction && Hammer.DIRECTION_VERTICAL) { console.warn('BioMap -> onPan -- vertically; implement me', evt); return false; // stop event propagation } return false; // do not stop propagation } + + /** + * First pan event in sequence + * @param evt + * @returns {boolean} + * @private + */ + _onPanStart(evt) { // TODO: send pan events to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) - console.warn('BioMap -> onPanStart -- vertically; implement me', evt); - return false; + // (don't scale the canvas element itself) + console.warn('BioMap -> onPanStart -- vertically; implement me', evt); + return false; } + + /** + * Final pan event in sequence + * @param evt + * @returns {boolean} + * @private + */ + _onPanEnd(evt) { // TODO: send pan events to the scenegraph elements which compose the biomap - // (dont scale the canvas element itself) + // (don't scale the canvas element itself) console.warn('BioMap -> onPanEnd -- vertically; implement me', evt); return false; // do not stop propagation } diff --git a/src/canvas/node/SceneGraphNodeGroup.js b/src/canvas/node/SceneGraphNodeGroup.js index 9dd0a755..693767c2 100644 --- a/src/canvas/node/SceneGraphNodeGroup.js +++ b/src/canvas/node/SceneGraphNodeGroup.js @@ -1,15 +1,26 @@ /** - * FeatureMarker - * A SceneGraphNode representing a feature on a Map with a line or hash mark. - */ + * FeatureMarker + * A SceneGraphNode representing a feature on a Map with a line or hash mark. + */ import {SceneGraphNodeBase} from './SceneGraphNodeBase'; export class SceneGraphNodeGroup extends SceneGraphNodeBase { + /** + * constructor + * @param params + */ + constructor(params) { super(params); } - get visible(){ + + /** + * Return visible children elements + * @returns {Array} + */ + + get visible() { let vis = []; let cVis = this.children.map(child => { return child.visible; diff --git a/src/canvas/node/SceneGraphNodeTrack.js b/src/canvas/node/SceneGraphNodeTrack.js index d3ad518c..0ea7340e 100644 --- a/src/canvas/node/SceneGraphNodeTrack.js +++ b/src/canvas/node/SceneGraphNodeTrack.js @@ -1,11 +1,15 @@ /** - * FeatureMarker - * A SceneGraphNode representing a feature on a Map with a line or hash mark. - */ + * Placeholder for advanced group nodes + */ import {SceneGraphNodeBase} from './SceneGraphNodeBase'; export class SceneGraphNodeTrack extends SceneGraphNodeBase { + /** + * Constructor + * @param params + */ + constructor(params) { super(params); } diff --git a/src/developmentTooling.js b/src/developmentTooling.js index 7ef62933..e42ccd77 100644 --- a/src/developmentTooling.js +++ b/src/developmentTooling.js @@ -1,17 +1,23 @@ /** - * development tooling: conditionally run code based on the ENV string, + * @file + * Development tooling: conditionally run code based on the ENV string, * which is interpolated by a plugin in the rollup.config.js. + * */ import PubSub from 'pubsub-js'; import * as topics from './topics'; +/** + * @description Logger to check that pub-sub events propagate. + */ + const monitorPubSub = () => { let logger = (topic, data) => { // eslint-disable-next-line no-console console.log(`[${topic}]`, data); }; - Object.keys(topics).forEach( t => { + Object.keys(topics).forEach(t => { //console.log(`subscribing to: ${t}`); PubSub.subscribe(t, logger); }); diff --git a/src/main.js b/src/main.js index 9d6584fb..d6b4631f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ /** - * main + * @file * Instantiate the CMAP class, and initialize it. * Also the entry point for bundling of javascript and css. */ @@ -14,6 +14,12 @@ import './util/concatAll'; import {CMAP} from './ui/CMAP'; /* istanbul ignore next: unable to test this module because of css imports */ +/** + * @description Initializes CMAP window with CSS and makes sure that the initial + * DOM events are properly handled. + * + */ + const main = () => { // FIXME: this way of exposing the cmap object seems kind of clunky. For // implementing a js api, maybe using this rollup plugin would be diff --git a/src/model/AppModel.js b/src/model/AppModel.js index 4e6428fe..77873ddf 100644 --- a/src/model/AppModel.js +++ b/src/model/AppModel.js @@ -12,12 +12,16 @@ import {DataSourceModel} from './DataSourceModel'; export class AppModel { + /** + * + */ + constructor() { // sources and bioMaps arrays will be populated in load() this.sources = []; this.bioMaps = []; this.tools = { - zoomFactor : 1, + zoomFactor: 1, layout: HorizontalLayout // the default layout }; this.selection = { @@ -33,8 +37,12 @@ export class AppModel { /** * load the app model - * @param Object - object with properties defined in cmap.json + * @param header + * @param attribution + * @param sources + * @param initialView */ + load({header, attribution, sources, initialView}) { let sourceConfigs = sources; this.header = header; @@ -42,26 +50,23 @@ export class AppModel { this.initialView = initialView || []; this.biomaps = []; let promises = sourceConfigs.map(config => { - console.log('config',config); let dsm = new DataSourceModel(config); this.sources.push(dsm); - console.log('push dsm',dsm); return dsm.load(); }); // wait for all data sources are loaded, then set this.bioMaps with // only the maps named in initialView // - Promise.all(promises).then( () => { - this.allMaps = this.sources.map( src => Object.values(src.bioMaps) ).concatAll(); - if(! this.initialView.length) { + Promise.all(promises).then(() => { + this.allMaps = this.sources.map(src => Object.values(src.bioMaps)).concatAll(); + if (!this.initialView.length) { this.defaultInitialView(); } else { this.setupInitialView(); } PubSub.publish(dataLoaded); - }). - catch( err => { + }).catch(err => { // TODO: make a nice mithril component to display errors in the UI const msg = `While fetching data source(s), ${err}`; console.error(msg); @@ -74,13 +79,14 @@ export class AppModel { /** * create this.bioMaps based on initialView of config file. */ + setupInitialView() { - this.bioMaps = this.initialView.map( viewConf => { + this.bioMaps = this.initialView.map(viewConf => { const res = this.allMaps.filter(map => { return (viewConf.source === map.source.id && - viewConf.map === map.name); + viewConf.map === map.name); }); - if(res.length == 0) { + if (res.length === 0) { // TODO: make a nice mithril component to display errors in the UI const info = JSON.stringify(viewConf); const msg = `failed to resolve initialView entry: ${info}`; @@ -88,7 +94,10 @@ export class AppModel { console.trace(); alert(msg); } - if(viewConf.qtl){ + if(viewConf.tracks){ + res[0].tracks = viewConf.tracks; + } + if (viewConf.qtl) { res[0].qtlGroups = viewConf.qtl; } return res; @@ -100,24 +109,27 @@ export class AppModel { * initialView was not defined in config file). */ defaultInitialView() { - this.bioMaps = this.sources.map( src => Object.values(src.bioMaps)[0] ); + this.bioMaps = this.sources.map(src => Object.values(src.bioMaps)[0]); } /** * Add map at the given index (note, this is called by MapAdditionDialog) - * @param Object bioMap - a bioMap from one of the already loaded data sources. - * @param Number index - zero based index into the bioMaps array. + * @param {Object} bioMap - a bioMap from one of the already loaded data sources. + * @param {Number} index - zero based index into the bioMaps array. */ - addMap(bioMap, index=0) { + + addMap(bioMap, index = 0) { this.bioMaps.splice(index, 0, bioMap); PubSub.publish(mapAdded, bioMap); } /** * PubSub event handler + * @private */ + _onReset() { - this.tools.zoomFactor = 1; + this.tools.zoomFactor = 1; this.tools.layout = HorizontalLayout; } } diff --git a/src/model/BioMapConfigModel.js b/src/model/BioMapConfigModel.js index b3abed27..28bdd32c 100644 --- a/src/model/BioMapConfigModel.js +++ b/src/model/BioMapConfigModel.js @@ -7,37 +7,98 @@ export class BioMapConfigModel { /** * create a BioMapConfigModel + * @param url + * @param method */ - constructor({url, method} ) { + + constructor({url, method}) { this.url = url; this.method = method; } - load(){ + /** + * + */ + load() { return m.request(this); } } +/** + * Constant that defines the default configuration of cmap maps + * when no other configuration information is present. + * + * @type {{backboneWidth: number, backboneColor: string, invert: boolean, markerColor: string, markerWeight: number, markerLabelSize: number, markerLabelFace: string, markerLabelColor: string, rulerWidth: number, rulerSpacing: number, rulerColor: string, rulerLabelFace: string, rulerLabelSize: number, rulerLabelColor: string, rulerPrecision: number, rulerSteps: number, trackWidth: number, trackSpacing: number, fillColor: string, trackLabelSize: number, trackLabelFace: string, trackLabelColor: string}} + */ + export const defaultConfig = { - 'backboneWidth' : 20, - 'backboneColor' : '#fff6e8', - 'markerColor' : 'black', - 'markerWeight': 1, - 'markerLabelSize' : 12, - 'markerLabelFace': 'Nunito', - 'markerLabelColor' : 'black', - 'rulerWidth' : 10 , - 'rulerSpacing' : 5, - 'rulerColor' : 'aqua', - 'rulerLabelFace' : 'Nunito', - 'rulerLabelSize' : 12, - 'rulerLabelColor' : 'black', - 'rulerPrecision' : 2, - 'rulerSteps' : 100, - 'trackWidth' : 5, - 'trackSpacing' : 5, - 'trackColor' : '#636081', - 'trackLabelSize' : 12, - 'trackLabelFace' : 'Nunito', - 'trackLabelColor' : 'black' - }; + 'backbone' : { + 'width' : 20, + 'fillColor' : '#fff6e8', + 'lineWeight' : 0, + 'lineColor' : 'black' + }, + 'ruler' : { + 'width' : 10, + 'padding' : 5, + 'fillColor' : 'aqua', + 'lineWeight' : 0, + 'lineColor' : 'black', + 'labelFace' : 'Nunito', + 'labelSize' : 12, + 'labelColor' : 'black', + 'innerLineWeight' : 1.0, + 'innerLineColor' : 'black', + 'precision' : 2, + 'steps' : 100 + }, + 'track' : { + 'width' : 5, + 'padding' : 5, + 'fillColor' : '#636081', + 'lineWeight' : 0, + 'lineColor' : 'black', + 'labelFace' : 'Nunito', + 'labelSize' : 12, + 'labelColor' : 'black', + 'internalPadding' : '5' + }, + 'marker':{ + 'lineWeight' : 1, + 'lineColor' : 'black', + 'labelFace' : 'Nunito', + 'labelSize' : 12, + 'labelColor' : 'black' + }, + + 'manhattan' :{ + 'width' : 2, + 'fillColor':'green', + 'lineWeight':1, + 'lineColor':'black', + 'labelFace' : 'Nunito', + 'labelSize' : 10, + 'labelColor' : 'black', + 'displayWidth' : 50, + 'featureLineWeight' : 3, + 'featureLineColor' : 'red', + 'rulerWeight' : 2, + 'rulerColor' : 'black', + 'rulerMajorMark':10, + 'rulerMinorMark':2, + 'type':'manhattan' + }, + 'qtl':{ + 'padding' : 20, + 'width': 5, + 'fillColor': ['green'], + 'labelSize': 12, + 'labelFace': 'Nunito', + 'labelColor': 'black', + 'trackMinWidth' : 50, + 'internalPadding': 5, + 'position' : 1, + 'type':'qtl' + }, + 'invert': false, +}; diff --git a/src/model/BioMapModel.js b/src/model/BioMapModel.js index 67a82843..7b419002 100644 --- a/src/model/BioMapModel.js +++ b/src/model/BioMapModel.js @@ -1,24 +1,26 @@ /** * BioMap data model */ + export class BioMapModel { /** * create a BioMapModel - * @param Object params having the following properties: - * @param String name - the map name - * @param Object source - the DataSourceModel where bioMap was loaded from - * @param Object coordinates - object w/ start and stop props - * @param Array features - an array of Feature instances. + * @param {Object} params having the following properties: + * @param {String} name - the map name + * @param {Object} source - the DataSourceModel where bioMap was loaded from + * @param {Object} coordinates - object w/ start and stop props + * @param {Array} features - an array of Feature instances. */ + constructor({ - name, - source, - features, - tags, - coordinates = { start: 0, stop: 0}, - config - }) { + name, + source, + features, + tags, + coordinates = {start: 0, stop: 0}, + config + }) { this.name = name; this.source = source; this.features = features; @@ -29,14 +31,19 @@ export class BioMapModel { /** * getter for length (coordinates.stop - coordinates.start) + * @returns {number} */ + get length() { return this.coordinates.stop - this.coordinates.start; } /** + * * getter for unique name (prefix map name the id of data source) + * @returns {string} */ + get uniqueName() { return `${this.source.id}/${this.name}`; } diff --git a/src/model/Bounds.js b/src/model/Bounds.js index 38883a82..92261344 100644 --- a/src/model/Bounds.js +++ b/src/model/Bounds.js @@ -1,26 +1,27 @@ /** - * Bounds - * Class representing a 2D bounds, having the same properties as a DOMRect. - * This class can be instantiated by script, unlike DOMRect object itself which - * comes from the browser's DOM by getBoundingClientRect(). - * https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect - */ + * @description + * Bounds + * Class representing a 2D bounds, having the same properties as a DOMRect. + * This class can be instantiated by script, unlike DOMRect object itself which + * comes from the browser's DOM by getBoundingClientRect(). + * https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect + */ import {isNil} from '../util/isNil'; export class Bounds { /** - * Create a Bounds - * - * @param {Object} params - having the following properties: - * @param {Number} bottom - * @param {Number} left - * @param {Number} right - * @param {Number} top - * @param {Number} width - * @param {Number} height - * @returns {Object} - */ - constructor({top, left, bottom, right, width, height, allowSubpixel=true}) { + * Create a Bounds + * + * @param {Object} params - having the following properties: + * @param {Number} bottom + * @param {Number} left + * @param {Number} right + * @param {Number} top + * @param {Number} width + * @param {Number} height + * @returns {Object} + */ + constructor({top, left, bottom, right, width, height, allowSubpixel = true}) { this._bottom = bottom; this._left = left; this._right = right; @@ -29,22 +30,28 @@ export class Bounds { this._width = width; this.allowSubpixel = allowSubpixel; - if(isNil(this.width)) this._width = this.right - this.left; - if(isNil(this.height)) this._height = this.bottom - this.top; - if(isNil(this.bottom)) this._bottom = this.top + this.height; - if(isNil(this.right)) this._right = this.left + this.width; + if (isNil(this.width)) this._width = this.right - this.left; + if (isNil(this.height)) this._height = this.bottom - this.top; + if (isNil(this.bottom)) this._bottom = this.top + this.height; + if (isNil(this.right)) this._right = this.left + this.width; - if(! allowSubpixel) { + if (!allowSubpixel) { + // noinspection JSSuspiciousNameCombination this._bottom = Math.floor(this.bottom); + // noinspection JSSuspiciousNameCombination this._top = Math.floor(this.top); this._left = Math.floor(this.left); this._right = Math.floor(this.right); this._width = Math.floor(this.width); + // noinspection JSSuspiciousNameCombination this._height = Math.floor(this.height); - if(this.x) this.x = Math.floor(this.x); - if(this.y) this.y = Math.floor(this.y); + if (this.x) this.x = Math.floor(this.x); + if (this.y) { // noinspection JSSuspiciousNameCombination + this.y = Math.floor(this.y); + } } } + /** * Getters and setters, should be allowed to update bounds without having to * resort to making a new bounds object. @@ -53,9 +60,9 @@ export class Bounds { get bottom() { return this._bottom; } - - set bottom(val){ - if(this.allowSubpixel){ + + set bottom(val) { + if (this.allowSubpixel) { this._bottom = val; this._height = this._bottom - this._top; } else { @@ -67,9 +74,9 @@ export class Bounds { get top() { return this._top; } - - set top(val){ - if(this.allowSubpixel){ + + set top(val) { + if (this.allowSubpixel) { this._top = val; this._height = this._bottom - this._top; } else { @@ -81,9 +88,9 @@ export class Bounds { get left() { return this._left; } - - set left(val){ - if(this.allowSubpixel){ + + set left(val) { + if (this.allowSubpixel) { this._left = val; this._width = this._right - this._left; } else { @@ -95,9 +102,9 @@ export class Bounds { get right() { return this._right; } - - set right(val){ - if(this.allowSubpixel){ + + set right(val) { + if (this.allowSubpixel) { this._right = val; this._width = this._right - this._left; } else { @@ -109,9 +116,9 @@ export class Bounds { get width() { return this._width; } - - set width(val){ - if(this.allowSubpixel){ + + set width(val) { + if (this.allowSubpixel) { this._width = val; this._right = this._left + this._width; } else { @@ -123,9 +130,9 @@ export class Bounds { get height() { return this._height; } - - set height(val){ - if(this.allowSubpixel){ + + set height(val) { + if (this.allowSubpixel) { this._height = val; this._bottom = this._top + this._height; } else { @@ -133,12 +140,12 @@ export class Bounds { this._bottom = Math.floor(this._top + this._height); } } - + /** * Check if width or height is zero, making the Bounds effectively empty. */ get isEmptyArea() { - return ! this.width || ! this.height; + return !this.width || !this.height; } /** @@ -149,39 +156,39 @@ export class Bounds { } /** - * Class method- test whether two bounds are equal (rounds to nearest pixel) - * - * @param bounds1 - DOMRect or Bounds instance - * @param bounds2 - DOMRect or Bounds instance - * @returns Boolean - */ + * Class method- test whether two bounds are equal (rounds to nearest pixel) + * + * @param bounds1 - DOMRect or Bounds instance + * @param bounds2 - DOMRect or Bounds instance + * @returns Boolean + */ static equals(bounds1, bounds2) { let p, n1, n2; - if(! bounds1 || ! bounds2) + if (!bounds1 || !bounds2) return false; // check for null args - for (var i = 0; i < PROPS.length; i++) { + for (let i = 0; i < PROPS.length; i++) { p = PROPS[i]; n1 = bounds1[p]; n2 = bounds2[p]; - if(n1 === undefined || n2 === undefined) { // skip test, see note about x,y + if (n1 === undefined || n2 === undefined) { // skip test, see note about x,y continue; } // cast properties from float to int before equality comparison - if(Math.floor(n1) !== Math.floor(n2)) + if (Math.floor(n1) !== Math.floor(n2)) return false; } return true; } /** - * Class method- test whether two bounds are equal in area (rounds to nearest pixel) - * - * @param bounds1 - DOMRect or Bounds instance - * @param bounds2 - DOMRect or Bounds instance - * @returns Boolean - */ + * Class method- test whether two bounds are equal in area (rounds to nearest pixel) + * + * @param bounds1 - DOMRect or Bounds instance + * @param bounds2 - DOMRect or Bounds instance + * @returns Boolean + */ static areaEquals(bounds1, bounds2) { - if(! bounds1 || ! bounds2) + if (!bounds1 || !bounds2) return false; // check for null args return Math.floor(bounds1.area) === Math.floor(bounds2.area); } diff --git a/src/model/DataSourceModel.js b/src/model/DataSourceModel.js index 162753ad..2c9d3de4 100644 --- a/src/model/DataSourceModel.js +++ b/src/model/DataSourceModel.js @@ -1,12 +1,13 @@ /** * Data source model */ + import m from 'mithril'; import parser from 'papaparse'; import {BioMapModel} from './BioMapModel'; import {Feature} from './Feature'; -import {BioMapConfigModel,defaultConfig} from './BioMapConfigModel'; +import {BioMapConfigModel, defaultConfig} from './BioMapConfigModel'; // TODO: implement filtering at data loading time @@ -14,51 +15,60 @@ export class DataSourceModel { /** * create a DataSourceModel - * @param Object params having the following properties: - * @param String id - uniqueId string for the data source (required) - * @param String method - HTTP method, get or post (required) - * @param String url - HTTP URL (required) - * @param Object data - query string parameters for the request (optional) + * @param {Object} params having the following properties: + * @param {String} id - uniqueId string for the data source (required) + * @param {String} method - HTTP method, get or post (required) + * @param {String} url - HTTP URL (required) + * @param {Object} data - query string parameters for the request (optional) */ - constructor({id, method, data, url, filters, linkouts,config}) { + + constructor({id, method, data, url, filters, linkouts, config}) { this.id = id; this.method = method; this.data = data; this.url = url; this.config = config || {}; - this.bioConfig = {"default":defaultConfig}; + this.bioConfig = {'default': defaultConfig}; // request bioconfig urlpage as a promise, if it is gettable, fill in all // default values that aren't defined using the base config, otherwise // set the default values to the base config (found in BioMapConfigModel). - (( ) => { // promise generator + (() => { // promise generator let cfg = new BioMapConfigModel(this.config); return cfg.load(); })().then( // promise resolution - (item)=>{ // success + (item) => { // success this.bioConfig = item; - for(const configGroup of Object.keys(this.bioConfig)){ - for( const key of Object.keys(defaultConfig)){ - if(this.bioConfig[configGroup][key] === undefined){ - this.bioConfig[configGroup][key] = defaultConfig[key]; + for (const configGroup of Object.keys(this.bioConfig)) { + for (const key of Object.keys(defaultConfig)) { + if (this.bioConfig[configGroup][key] === undefined) { + this.bioConfig[configGroup][key] = this.bioConfig.default[key] || defaultConfig[key]; + } + for(const subkey of Object.keys(defaultConfig[key])) { + if (this.bioConfig[configGroup][key][subkey] === undefined) { + this.bioConfig[configGroup][key][subkey] = this.bioConfig.default[key][subkey] || defaultConfig[key][subkey]; + } } } } - }, - ()=>{ // failure + }, + () => { // failure this.bioConfig.default = defaultConfig; } ); this.filters = filters || []; this.linkouts = linkouts || []; - this.linkouts.forEach(l => {l.featuretypePattern != undefined ? l.featuretypePattern = new RegExp(l.featuretypePattern) : undefined;}); + this.linkouts.forEach(l => { + l.featuretypePattern !== undefined ? l.featuretypePattern = new RegExp(l.featuretypePattern) : undefined; + }); this.background = true; // mithril not to redraw upon completion } /** - * Load the data source with mithril request - * @return Promise + *Load the data source with mithril request + * @returns {*} */ + load() { return m.request(this); } @@ -67,20 +77,21 @@ export class DataSourceModel { * Callback from mithril request(); instead of the default deserialization * which is JSON, use the papaparse library to parse csv or tab delimited * content. - * @param String delimited text - csv or tsv + * @param {String} data - delimited text, csv or tsv */ + deserialize(data) { const res = parser.parse(data, { header: true, dynamicTyping: true, skipEmptyLines: true }); - if(res.errors.length) { + if (res.errors.length) { console.error(res.errors); alert(`There were parsing errors in ${this.url}, please see console.`); } // apply filters from config file - res.data = res.data.filter( d => this.includeRecord(d) ); + res.data = res.data.filter(d => this.includeRecord(d)); this.parseResult = res; } @@ -89,27 +100,28 @@ export class DataSourceModel { * processed sequentially and the result is all or nothing, effectively like * SQL AND. * - * @param Object d - key/value properies of 1 record - * @return Boolean - true for include, false for exclude + * @param {Object} d - key/value properties of 1 record + * @return {Boolean} true for include, false for exclude */ + includeRecord(d) { let hits = 0; - this.filters.forEach( f => { + this.filters.forEach(f => { let col = f.column; - if(d.hasOwnProperty(col)) { + if (d.hasOwnProperty(col)) { let testVal = d[col]; let match; - if(f.operator === 'equals') { + if (f.operator === 'equals') { match = (testVal === f.value); } - else if(f.operator === 'regex') { + else if (f.operator === 'regex') { match = testVal.match(f.value); } - if(f.not) { - if(! match) ++hits; + if (f.not) { + if (!match) ++hits; } else { - if(match) ++hits; + if (match) ++hits; } } }); @@ -120,8 +132,9 @@ export class DataSourceModel { * bioMaps getter; return a mapping of the uniquified map name to * an instance of BioMapModel. * - * @return Object - key: prefix + map_name -> val: BioMapModel instance + * @return {Object} key: prefix + map_name -> val: BioMapModel instance */ + get bioMaps() { const res = {}; try { @@ -156,12 +169,12 @@ export class DataSourceModel { ); if(d[typeField] !== '' && res[uniqueMapName].tags.indexOf(d[typeField]) === -1){ res[uniqueMapName].tags.push(d[typeField]); - } - }); - } catch(e) { - console.trace(); - console.error(e); - } - return res; + } + }); + } catch (e) { + console.trace(); + console.error(e); + } + return res; } } diff --git a/src/model/Feature.js b/src/model/Feature.js index 292a2cde..14f72e52 100644 --- a/src/model/Feature.js +++ b/src/model/Feature.js @@ -13,13 +13,14 @@ class Feature { * @param {Object} aliases - array of alternate names, optional * @returns {Object} */ + constructor({ - source, - coordinates = { start: 0, stop: 0}, - name, - tags=[], - aliases=[], - }) { + source, + coordinates = {start: 0, stop: 0}, + name, + tags = [], + aliases = [], + }) { this.source = source; this.coordinates = Object.freeze(coordinates); // object w/ start and end props this.name = name; @@ -27,48 +28,71 @@ class Feature { this.aliases = aliases; } + /** + * + * @returns {number} + */ + get length() { return this.coordinates.stop - this.coordinates.start; } + /** + * + * @returns {boolean} + */ + get typeHasLinkouts() { return this.source.linkouts.some(l => { - return this.typeLinkedBy(l); - }); + return this.typeLinkedBy(l); + }); } + /** + * + * @param linkout + * @returns {Array|*|boolean} + */ + typeLinkedBy(linkout) { - return linkout.featuretypePattern != undefined ? - this.tags.some(t => {return linkout.featuretypePattern.test(t);}) - : this.tags.includes(linkout.featuretype); + return linkout.featuretypePattern !== undefined ? + this.tags.some(t => { + return linkout.featuretypePattern.test(t); + }) + : this.tags.includes(linkout.featuretype); } } /** - * Find the common features based on name and aliases. * @param Array features1 - 1st collection of features * @param Array features2 - 2nd collection of features * @return Array - tuples of results in common [[feat1, feat2], ...] */ +/** + * Find the common features based on name and aliases. + * @param features1 + * @param features2 + * @returns {any[]} + */ + // TODO: support more than two collections of features function featuresInCommon(features1, features2) { const setupDict = (features) => { let dict = {}; - features.forEach( f => { + features.forEach(f => { dict[f.name] = f; - f.aliases.forEach( a => { - if(a) dict[a] = f; + f.aliases.forEach(a => { + if (a) dict[a] = f; }); }); return dict; }; let dict1 = setupDict(features1); let dict2 = setupDict(features2); - let intersectedKeys = Object.keys(dict1).filter( key => dict2[key] ); - return intersectedKeys.map( key => { - return [ dict1[key], dict2[key] ]; + let intersectedKeys = Object.keys(dict1).filter(key => dict2[key]); + return intersectedKeys.map(key => { + return [dict1[key], dict2[key]]; }); } - export {Feature, featuresInCommon}; diff --git a/src/polyfill/elementsFromPoint.js b/src/polyfill/elementsFromPoint.js index d5ae686f..caa41a96 100644 --- a/src/polyfill/elementsFromPoint.js +++ b/src/polyfill/elementsFromPoint.js @@ -13,8 +13,8 @@ if (!document.elementsFromPoint) { /* istanbul ignore next: depends on browser native elementFromPoint(x,y) */ function elementsFromPoint(x, y) { - var parents = []; - var parent = void 0; + let parents = []; + let parent = void 0; do { if (parent !== document.elementFromPoint(x, y)) { parent = document.elementFromPoint(x, y); diff --git a/src/polyfill/index.js b/src/polyfill/index.js index 4df06769..6d89a910 100644 --- a/src/polyfill/index.js +++ b/src/polyfill/index.js @@ -3,7 +3,7 @@ * Javascript, HTML5, or CSS3 features. */ - // adds Math.scale, fscale, clamp, radians, degrees +// adds Math.scale, fscale, clamp, radians, degrees import 'ecma-proposal-math-extensions'; // adds elementsFromPoint diff --git a/src/topics.js b/src/topics.js index 2c4e7c6b..ac12c5d5 100644 --- a/src/topics.js +++ b/src/topics.js @@ -1,7 +1,8 @@ /** - * topics - * define constants for all PubSub message topics used by cmap + * @file + * defines constants for all PubSub message topics used by cmap */ + export const selectedMap = 'selectedMap'; // the selected map (canvas) changed export const reset = 'reset'; // reset button click export const layout = 'layout'; // layout selection changed @@ -11,5 +12,6 @@ export const dataLoaded = 'loaded'; // data finished loading, or was filtered/up // by user export const mapRemoved = 'mapRemoved'; export const mapAdded = 'mapAdded'; +export const mapReorder = 'mapReorder'; export const featureUpdate = 'featureUpdate'; // change to qtlConfig from modal diff --git a/src/ui/CMAP.js b/src/ui/CMAP.js index bb022412..93a57e1e 100644 --- a/src/ui/CMAP.js +++ b/src/ui/CMAP.js @@ -1,15 +1,20 @@ /** - * CMAP - */ + * CMAP + */ + import m from 'mithril'; import {AppModel} from './../model/AppModel'; import {UI} from './UI'; - /* istanbul ignore next: mithril-query does not work with m.mount, and dom id is hardcoded as well */ export class CMAP { + /** + * + * @param configURL + */ + load(configURL) { this.rootElement = document.getElementById('cmap-ui'); this.appState = new AppModel({}); @@ -23,16 +28,16 @@ export class CMAP { this.appState.status = 'loading configuration file...'; this.appState.busy = true; - m.request(configURL).then( config => { + m.request(configURL).then(config => { let numSources = config.sources.length; - let plural = numSources > 1 ? 's': ''; + let plural = numSources > 1 ? 's' : ''; this.appState.status = `loading ${numSources} data file${plural}...`; let promises = this.appState.load(config); - Promise.all(promises).then( () => { + Promise.all(promises).then(() => { this.appState.status = ''; this.appState.busy = false; }); - }).catch( err => { + }).catch(err => { // TODO: make a nice mithril component to display errors in the UI console.error(err); console.trace(); diff --git a/src/ui/Header.js b/src/ui/Header.js index 8b04f968..07101174 100644 --- a/src/ui/Header.js +++ b/src/ui/Header.js @@ -5,11 +5,20 @@ import m from 'mithril'; export class Header { // constructor() - prefer do not use in mithril components + /** + * mithril lifecycle method + * @param vnode + */ oninit(vnode) { this.appState = vnode.attrs.appState; } + /** + * Mithril lifecycle component + * @returns {*} cmap-header + */ + view() { return m('div.cmap-hbox', m('h4.cmap-header', [ diff --git a/src/ui/README.md b/src/ui/README.md new file mode 100644 index 00000000..820b8f65 --- /dev/null +++ b/src/ui/README.md @@ -0,0 +1,14 @@ + #src/ui/ # +Contains user-interface elements based on MithrilJs components, i.e. +they are populated into the DOM, not drawing with the Canvas API. +___ + +`/css` - self explanatory + +`/layout` - layout templates for canvas elements + +`/layout/components` - mithril wrappers for commonly re-used layout components + +`/menus` - full screen modal menus + +`/tools` - menus that populate as dropdowns from buttons in the header diff --git a/src/ui/README.txt b/src/ui/README.txt deleted file mode 100644 index 38b38a60..00000000 --- a/src/ui/README.txt +++ /dev/null @@ -1,2 +0,0 @@ -src/ui/ contains user-interface elements based on MithrilJs components, i.e. -they are populated into the DOM, not drawing with the Canvas API. diff --git a/src/ui/RegisterComponentMixin.js b/src/ui/RegisterComponentMixin.js index 89230b9d..2fe82ac4 100644 --- a/src/ui/RegisterComponentMixin.js +++ b/src/ui/RegisterComponentMixin.js @@ -1,24 +1,39 @@ /** - * Store a reference to the mithril component on it's corresponding dom - * element. - */ + * Store a reference to the mithril component on it's corresponding dom + * element. + */ export let RegisterComponentMixin = (superclass) => class extends superclass { + /** + * + * @param vnode + */ + oninit(vnode) { - if(super.oninit) super.oninit(vnode); - if(vnode.attrs && vnode.attrs.registerComponentCallback) { + if (super.oninit) super.oninit(vnode); + if (vnode.attrs && vnode.attrs.registerComponentCallback) { vnode.attrs.registerComponentCallback(this); } } + /** + * + * @param vnode + */ + oncreate(vnode) { - if(super.oncreate) super.oncreate(vnode); + if (super.oncreate) super.oncreate(vnode); vnode.dom.mithrilComponent = this; } + /** + * + * @param vnode + */ + onbeforeremove(vnode) { - if(super.onbeforeremove) super.onbeforeremove(vnode); + if (super.onbeforeremove) super.onbeforeremove(vnode); delete vnode.dom.mithrilComponent; } }; diff --git a/src/ui/StatusBar.js b/src/ui/StatusBar.js index 3621f11a..fa0768dc 100644 --- a/src/ui/StatusBar.js +++ b/src/ui/StatusBar.js @@ -1,20 +1,30 @@ /** - * StatusBar - * A mithril component of a status bar and/or footer. - */ + * StatusBar + * A mithril component of a status bar and/or footer. + */ import m from 'mithril'; export class StatusBar { // constructor() - prefer do not use in mithril components + /** + * + * @param vnode + */ + oninit(vnode) { this.appState = vnode.attrs.appState; } + /** + * + * @returns {*} + */ + view() { return m('div', [ m('div.cmap-attribution', this.appState.attribution), - m('div',{id:'cmap-disclaimer'}, 'cmap-js is still in alpha. As the software is still in development, the current state of the project may not reflect the final release.'), + m('div', {id: 'cmap-disclaimer'}, 'cmap-js is still in alpha. As the software is still in development, the current state of the project may not reflect the final release.'), m('div.cmap-footer', [ this.appState.busy ? m('img[src=images/ajax-loader.gif]') : '', this.appState.status diff --git a/src/ui/UI.js b/src/ui/UI.js index 2577bd54..a93a8903 100644 --- a/src/ui/UI.js +++ b/src/ui/UI.js @@ -1,7 +1,7 @@ /** - * UI - * A mithril component presenting all DOM aspects of user-interface. - */ + * UI + * A mithril component presenting all DOM aspects of user-interface. + */ import m from 'mithril'; import Hammer from 'hammerjs'; import Hamster from 'hamsterjs'; @@ -19,16 +19,19 @@ export class UI extends mix().with(RegisterComponentMixin) { /** * Create a UI instance - * @param Object - the appState, instance of model/AppModel. + * @param {Object} appState - instance of model/AppModel. */ + constructor(appState) { super(); this.appState = appState; } /** - * mithril lifecycle method + * Mithril lifecycle method + * @param vnode */ + oncreate(vnode) { super.oncreate(vnode); this.el = vnode.dom; @@ -37,7 +40,9 @@ export class UI extends mix().with(RegisterComponentMixin) { /** * mithril component render callback + * @returns {*} mithril vnode component */ + view() { const childAttrs = { appState: this.appState, @@ -46,8 +51,9 @@ export class UI extends mix().with(RegisterComponentMixin) { return m('div.cmap-layout.cmap-vbox', [ m(Header, childAttrs), m(Tools, childAttrs), - [ m('div.cmap-menu-viewport#cmap-menu-viewport',{style:'display:none'}), - m('div.cmap-layout-viewport.cmap-hbox', { id: 'cmap-layout-viewport',style:'position:relative;' }, + m('div#cmap-layout-titles', {style: 'display:inline-flex;'}), + [m('div.cmap-menu-viewport#cmap-menu-viewport', {style: 'display:none;'}), + m('div.cmap-layout-viewport.cmap-hbox', {id: 'cmap-layout-viewport', style: 'position:relative;'}, m(LayoutContainer, { appState: this.appState, registerComponentCallback: (comp) => this._layoutContainer = comp @@ -57,12 +63,22 @@ export class UI extends mix().with(RegisterComponentMixin) { ]); } + /** + * + * @private + */ + _logRenders() { - if(! this.count) this.count = 0; + if (!this.count) this.count = 0; this.count += 1; console.log(`*** mithril render #${this.count} ***`); } + /** + * + * @private + */ + _setupEventHandlers() { this._setupMousewheel(); this._setupGestures(); @@ -71,7 +87,9 @@ export class UI extends mix().with(RegisterComponentMixin) { /** * Setup pubsub subscriptions + * @private */ + _setupPubSub() { PubSub.subscribe(reset, () => { // if the viewport were refactored into it's own mithril component, then @@ -84,16 +102,18 @@ export class UI extends mix().with(RegisterComponentMixin) { /** * setup mouse wheel (hamsterjs) handlers. + * @private */ + _setupMousewheel() { const hamster = Hamster(this.el); const hamsterHandler = (evt, delta, deltaX, deltaY) => { - // hamsterjs claims to normalizizing the event object, across browsers, + // hamsterjs claims to normalizing the event object, across browsers, // but at least in firefox it is not because deltaY is not on the evt. evt.deltaY = deltaY; // workaround // add an additional property to make it similar enough to the pinch // gesture so event consumers can just implement one 'zoom', if they want. - evt.center = { x: evt.originalEvent.x, y: evt.originalEvent.y }; + evt.center = {x: evt.originalEvent.x, y: evt.originalEvent.y}; this._dispatchGestureEvt(evt); }; hamster.wheel(hamsterHandler); @@ -101,44 +121,52 @@ export class UI extends mix().with(RegisterComponentMixin) { /** * setup gesture (hammerjs) handlers. + * @private */ + _setupGestures() { const hammer = Hammer(this.el); const hammerHandler = (evt) => this._dispatchGestureEvt(evt); const hammerEvents = 'panmove panend panstart pinchmove pinchend tap'; - hammer.get('pan').set({ direction: Hammer.DIRECTION_ALL }); - hammer.get('pinch').set({ enable: true }); + hammer.get('pan').set({direction: Hammer.DIRECTION_ALL}); + hammer.get('pinch').set({enable: true}); hammer.on(hammerEvents, hammerHandler); } /** - * Custom dispatch of ui events. Layout elements like BioMap and - * CorrespondenceMap are visually overlapping, and so do not fit cleanly into - * the js event capture or bubbling phases. Query the dom at the events - * coordinates, and dispatch the event to child who - * a) intersects with this point - * b) wants to handle this event (it can decide whether to based on it's - * canvas own scenegraph contents, etc.) - */ + * Custom dispatch of ui events. Layout elements like BioMap and + * CorrespondenceMap are visually overlapping, and so do not fit cleanly into + * the js event capture or bubbling phases. Query the dom at the events + * coordinates, and dispatch the event to child who + * a) intersects with this point + * b) wants to handle this event (it can decide whether to based on it's + * canvas own scenegraph contents, etc.) + * + * @param evt + * @private + */ + _dispatchGestureEvt(evt) { let hitElements = document.elementsFromPoint(evt.center.x, evt.center.y); - let filtered = hitElements.filter( el => { + let filtered = hitElements.filter(el => { return (el.mithrilComponent && el.mithrilComponent.handleGesture); }); // dispatch event to all the mithril components, until one returns true; // effectively the same as 'stopPropagation' on a normal event bubbling. - filtered.some( el =>{ - var state = el.mithrilComponent.handleGesture(evt); - return state; + filtered.some(el => { + return el.mithrilComponent.handleGesture(evt); }); } + /** * Gesture event recapture and force upon the LayoutContainer. This is to * prevent the the layout container from missing events after it has partially * moved out of the viewport. + * + * @param evt */ + handleGesture(evt) { this._layoutContainer.handleGesture(evt); } - } diff --git a/src/ui/css/cmap.css b/src/ui/css/cmap.css index b77fe6c6..32e1a684 100644 --- a/src/ui/css/cmap.css +++ b/src/ui/css/cmap.css @@ -1,89 +1,90 @@ html, body { - width: 100%; - height: 100%; - margin: 0; - overflow: hidden; - font-family: Nunito,HelveticaNeue,Helvetica Neue,Helvetica,Arial,sans-serif; + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + font-family: Nunito, HelveticaNeue, Helvetica Neue, Helvetica, Arial, sans-serif; } #cmap-ui { - width: calc(100% - 2rem); - height: calc(100% - 2rem); - margin: 2rem; - margin-top: 0rem; + width: calc(100% - 2rem); + height: calc(100% - 2rem); + margin: 0 2rem 2rem; } + #cmap-disclaimer { - font-size: 20px; - color: red; - border: solid 2px black; - border-radius: 8px; - padding: 10px; + font-size: 20px; + color: red; + border: solid 2px black; + border-radius: 8px; + padding: 10px; } .cmap-vbox { - display: flex; - flex-direction: column; - align-content: stretch; + display: flex; + flex-direction: column; + align-content: stretch; } .cmap-hbox { - display: flex; - flex-direction: row; - align-content: stretch; - justify-content: start; + display: flex; + flex-direction: row; + align-content: stretch; + justify-content: start; } .cmap-layout { - width: calc(100% - 2rem); - height: calc(100% - 2rem); + width: calc(100% - 2rem); + height: calc(100% - 2rem); } .cmap-layout-viewport { - overflow: auto; /* overflow the content using scrollbars */ - flex: auto; /* expand to fill the vertical space allowed by flexbox */ - position: relative; + overflow-y: hidden; /* overflow the content using scrollbars */ + overflow-x: hidden; + flex: auto; /* expand to fill the vertical space allowed by flexbox */ + position: relative; } .cmap-layout-container { - position: absolute;; - width: calc(100% - 2rem); - height: calc(100% - 2.5rem); + position: absolute;; + width: calc(100% - 2rem); + height: calc(100% - 2.5rem); } .cmap-canvas { - position: absolute; + position: absolute; } .cmap-layout-horizontal { - width: calc(100% - 2rem); - height: calc(100% - 2rem); - position: relative; + width: calc(100% - 2rem); + height: calc(100% - 2rem); + position: relative; } .cmap-layout-circos { - width: calc(100% - 2rem); - height: calc(100% - 2rem); - position: relative; + width: calc(100% - 2rem); + height: calc(100% - 2rem); + position: relative; } .cmap-correspondence-map { - /* border: dashed 2px cyan; */ + /* border: dashed 2px cyan; */ } .cmap-biomap { - /* border: dashed 2px grey; */ + border: dashed 2px grey; } .cmap-biomap.selected { - border: dashed 2px magenta; + border: dashed 2px magenta; } h4.cmap-header { - margin-bottom: 0; + margin-bottom: 0; } .cmap-tools h5 { - margin-bottom: 0; + margin-bottom: 0; } span.cmap-header { @@ -96,124 +97,130 @@ span.cmap-header { } .cmap-map-removal-dialog, .cmap-map-addition-dialog { - padding-left: 2rem; - border-left: solid 4px grey; + padding-left: 2rem; + border-left: solid 4px grey; } .cmap-map-addition-dialog label { - display: inline-block; - margin: 0 0.5rem 0 0.5rem; + display: inline-block; + margin: 0 0.5rem 0 0.5rem; } -button > i{ - vertical-align: -30%; +button > i { + vertical-align: -30%; } div.cmap-tools button { - margin-right: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; + margin-right: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; } button[disabled] { - background:#ccc; - border-color: #ccc; - text-shadow:none; + background: #ccc; + border-color: #ccc; + text-shadow: none; } span.cmap-map-name-chip { - background: #eee; - border-radius: 4px; - padding: 0.2rem 1rem 0.2rem 1rem; - margin-right: 0.5rem; + background: #eee; + border-radius: 4px; + padding: 0.2rem 1rem 0.2rem 1rem; + margin-right: 0.5rem; } .biomap-info { - width: 15em; - height: 10em; - border: 1px solid #bbb; - border-radius: 4px; - background: white; - position: absolute; - display: inline-block; - overflow-y: auto; - z-index: 10; + width: 15em; + height: 10em; + border: 1px solid #bbb; + border-radius: 4px; + background: white; + position: absolute; + display: inline-block; + overflow-y: auto; + z-index: 10; } .biomap-info-name { - margin: 5px; - border: 1px solid #bbb; - border-radius: 4px; - background:white; - text-align: center; + margin: 5px; + border: 1px solid #bbb; + border-radius: 4px; + background: white; + text-align: center; } .biomap-info-name:hover { - background:#ccc; + background: #ccc; } .biomap-info-data { - margin: 5px 20px 5px 20px; -} -.swap-div{ - opacity:50%; - z-index:100; - display:flex; -} -.swap-map-order{ - margin: 5px 10px 5px 10px; - padding: 5px 10px 5px 10px; - border: 1px solid #bbb; - border-radius: 4px; - background: white; - text-align:center; - opacity:0.5; + margin: 5px 20px 5px 20px; +} + +.swap-div { + opacity: 0.6; + z-index: 10000; + border: 1px solid #bbb; + border-radius: 4px; +} + +.swap-map-order { + margin: 5px 10px 5px 10px; + padding: 5px 10px 5px 10px; + border: 1px solid #bbb; + border-radius: 4px; + background: white; + text-align: center; + opacity: 0.5; } + .swap-map-order:hover { - background:#ccc; -} -.map-title{ - margin: 5px 10px 5px 10px; - padding: 5px 10px 5px 10px; - text-align: center; - background: white; -} - -.feature-title{ - z-index:1000; - background:white; - border: 1px solid black; - border-radius: 4px; - padding: 5px 0px 5px 0px; - margin: 5px 0px 5px 0px; - text-align: center; - white-space:nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.feature-menu{ - z-index: 10000; - background:white; - border: 1px solid black; - border-radius: 4px; - padding: 5px 0px 5px 0px; - margin: 5px 0px 5px 0px; - text-align: center; -} - -#cmap-menu-viewport{ - margin: 5px 5px 5px 5px; - padding: 5px 10px 5px 10px; - border: 1px solid #bbb; - border-radius: 4px; - background: white; -} - -.cmap-biomap{ - border: 1px dashed black; - border-radius: 4px; -} -.cmap-correspondence-map{ - /*border:1px dashed aqua;*/ + background: #ccc; +} + +.map-title { + margin: 5px 10px 5px 10px; + padding: 5px 10px 5px 10px; + text-align: center; + background: white; +} + +.feature-title { + z-index: 1000; + background: white; + border: 1px solid black; + border-radius: 4px; + padding: 5px 0 5px 0; + margin: 5px 0 5px 0; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.feature-menu { + z-index: 10000; + background: white; + border: 1px solid black; + border-radius: 4px; + padding: 5px 0 5px 0; + margin: 5px 0 5px 0; + text-align: center; +} + +#cmap-menu-viewport { + margin: 5px 5px 5px 5px; + padding: 5px 10px 5px 10px; + border: 1px solid #bbb; + border-radius: 4px; + background: white; +} + +.cmap-biomap { + /* border: 1px dashed black; + border-radius: 4px; */ +} + +.cmap-correspondence-map { + /*border:1px dashed aqua;*/ } diff --git a/src/ui/layout/CircosLayout.js b/src/ui/layout/CircosLayout.js index 90b88be8..c511e105 100644 --- a/src/ui/layout/CircosLayout.js +++ b/src/ui/layout/CircosLayout.js @@ -1,7 +1,7 @@ /** - * CircosLayout - * A mithril component for circos-style layout of BioMaps. - */ + * CircosLayout + * A mithril component for circos-style layout of BioMaps. + */ import m from 'mithril'; import {mix} from '../../../mixwith.js/src/mixwith'; @@ -10,28 +10,33 @@ import {Bounds} from '../../model/Bounds'; import {RegisterComponentMixin} from '../RegisterComponentMixin'; const radians = degrees => degrees * Math.PI / 180; + //const degrees = radians => radians * 180 / Math.PI; // TODO: remove if unused export class CircosLayout - extends mix(LayoutBase) - .with(RegisterComponentMixin) { + extends mix(LayoutBase) + .with(RegisterComponentMixin) { // constructor() - prefer do not use in mithril components + /** + * + * @private + */ _layout() { let domRect = this.el.getBoundingClientRect(); - if(! domRect.width || ! domRect.height) { + if (!domRect.width || !domRect.height) { // may occur when component is created but dom element has not yet filled // available space; expect onupdate() will occur. console.warn('deferring layout'); return; } let newBounds = new Bounds(domRect); - let dirty = ! Bounds.equals(this.domBounds, newBounds); + let dirty = !Bounds.equals(this.domBounds, newBounds); this.domBounds = newBounds; /* update child elements with their bounds */ let radius = this.domBounds.width > this.domBounds.height - ? this.domBounds.height * 0.4 : this.domBounds.width * 0.4; + ? this.domBounds.height * 0.4 : this.domBounds.width * 0.4; let n = this.bioMaps.length; let center = { x: Math.floor(this.domBounds.width * 0.5), @@ -42,24 +47,29 @@ export class CircosLayout let childHeight = Math.floor(childWidth * 0.6); let startDegrees = -180; let degrees = startDegrees; - this.bioMaps.forEach( child => { + this.bioMaps.forEach(child => { let rad = radians(degrees); let x = center.x - Math.floor(childWidth * 0.5) + Math.floor(radius * Math.cos(rad)); let y = center.y - Math.floor(childHeight * 0.5) + Math.floor(radius * Math.sin(rad)); - let bounds = new Bounds({ + // noinspection JSSuspiciousNameCombination + // noinspection JSSuspiciousNameCombination + child.domBounds = new Bounds({ left: x, top: y, width: childHeight, // swap the width and height height: childWidth }); - child.domBounds = bounds; child.rotation = Math.floor(degrees) + startDegrees; degrees += degreesPerChild; }); - if(dirty) m.redraw(); + if (dirty) m.redraw(); } /* mithril render callback */ + /** + * + * @returns {*} + */ view() { return m('div.cmap-layout-circos', this.children.map(m) diff --git a/src/ui/layout/HorizontalLayout.js b/src/ui/layout/HorizontalLayout.js index 7e0fa5c3..fc1f6eb3 100644 --- a/src/ui/layout/HorizontalLayout.js +++ b/src/ui/layout/HorizontalLayout.js @@ -1,38 +1,46 @@ /** - * HorizontalLayout (left to right) - * A mithril component for horizontal layout of BioMaps. - */ + * HorizontalLayout (left to right) + * A mithril component for horizontal layout of BioMaps. + */ import m from 'mithril'; import {mix} from '../../../mixwith.js/src/mixwith'; import PubSub from 'pubsub-js'; -import {dataLoaded, mapAdded, mapRemoved, reset, featureUpdate} from '../../topics'; +import {dataLoaded, mapAdded, mapRemoved, mapReorder, reset, featureUpdate} from '../../topics'; import {LayoutBase} from './LayoutBase'; import {Bounds} from '../../model/Bounds'; -import {BioMap as BioMapComponent} from '../../canvas/layout/BioMap'; -import {CorrespondenceMap as CorrMapComponent} from '../../canvas/layout/CorrespondenceMap'; -import {QtlTrack} from '../../canvas/layout/QtlTrack'; +import {BioMap as BioMapComponent} from '../../canvas/canvas/BioMap'; +import {CorrespondenceMap as CorrMapComponent} from '../../canvas/canvas/CorrespondenceMap'; +import {FeatureTrack} from '../../canvas/layout/FeatureTrack'; import {Popover} from '../menus/Popover'; import {FeatureMenu} from '../menus/FeatureMenu'; import {RegisterComponentMixin} from '../RegisterComponentMixin'; +import {TitleComponent} from './components/TitleComponent'; +import {BioMapComponent as BioMapVnode} from './components/BioMapComponent'; export class HorizontalLayout - extends mix(LayoutBase) - .with(RegisterComponentMixin) { + extends mix(LayoutBase) + .with(RegisterComponentMixin) { // constructor() - prefer do not use in mithril components /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { super.oninit(vnode); + this.contentBounds = vnode.attrs.contentBounds; + this.vnode = vnode; this.bioMapComponents = []; this.correspondenceMapComponents = []; - this.popoverComponents=[]; - this.swapComponents=[]; - this.featureControls=[]; - this.modal=[]; + this.popoverComponents = []; + this.swapComponents = []; + this.featureControls = []; + this.modal = []; + this.bioMapOrder = []; + this.test = 0; const handler = () => this._onDataLoaded(); this.subscriptions = [ // all of these topics have effectively the same event handler for @@ -40,140 +48,195 @@ export class HorizontalLayout PubSub.subscribe(dataLoaded, handler), PubSub.subscribe(mapRemoved, handler), PubSub.subscribe(mapAdded, handler), - PubSub.subscribe(reset,() => { this._onReset();}), - PubSub.subscribe(featureUpdate, ()=>{this._onFeatureUpdate();}) + PubSub.subscribe(reset, () => { + this._onReset(); + }), + PubSub.subscribe(featureUpdate, (msg, data) => { + this._onFeatureUpdate(data); + }), + PubSub.subscribe(mapReorder, () => { + this._onReorder(); + }) ]; } + /** + * + * @param vnode + */ + + onupdate(vnode) { + this.contentBounds = vnode.attrs.contentBounds; + } + /** * mithril lifecycle method */ + onremove() { - this.subscriptions.forEach( token => PubSub.unsubscribe(token) ); + this.subscriptions.forEach(token => PubSub.unsubscribe(token)); } /** * mithril component render method + * @returns {*} mithril vnode */ + view() { + //m.mount(document.getElementById('cmap-layout-titles'),null); + // let mo = this.bioMapOrder.map(i => { + // return this.bioMapComponents[i]; + // }); return m('div.cmap-layout-horizontal', - [this.swapComponents,this.bioMapComponents.map(m),this.featureControls, - //this.modal.map(modal =>{ return m(modal,{info:modal.info, bounds: modal.bounds, order:modal.order}); }), - this.correspondenceMapComponents.map(m), - this.popoverComponents.map(popover =>{ return m(popover,{info:popover.info, domBounds:popover.domBounds});})] + [//this.swapComponents, + this.correspondenceMapComponents.map(m), + this.bioMapOrder.map((i) => { + return m(BioMapVnode, {bioMap: this.bioMapComponents[i]}); + }), this.featureControls, + //this.modal.map(modal =>{ return m(modal,{info:modal.info, bounds: modal.bounds, order:modal.order}); }), + this.popoverComponents.map(popover => { + return m(popover, {info: popover.info, domBounds: popover.domBounds}); + })] ); } /** * pub/sub event handler + * + * @private */ + _onDataLoaded() { this._layoutBioMaps(); - this._layoutSwapComponents(); + this._layoutSwapComponents(); this._layoutFeatureControls(); this._layoutCorrespondenceMaps(); this._layoutPopovers(); m.redraw(); } - _layoutSwapComponents(){ - this.swapComponents = []; - let n = this.bioMapComponents.length; - let maps = this; - for (var i = 0; i < n; i++) { - let bMap = this.bioMapComponents[i]; - const b = i; - let left ='',right=''; - if(b>0){ - left = m('div', {class:'swap-map-order', onclick: function() { - if(b > 0){ - const tmp = maps.appState.bioMaps[b-1]; - maps.appState.bioMaps[b-1] = maps.appState.bioMaps[b]; - maps.appState.bioMaps[b] = tmp; - maps._onDataLoaded(); - } - } - },'<'); - } else { - left = m('div', {class:'swap-map-order',style:'background:#ccc;'},'<'); - } + /** + * + * @private + */ - if(b< n-1){ - right = m('div', {class:'swap-map-order', onclick: function() { - if(b < n-1){ - const tmp = maps.appState.bioMaps[b]; - maps.appState.bioMaps[b] = maps.appState.bioMaps[b+1]; - maps.appState.bioMaps[b+1] = tmp; - maps._onDataLoaded(); - } - }},'>'); - } else { - right = m('div', {class:'swap-map-order',style:'background:#ccc;'},'>'); + _onReorder() { + let left = 0; + let bmaps = this.bioMapComponents; + let sc = this.bioMapOrder; + bmaps.forEach(comp => { + comp.dirty = true; + }); + for (let i = 0; i < bmaps.length; i++) { + let map = bmaps[sc[i]]; + //const mapC = bmaps[sc[i]].domBounds; + const mw = map.domBounds.width; + map.domBounds.left = left; + map.domBounds.right = left + mw; + left = map.domBounds.right; + } + this._layoutCorrespondenceMaps(); + this._layoutFeatureControls(); + m.mount(document.getElementById('cmap-layout-titles'), null); + m.redraw(); + this._layoutSwapComponents(); + } + + /** + * + * @private + */ + + _layoutSwapComponents() { + this.swapComponents = []; + let sc = this.bioMapOrder; + let maps = this; + let cb = this.contentBounds; + //let bmaps = this.bioMapComponents; + let pan = []; + pan[0] = false; + m.mount(document.getElementById('cmap-layout-titles'), { + view: function () { + return sc.map((order) => { + return m(TitleComponent, { + bioMaps: maps.bioMapComponents, + order: order, + titleOrder: sc, + contentBounds: cb, + pan: pan + }); + }); } - - console.log('swap comp',bMap,bMap.p); - this.swapComponents.push( m('div', { - class: 'swap-div', id: `swap-${i}`, - style: `position:absolute; left: ${Math.floor(bMap.domBounds.left+bMap.ruler.globalBounds.left/2)}px; top: ${bMap.domBounds.top}px;`}, - [left,m('div',{class:'map-title',style:'display:inline-block;'}, [bMap.model.name,m('br'),bMap.model.source.id]), right])); - } - - } + }); - _layoutFeatureControls(){ - this.featureControls = []; - let n = this.bioMapComponents.length; - let maps = this; - this.bioMapComponents.forEach( component => { - component.children.forEach( child => { - if( child instanceof QtlTrack){ - for( let i = 0; i < child.children.length; i++){ - if(child.children[i].bounds.width > 0){ + } + + /** + * + * @private + */ + + _layoutFeatureControls() { + this.featureControls = []; + //let n = this.bioMapComponents.length; + //let maps = this; + this.bioMapComponents.forEach(component => { + component.children.forEach(child => { + if (child instanceof FeatureTrack) { + for (let i = 0; i < child.children.length; i++) { + if (child.children[i].bounds.width > 0) { let featureGroup = child.children[i]; - this.featureControls.push( + this.featureControls.push( m('div', { class: 'feature-title', id: `feature-${component.model.name}-${i}`, - style: `position:absolute; left: ${Math.floor(component.domBounds.left + featureGroup.globalBounds.left)}px; + style: `position:absolute; left: ${Math.floor(component.domBounds.left + featureGroup.globalBounds.left)}px; top: ${component.domBounds.top}px; width: ${featureGroup.globalBounds.width}px;`, - onclick: function(){ - let info = child.children[0]; - let order = i; - new FeatureMenu(info,order); - } - }, featureGroup.tags[0]) - ); + onclick: function () { + let info = child.children[i]; + info.position = child.trackPos; + new FeatureMenu(info, child.children[i].config.tracksIndex); + } + }, featureGroup.title) + ); } } // push controller to add new track - this.featureControls.push( - m('div', { - class: 'feature-title', - id: `feature-${component.model.name}-new`, - style: `position:absolute; left: ${Math.floor(component.domBounds.left + child.globalBounds.right + 20)}px; + this.featureControls.push( + m('div', { + class: 'feature-title', + id: `feature-${component.model.name}-new`, + style: `position:absolute; left: ${Math.floor(component.domBounds.left + child.globalBounds.right + 20)}px; top: ${component.domBounds.top}px; width: 20px;`, - onclick: function(){ - let info = child.children[0]; - let order = child.children.lenght; - new FeatureMenu(info,order); - } - },`+`) - ); - } + onclick: function () { + let info = component.model; + info.position = child.trackPos; + let order = child.model.tracks.length; + new FeatureMenu(info, order); + } + }, '+') + ); + } }); }); - } + } + /** - * Horizonal (left to right) layout of BioMaps + * Horizontal (left to right) layout of BioMaps + * + * @returns {Array} + * @private */ + _layoutBioMaps() { - if(! this.bounds) return []; // early out if the layout bounds is unknown + if (!this.bounds) return []; // early out if the layout bounds is unknown let n = this.appState.bioMaps.length; let padding = Math.floor(this.bounds.width * 0.1 / n); padding = 0; // TODO: decide whether to add padding between the biomaps let childHeight = Math.floor(this.bounds.height * 0.95); let cursor = Math.floor(padding * 0.5); - this.bioMapComponents = this.appState.bioMaps.map( (model,mapIndex) => { + this.bioMapComponents = this.appState.bioMaps.map((model, mapIndex) => { + this.bioMapOrder.push(mapIndex); let layoutBounds = new Bounds({ left: cursor, top: 10, @@ -184,7 +247,8 @@ export class HorizontalLayout bioMapModel: model, layoutBounds: layoutBounds, appState: this.appState, - bioMapIndex: mapIndex + bioMapIndex: mapIndex, + initialView : this.appState.initialView[mapIndex] }); model.component = component; // save a reference for mapping model -> component cursor += component.domBounds.width + padding; @@ -192,8 +256,14 @@ export class HorizontalLayout }); } - _layoutPopovers(){ - this.popoverComponents = this.bioMapComponents.map( model => { + + /** + * + * @private + */ + + _layoutPopovers() { + this.popoverComponents = this.bioMapComponents.map(model => { let component = new Popover(); component.info = model.info; component.domBounds = model.domBounds; @@ -201,63 +271,66 @@ export class HorizontalLayout }); } - /** + /** * Horizontal layout of Correspondence Maps. In this layout, for N maps there * are N -1 correspondence maps. + * @returns {Array} + * @private */ + _layoutCorrespondenceMaps() { - if(! this.bounds) return []; // early out if our canvas bounds is unknown + if (!this.bounds) return []; // early out if our canvas bounds is unknown let childHeight = Math.floor(this.bounds.height * 0.95); let n = this.bioMapComponents.length; this.correspondenceMapComponents = []; - for (var i = 0; i < n-1; i++) { + for (let i = 0; i < n - 1; i++) { let left = this.bioMapComponents[i]; - let right = this.bioMapComponents[i+1]; + let right = this.bioMapComponents[i + 1]; let layoutBounds = new Bounds({ - left: Math.floor(left.domBounds.left+left.backbone.globalBounds.right), - right: Math.floor(right.domBounds.left+right.backbone.globalBounds.left), + left: Math.floor(left.domBounds.left + left.backbone.globalBounds.right), + right: Math.floor(right.domBounds.left + right.backbone.globalBounds.left), top: 10, height: childHeight }); let component = new CorrMapComponent({ - bioMapComponents: [ left, right ], + bioMapComponents: [left, right], appState: this.appState, layoutBounds: layoutBounds }); this.correspondenceMapComponents.push(component); } } + /** * Reset local zoom here. Easier to iterate through base element * and redraw components once from the base layout than deal with - * it through the individual components. + * it through the individual components. * (Difficulty in reaching the mithril component to get canvas context) - * + * @private */ - _onReset(){ + + _onReset() { this.bioMapComponents.forEach(item => { item.model.view.visible = item.model.view.base; item.verticalScale = 1.0; item.info.visible = 'hidden'; }); - [].forEach.call(document.getElementsByClassName('cmap-canvas'), el =>{ - el.mithrilComponent.draw(); + [].forEach.call(document.getElementsByClassName('cmap-canvas'), el => { + el.mithrilComponent.draw(); }); m.redraw(); } - - _onFeatureUpdate(msg,data){ - this._layoutBioMaps(); - this._layoutSwapComponents(); - this._layoutFeatureControls(); - var rightShift = 0; - this.appState.bioMaps.map( bmap => { - bmap.component.lb.left = rightShift; - bmap.component.domBounds.left = rightShift; - rightShift += bmap.component.domBounds.width; - }) - this._layoutCorrespondenceMaps(); - this._layoutPopovers(); - } + /** + * + * @param data + * @private + */ + + _onFeatureUpdate(data) { + //this._onDataLoaded(); + this.bioMapComponents[data.mapIndex]._layout(); + m.redraw(); + this._onReorder(); + } } diff --git a/src/ui/layout/LayoutBase.js b/src/ui/layout/LayoutBase.js index a5bdc336..234ffa35 100644 --- a/src/ui/layout/LayoutBase.js +++ b/src/ui/layout/LayoutBase.js @@ -1,24 +1,28 @@ /** - * LayoutBase - * A Mithril component Base class for Layouts, e.g. HorizontalLayout and - * CircosLayout. - */ + * LayoutBase + * A Mithril component Base class for Layouts, e.g. HorizontalLayout and + * CircosLayout. + */ import {Bounds} from '../../model/Bounds'; -export class LayoutBase { +export class LayoutBase { // constructor() - prefer do not use in mithril components /** * mithril lifecycle callback + * @param vnode */ + oninit(vnode) { this.appState = vnode.attrs.appState; } /** * mithril lifecycle method + * @param vnode */ + oncreate(vnode) { // save a reference to this component's dom element this.el = vnode.dom; @@ -27,7 +31,9 @@ export class LayoutBase { /** * mithril lifecycle method + * @param vnode */ + onupdate(vnode) { this.bounds = new Bounds(vnode.dom.getBoundingClientRect()); } diff --git a/src/ui/layout/LayoutContainer.js b/src/ui/layout/LayoutContainer.js index 1f73a035..6e4a0568 100644 --- a/src/ui/layout/LayoutContainer.js +++ b/src/ui/layout/LayoutContainer.js @@ -1,8 +1,8 @@ /** - * LayoutContainer - * A mithril component to wrap the current layout component inside of a - * scrollable div. - */ + * LayoutContainer + * A mithril component to wrap the current layout component inside of a + * scrollable div. + */ import m from 'mithril'; import PubSub from 'pubsub-js'; import {mix} from '../../../mixwith.js/src/mixwith'; @@ -14,7 +14,7 @@ import {Bounds} from '../../model/Bounds'; import {RegisterComponentMixin} from '../RegisterComponentMixin'; // define allowed min/max range for scale (zoom operation) -const SCALE = Object.freeze({ min: 0.05, max: 2}); +const SCALE = Object.freeze({min: 0.05, max: 2}); export class LayoutContainer extends mix().with(RegisterComponentMixin) { @@ -22,31 +22,35 @@ export class LayoutContainer extends mix().with(RegisterComponentMixin) { /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { super.oninit(vnode); this.appState = vnode.attrs.appState; - + this.vnode = vnode; PubSub.subscribe(reset, () => this._onReset()); // create some regular expressions for faster dispatching of events this._gestureRegex = { - pan: new RegExp('^pan'), + pan: new RegExp('^pan'), pinch: new RegExp('^pinch'), - tap: new RegExp('^tap'), + tap: new RegExp('^tap'), wheel: new RegExp('^wheel') }; } /** * mithril lifecycle method + * @param vnode */ + oncreate(vnode) { super.oncreate(vnode); this.el = vnode.dom; // this is the outer m('div') from view() //this._setupEventHandlers(this.el); - this.bounds = new Bounds(this.el.getBoundingClientRect()); - this.contentBounds = new Bounds({ + vnode.state.bounds = this.bounds = new Bounds(this.el.getBoundingClientRect()); + vnode.state.contentBounds = this.contentBounds = new Bounds({ left: 0, top: 0, width: this.bounds.width, @@ -58,15 +62,20 @@ export class LayoutContainer extends mix().with(RegisterComponentMixin) { /** * mithril lifecycle method + * @param vnode */ + onupdate(vnode) { this.bounds = new Bounds(vnode.dom.getBoundingClientRect()); } /** * mithril component render method + * @param vnode + * @returns {*} */ - view() { + + view(vnode) { let b = this.contentBounds || {}; // relative bounds of the layout-container let scale = this.appState.tools.zoomFactor; return m('div.cmap-layout-container', { @@ -75,19 +84,26 @@ export class LayoutContainer extends mix().with(RegisterComponentMixin) { transform: scale(${scale})` }, [ this.appState.tools.layout === HorizontalLayout - ? - m(HorizontalLayout, {appState: this.appState, layoutBounds: this.bounds }) - : - m(CircosLayout, {appState: this.appState, layoutBounds: this.bounds}) + ? + m(HorizontalLayout, { + appState: this.appState, + layoutBounds: this.bounds, + contentBounds: vnode.state.contentBounds + }) + : + m(CircosLayout, {appState: this.appState, layoutBounds: this.bounds}) ]); } /** * handle the event from _dispatchGestureEvt. Returns true or false * to stop or continue event propagation. + * @param evt + * @returns {boolean} */ + handleGesture(evt) { - if(evt.type.match(this._gestureRegex.pan)) { + if (evt.type.match(this._gestureRegex.pan)) { return this._onPan(evt); } else if (evt.type.match(this._gestureRegex.pinch)) { @@ -99,25 +115,40 @@ export class LayoutContainer extends mix().with(RegisterComponentMixin) { return false; // do not stop event propagation } + /** + * + * @param evt + * @returns {boolean} + * @private + */ + _onZoom(evt) { // TODO: utilize the distance of touch event for better interaction const normalized = evt.deltaY / this.bounds.height; const z = this.appState.tools.zoomFactor + normalized; + // noinspection Annotator this.appState.tools.zoomFactor = Math.clamp(z, SCALE.min, SCALE.max); m.redraw(); return true; // stop evt propagation } + /** + * + * @param evt + * @returns {boolean} + * @private + */ + _onPan(evt) { console.log('LayoutContainer -> onPan', evt); // hammer provides the delta x,y in a distance since the start of the // gesture so need to convert it to delta x,y for this event. - if(evt.type === 'panend') { + if (evt.type === 'panend') { this.lastPanEvent = null; - return; + return true; } let delta = {}; - if(this.lastPanEvent) { + if (this.lastPanEvent) { delta.x = -1 * (this.lastPanEvent.deltaX - evt.deltaX); delta.y = -1 * (this.lastPanEvent.deltaY - evt.deltaY); } @@ -126,18 +157,19 @@ export class LayoutContainer extends mix().with(RegisterComponentMixin) { delta.y = evt.deltaY; } this.contentBounds.left += delta.x; - this.contentBounds.top += delta.y; + //this.contentBounds.top += delta.y; m.redraw(); this.lastPanEvent = evt; return true; // stop event propagation } /** - * PubSub event handler + * + * @private */ + _onReset() { this.contentBounds = new Bounds(this.originalContentBounds); m.redraw(); } - } diff --git a/src/ui/layout/components/BioMapComponent.js b/src/ui/layout/components/BioMapComponent.js new file mode 100644 index 00000000..f756e7ad --- /dev/null +++ b/src/ui/layout/components/BioMapComponent.js @@ -0,0 +1,67 @@ +/** + * + * Base Component, placeholder for other canvas components + * + */ + +import m from 'mithril'; +//import {Bounds} from '../../../model/Bounds'; + +export class BioMapComponent { + constructor(vnode) { + console.log(vnode); + } + + oncreate(vnode) { + //have state be tied to passed attributes + vnode.state = vnode.attrs; + + //dom components and state + vnode.state.canvas = vnode.state.bioMap.canvas = vnode.dom; + vnode.state.domBounds = vnode.state.bioMap.domBounds; + vnode.state.context2d = vnode.state.bioMap.context2d = vnode.state.canvas.getContext('2d'); + vnode.state.context2d.imageSmoothingEnabled = false; + + //setup vnode.dom for ui gesture handling + vnode.dom.mithrilComponent = this; + + //store vnode to be able to access state for non mithril lifecycle commands + this.vnode = vnode; + } + + onupdate(vnode) { + //redraw biomap if dirty (drawing has changed, instead of just changing position) + if (vnode.state.bioMap.dirty === true) { + vnode.state.context2d.clearRect(0, 0, vnode.state.canvas.width, vnode.state.canvas.height); + vnode.state.bioMap.draw(); + } + } + + view(vnode) { + // store these bounds, for checking in drawLazily() + let domBounds = vnode.state.domBounds || null; + if (domBounds && !domBounds.isEmptyArea) { + this.lastDrawnMithrilBounds = domBounds; + } + let b = domBounds || {}; + let selectedClass = vnode.state.selected ? 'selected' : ''; + return m('canvas', { + class: `cmap-canvas cmap-biomap ${selectedClass}`, + style: `left: ${b.left}px; top: ${b.top}px; + width: ${b.width}px; height: ${b.height}px; + transform: rotate(${vnode.state.rotation}deg);`, + width: b.width, + height: b.height + }); + } + + handleGesture(evt) { + let state = this.vnode.state; + if (state.bioMap.handleGesture(evt)) { + state.bioMap.dirty = true; + return true; + } + return false; + } +} + diff --git a/src/ui/layout/components/TitleComponent.js b/src/ui/layout/components/TitleComponent.js new file mode 100644 index 00000000..be55afc4 --- /dev/null +++ b/src/ui/layout/components/TitleComponent.js @@ -0,0 +1,153 @@ +/** + * + * Base Component, placeholder for other canvas components + * + */ + +import m from 'mithril'; +import PubSub from 'pubsub-js'; + +import {mapReorder} from '../../../topics'; + +export let TitleComponent = { + oninit: function (vnode) { + vnode.state = vnode.attrs; + vnode.state.left = 0; + vnode.state.domOrder = vnode.state.titleOrder.indexOf(vnode.state.order); + vnode.state.leftBound = vnode.state.bioMaps[vnode.state.order].domBounds.left; + vnode.state.rightBound = vnode.state.bioMaps[vnode.state.order].domBounds.right; + vnode.state.leftStart = vnode.state.bioMaps[vnode.state.order].domBounds.left; + vnode.state._gestureRegex = { + pan: new RegExp('^pan') + }; + }, + + oncreate: function (vnode) { + // register mithrilComponent for gesture handling + vnode.dom.mithrilComponent = this; + // register functions to state/dom for gesture handling + vnode.dom.mithrilComponent.handleGesture = vnode.tag.handleGesture; + vnode.state._onPan = vnode.tag._onPan; + vnode.state.zIndex = 0; + this.vnode = vnode; + }, + + onbeforeupdate: function (vnode) { + vnode.state.bioMaps = vnode.attrs.bioMaps; + if (this.titleOrder[this.domOrder] !== this.order) { + this.domOrder = this.titleOrder.indexOf(this.order); + } + }, + + onupdate: function (vnode) { + let dispOffset = vnode.state.bioMaps[vnode.state.order].domBounds.left - vnode.state.leftStart; + if (vnode.state.left !== dispOffset && !vnode.state.swap) { + this.left = dispOffset; + this.dirty = true; + } + if (vnode.state.swap) { + this.left = 0; + this.swap = false; + this.dirty = true; + this.left = 0; + } + if (this.dirty) { // trigger redraw on changed canvas that has possibly edited bounds in process of view layout + this.dirty = false; + m.redraw(); + } + }, + + view: function (vnode) { + if (!vnode.attrs || !vnode.state.contentBounds) return; + let bMap = vnode.state.bioMaps[vnode.state.order]; + vnode.state.contentBounds.left = vnode.state.contentBounds.right - vnode.state.contentBounds.width; + let left = vnode.state.left + vnode.state.contentBounds.left; + return m('div', { + class: 'swap-div', id: `swap-${vnode.state.domOrder}`, + style: `display:grid; position:relative; left:${left}px; min-width:${bMap.domBounds.width}px; z-index:${vnode.state.zIndex};` + }, + [m('div', {class: 'map-title', style: 'display:inline-block;'}, [bMap.model.name, m('br'), bMap.model.source.id]) + ] + ); + }, + + handleGesture: function (evt) { + if (evt.type.match(this._gestureRegex.pan)) { + return this._onPan(evt); + } + return true; + }, + + _onPan: function (evt) { + //Start pan move zIndex up to prevent interrupting pan early + if (evt.type === 'panstart') { + this.vnode.state.zIndex = 1000; + this.lastPanEvent = null; + this.left = 0; + } + //End pan to set rearrangement + if (evt.type === 'panend') { + this.vnode.state.zIndex = 0; + PubSub.publish(mapReorder, null); + return; + } + + //Pan the title + //Calculate map movement + let delta = {}; + if (this.lastPanEvent) { + delta.x = -1 * (this.lastPanEvent.deltaX - evt.deltaX); + } else { + delta.x = Math.round(evt.deltaX); + } + this.left += delta.x; + + //Setup maps and swap points + let selLeftEdge = this.left + this.leftStart; + //let selRightEdge = selLeftEdge + this.bioMaps[this.order].domBounds.width; + const leftMap = this.domOrder > 0 ? this.titleOrder[this.domOrder - 1] : null; + const rightMap = this.titleOrder[this.domOrder + 1] > -1 ? this.titleOrder[this.domOrder + 1] : null; + const leftSwapBound = leftMap ? this.leftBound - this.bioMaps[leftMap].domBounds.width : null; + const rightSwapBound = rightMap ? this.leftBound + this.bioMaps[rightMap].domBounds.width : null; + + if (leftMap && selLeftEdge < leftSwapBound) { // Swap Left + this.leftBound -= this.bioMaps[leftMap].domBounds.width; + this.rightBound -= this.bioMaps[leftMap].domBounds.width; + + this.titleOrder[this.domOrder] = this.titleOrder[this.domOrder - 1];//= this.titleOrder[rightMap]; + this.titleOrder[this.domOrder - 1] = this.order; + this.domOrder = this.titleOrder[this.domOrder]; + + } else if (rightMap && selLeftEdge > rightSwapBound) { // Swap Right + this.leftBound += this.bioMaps[rightMap].domBounds.width; + this.rightBound += this.bioMaps[rightMap].domBounds.width; + + this.titleOrder[this.domOrder] = this.titleOrder[this.domOrder + 1];//= this.titleOrder[rightMap]; + this.titleOrder[this.domOrder + 1] = this.order; + this.domOrder = this.titleOrder[this.domOrder]; + + } else if (!(!leftMap && selLeftEdge <= 0) && !(!rightMap && selLeftEdge > this.leftBound)) { //Move current map and its left/right partner + + let movedMap = rightMap; + + if (selLeftEdge < this.leftBound || (selLeftEdge === this.leftBound && delta.x < 0)) { + movedMap = leftMap; + } + + let shiftScale = this.bioMaps[this.order].domBounds.width / this.bioMaps[movedMap].domBounds.width; + this.bioMaps[this.order].domBounds.left += delta.x; + this.bioMaps[this.order].domBounds.right += delta.x; + const mw = this.bioMaps[movedMap].domBounds.width; + this.bioMaps[movedMap].domBounds.left -= delta.x * shiftScale; + this.bioMaps[movedMap].domBounds.right = this.bioMaps[movedMap].domBounds.left + mw; + + } else { // edge case don't move map + this.left -= delta.x; + } + + this.lastPanEvent = evt; + m.redraw(); + return true; + } +}; + diff --git a/src/ui/menus/ColorPicker.js b/src/ui/menus/ColorPicker.js index 73e39ca8..9d39789a 100644 --- a/src/ui/menus/ColorPicker.js +++ b/src/ui/menus/ColorPicker.js @@ -8,47 +8,78 @@ import PubSub from 'pubsub-js'; import {pageToCanvas} from '../../util/CanvasUtil'; +/** + * + * @type {{oninit: ColorPicker.oninit, onupdate: ColorPicker.onupdate, view: ColorPicker.view}} + */ export let ColorPicker = { - oninit: function(vnode){ + /** + * + * @param vnode + */ + + oninit: function (vnode) { vnode.state = vnode.attrs; vnode.state.colors = { - baseColor : vnode.attrs.settings.trackColor[vnode.attrs.order] || 'red', - currentColor : null, - hueValueColor : null + baseColor: vnode.attrs.settings.fillColor[vnode.attrs.order] || 'red', + currentColor: null, + hueValueColor: null }; }, - onupdate: function(vnode){ - vnode.attrs.settings.trackColor[vnode.attrs.order] = vnode.state.colors.baseColor; + + /** + * + * @param vnode + */ + + onupdate: function (vnode) { + vnode.attrs.settings.fillColor[vnode.attrs.order] = vnode.state.colors.baseColor; }, - view: function(vnode) { + /** + * + * @param vnode + * @returns {*[]} + */ + + view: function (vnode) { // store these bounds, for checking in drawLazily() - return [ m('div.color-picker',{style:`display:${vnode.state.hidden[vnode.state.order]}`}, [ - m(BaseSelector,{info:vnode.state}), - m(SaturationSelector,{info:vnode.state}), - m(ColorPreview,{info:vnode.state}), - m('div#color-apply-controls',{style:'text-align:center; margin-left:10px; display:inline-block; padding:auto'}, - [m(ColorBox,{info:vnode.state}),//,settings:vnode.attrs.settings}), - m(ColorApplyButton ,{info:vnode.state,settings:vnode.state.settings}), - m(ColorResetButton,{info:vnode.state}) - ] - ) - ]) + return [m('div.color-picker', {style: `display:${vnode.state.hidden[vnode.state.order]}`}, [ + m(BaseSelector, {info: vnode.state}), + m(SaturationSelector, {info: vnode.state}), + m(ColorPreview, {info: vnode.state}), + m('div#color-apply-controls', {style: 'text-align:center; margin-left:10px; display:inline-block; padding:auto'}, + [m(ColorBox, {info: vnode.state}),//,settings:vnode.attrs.settings}), + m(ColorApplyButton, {info: vnode.state, settings: vnode.state.settings}), + m(ColorResetButton, {info: vnode.state}) + ] + ) + ]) ]; } }; +/** + * + * @type {{oncreate: BaseSelector.oncreate, onupdate: BaseSelector.onupdate, view: BaseSelector.view, draw: BaseSelector.draw, handleGesture: BaseSelector.handleGesture, _locationChange: BaseSelector._locationChange, _changeColor: BaseSelector._changeColor, _posFromHsv: BaseSelector._posFromHsv, _hsvFromPos: BaseSelector._hsvFromPos}} + */ export let BaseSelector = { - oncreate:function(vnode) { + + /** + * + * @param vnode + */ + + oncreate: function (vnode) { vnode.dom.mithrilComponent = this; this.vnode = vnode; vnode.state = vnode.attrs; vnode.state.canvas = this.el = vnode.dom; vnode.state.context2d = vnode.dom.getContext('2d'); - if(!vnode.state.info.currentColor || !vnode.state.info.hueValueColor){ + if (!vnode.state.info.currentColor || !vnode.state.info.hueValueColor) { vnode.state.context2d.fillStyle = vnode.state.info.colors.baseColor; - //use the context to convert the original color into a hex string - //avoiding needing to parse html color words + //use the context to convert the original color into a hex string + //avoiding needing to parse html color words vnode.state.info.colors.baseColor = vnode.state.context2d.fillStyle; vnode.state.info.colors.currentColor = vnode.state.context2d.fillStyle; vnode.state.info.colors.hueValueColor = rgbToHsv(hexToRgb(vnode.state.context2d.fillStyle)); @@ -63,18 +94,22 @@ export let BaseSelector = { /** * mithril lifecycle method + * @param vnode */ - onupdate: function(vnode) { + + onupdate: function (vnode) { vnode.state.ptrPos = vnode.dom.mithrilComponent._posFromHsv(vnode.state.info.colors.hueValueColor); - vnode.dom.mithrilComponent.draw(); + vnode.dom.mithrilComponent.draw(); }, /** - * mithril component render method + * + * @returns {*} */ - view: function() { + + view: function () { // store these bounds, for checking in drawLazily() - return m('canvas', { + return m('canvas', { class: 'color-canvas-main', style: 'width: 200; height: 100;', width: 200, @@ -82,7 +117,11 @@ export let BaseSelector = { }); }, - draw: function(){ + /** + * + */ + + draw: function () { let canvas = this.vnode.state.canvas; let ctx = this.vnode.state.context2d; let ptrPos = this.vnode.state.ptrPos; @@ -108,104 +147,144 @@ export let BaseSelector = { // Draw the selection pointer ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.strokeStyle='black'; - ctx.lineWidth=1; - ctx.strokeRect(0,0,canvas.width, canvas.height); + ctx.strokeStyle = 'black'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvas.width, canvas.height); ctx.lineWidth = 2; ctx.beginPath(); - ctx.moveTo(ptrPos.x-10,ptrPos.y); - ctx.lineTo(ptrPos.x-3,ptrPos.y); + ctx.moveTo(ptrPos.x - 10, ptrPos.y); + ctx.lineTo(ptrPos.x - 3, ptrPos.y); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = 'white'; - ctx.moveTo(ptrPos.x-3,ptrPos.y); - ctx.lineTo(ptrPos.x-1,ptrPos.y); - ctx.moveTo(ptrPos.x+1,ptrPos.y); - ctx.lineTo(ptrPos.x+3,ptrPos.y); + ctx.moveTo(ptrPos.x - 3, ptrPos.y); + ctx.lineTo(ptrPos.x - 1, ptrPos.y); + ctx.moveTo(ptrPos.x + 1, ptrPos.y); + ctx.lineTo(ptrPos.x + 3, ptrPos.y); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = 'black'; - ctx.moveTo(ptrPos.x+3,ptrPos.y); - ctx.lineTo(ptrPos.x+10,ptrPos.y); + ctx.moveTo(ptrPos.x + 3, ptrPos.y); + ctx.lineTo(ptrPos.x + 10, ptrPos.y); ctx.stroke(); ctx.beginPath(); - ctx.moveTo(ptrPos.x,ptrPos.y-10); - ctx.lineTo(ptrPos.x,ptrPos.y-3); + ctx.moveTo(ptrPos.x, ptrPos.y - 10); + ctx.lineTo(ptrPos.x, ptrPos.y - 3); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = 'white'; - ctx.moveTo(ptrPos.x,ptrPos.y-3); - ctx.lineTo(ptrPos.x,ptrPos.y-1); - ctx.moveTo(ptrPos.x,ptrPos.y+1); - ctx.lineTo(ptrPos.x,ptrPos.y+3); + ctx.moveTo(ptrPos.x, ptrPos.y - 3); + ctx.lineTo(ptrPos.x, ptrPos.y - 1); + ctx.moveTo(ptrPos.x, ptrPos.y + 1); + ctx.lineTo(ptrPos.x, ptrPos.y + 3); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = 'black'; - ctx.moveTo(ptrPos.x,ptrPos.y+3); - ctx.lineTo(ptrPos.x,ptrPos.y+10); + ctx.moveTo(ptrPos.x, ptrPos.y + 3); + ctx.lineTo(ptrPos.x, ptrPos.y + 10); ctx.stroke(); }, - handleGesture: function(evt){ - if(evt.type.match(this._gestureRegex.tap) || - evt.type.match(this._gestureRegex.pan)){ + /** + * + * @param evt + * @returns {boolean} + */ + + handleGesture: function (evt) { + if (evt.type.match(this._gestureRegex.tap) || + evt.type.match(this._gestureRegex.pan)) { let point = pageToCanvas(evt, this.vnode.state.canvas); this._locationChange(point); } return true; }, - _locationChange: function(evt){ + /** + * + * @param evt + * @private + */ + + _locationChange: function (evt) { let hueValue = this.vnode.state.info.colors.hueValueColor; this.vnode.state.ptrPos = { - x:evt.x, - y:evt.y + x: evt.x, + y: evt.y }; let hsv = this._hsvFromPos(this.vnode.state.ptrPos); hueValue[0] = hsv[0]; hueValue[2] = hsv[2]; - if(!hueValue[1]){ + if (!hueValue[1]) { hueValue[1] = 100; } this._changeColor(); }, - _changeColor: function(){ + /** + * + * @private + */ + + _changeColor: function () { //PubSub to alert the Saturation slider that the position has changed //order is passed to not update *every* color selector //Can be removed, but using PubSub means dynamic response from other forms - PubSub.publish('hueValue',{color:this.vnode.state.colors, order:this.vnode.state.info.order}); + PubSub.publish('hueValue', {color: this.vnode.state.colors, order: this.vnode.state.info.order}); this.draw(); - }, + }, - _posFromHsv: function(hsv){ + /** + * + * @param hsv + * @returns {{x: number, y: number}} + * @private + */ + + _posFromHsv: function (hsv) { // Math.round to avoid annoying sub-pixel rendering - hsv[0] = Math.max(0,Math.min(360,hsv[0])); - hsv[2] = Math.max(0,Math.min(100,hsv[2])); + hsv[0] = Math.max(0, Math.min(360, hsv[0])); + hsv[2] = Math.max(0, Math.min(100, hsv[2])); return { - x: parseFloat(hsv[0]/360)*(this.vnode.state.canvas.width), - y: (1-(hsv[2]/100))*(this.vnode.state.canvas.height) + x: parseFloat(hsv[0] / 360) * (this.vnode.state.canvas.width), + y: (1 - (hsv[2] / 100)) * (this.vnode.state.canvas.height) }; }, - _hsvFromPos: function(pos){ - let h = Math.max(0,(pos.x*360)/this.vnode.state.canvas.width); + /** + * + * @param pos + * @returns {*[]} + * @private + */ + + _hsvFromPos: function (pos) { + let h = Math.max(0, (pos.x * 360) / this.vnode.state.canvas.width); let s = 100; - let l = 100*(1-(pos.y/this.vnode.state.canvas.height)); - return [h,s,l]; + let l = 100 * (1 - (pos.y / this.vnode.state.canvas.height)); + return [h, s, l]; } }; +/** + * + * @type {{oncreate: SaturationSelector.oncreate, onupdate: SaturationSelector.onupdate, view: SaturationSelector.view, draw: SaturationSelector.draw, handleGesture: SaturationSelector.handleGesture, _changeColor: SaturationSelector._changeColor, _hueUpdated: SaturationSelector._hueUpdated, _posFromHsv: SaturationSelector._posFromHsv, _sFromPos: SaturationSelector._sFromPos}} + */ export let SaturationSelector = { - oncreate: function(vnode) { + /** + * + * @param vnode + */ + + oncreate: function (vnode) { vnode.dom.mithrilComponent = this; this.vnode = vnode; vnode.state = vnode.attrs; vnode.state.canvas = this.el = vnode.dom; vnode.state.context2d = vnode.dom.getContext('2d'); - if(!vnode.state.info.colors.hueValueColor[1]){ + if (!vnode.state.info.colors.hueValueColor[1]) { vnode.context2d.fillStyle = vnode.state.info.colors.baseColor; - //use the context to convert the original color into a hex string - //avoiding needing to parse html color words + //use the context to convert the original color into a hex string + //avoiding needing to parse html color words vnode.state.info.colors.hueValueColor[1] = rgbToHsv(hexToRgb(vnode.state.context2d.fillStyle))[1]; } vnode.state.ptrPos = this._posFromHsv(vnode.state.info.colors.hueValueColor); @@ -213,7 +292,9 @@ export let SaturationSelector = { pan: new RegExp('^pan'), tap: new RegExp('^tap') }; - PubSub.subscribe('hueValue', (msg,data)=>{if(data.order === vnode.state.info.order) this._hueUpdated(data.color);}); + PubSub.subscribe('hueValue', (msg, data) => { + if (data.order === vnode.state.info.order) this._hueUpdated(data.color); + }); this._gestureRegex = { pan: new RegExp('^pan'), tap: new RegExp('^tap') @@ -221,101 +302,153 @@ export let SaturationSelector = { this.draw(); }, - onupdate: function(vnode) { + /** + * + * @param vnode + */ + + onupdate: function (vnode) { vnode.state.ptrPos = vnode.dom.mithrilComponent._posFromHsv(vnode.state.info.colors.hueValueColor); - PubSub.publish('satUpdated',{order:vnode.state.info.order,currentColors:vnode.state.info.colors}); // keeps hex value in sync - vnode.dom.mithrilComponent.draw(); + PubSub.publish('satUpdated', {order: vnode.state.info.order, currentColors: vnode.state.info.colors}); // keeps hex value in sync + vnode.dom.mithrilComponent.draw(); }, /** * mithril component render method + * @returns {*} */ - view: function() { + + view: function () { // store these bounds, for checking in drawLazily() - return m('canvas', { - class: 'color-canvas-sat', + return m('canvas#color-canvas-sat', { style: 'margin-left:10px; width: 20; height: 100;', width: 20, height: 100 }); }, - draw: function(){ + /** + * + */ + + draw: function () { let canvas = this.vnode.state.canvas; let ctx = this.vnode.state.context2d; let ptrPos = this.vnode.state.ptrPos; // clear and redraw gradient slider for current picked HueValue color; ctx.clearRect(0, 0, canvas.width, canvas.height); - var grad = ctx.createLinearGradient(0, 0, 0,canvas.height); + let grad = ctx.createLinearGradient(0, 0, 0, canvas.height); let hueValueColor = this.vnode.state.info.colors.hueValueColor; - let rgbStart = hsvToRgb([hueValueColor[0],100,hueValueColor[2]]).map(color => { + let rgbStart = hsvToRgb([hueValueColor[0], 100, hueValueColor[2]]).map(color => { return Math.floor(color); }); - let rgbStop = hsvToRgb([hueValueColor[0],0,hueValueColor[2]]).map(color => { + let rgbStop = hsvToRgb([hueValueColor[0], 0, hueValueColor[2]]).map(color => { return Math.floor(color); }); - grad.addColorStop(0 , `rgba(${rgbStart[0]},${rgbStart[1]},${rgbStart[2]},1)`); - grad.addColorStop(1 , `rgba(${rgbStop[0]},${rgbStop[1]},${rgbStop[2]},1)`); + grad.addColorStop(0, `rgba(${rgbStart[0]},${rgbStart[1]},${rgbStart[2]},1)`); + grad.addColorStop(1, `rgba(${rgbStop[0]},${rgbStop[1]},${rgbStop[2]},1)`); ctx.fillStyle = grad; ctx.fillRect(0, 0, canvas.width, canvas.height); // draw slider pointer - ctx.strokeStyle='black'; - ctx.lineWidth=1; - ctx.strokeRect(0,0,canvas.width, canvas.height); - ctx.fillStyle = 'black'; + ctx.strokeStyle = 'black'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = 'black'; ctx.beginPath(); ctx.strokeStyle = 'white'; - ctx.moveTo(canvas.width,ptrPos+5); - ctx.lineTo(canvas.width/2, ptrPos); - ctx.lineTo(canvas.width,ptrPos-5); + ctx.moveTo(canvas.width, ptrPos + 5); + ctx.lineTo(canvas.width / 2, ptrPos); + ctx.lineTo(canvas.width, ptrPos - 5); ctx.closePath(); ctx.fill(); ctx.stroke(); }, - handleGesture: function(evt){ - if(evt.type.match(this._gestureRegex.tap) || - evt.type.match(this._gestureRegex.pan)){ - var point = pageToCanvas(evt, this.vnode.state.canvas); + /** + * + * @param evt + * @returns {boolean} + */ + + handleGesture: function (evt) { + if (evt.type.match(this._gestureRegex.tap) || + evt.type.match(this._gestureRegex.pan)) { + let point = pageToCanvas(evt, this.vnode.state.canvas); this._changeColor(point); } return true; }, - _changeColor: function(evt){ + /** + * + * @param evt + * @private + */ + + _changeColor: function (evt) { this.vnode.state.ptrPos = evt.y; this.vnode.state.info.colors.hueValueColor[1] = this._sFromPos(this.vnode.state.ptrPos); this._hueUpdated(this.vnode.state.info.colors); - }, + }, + + /** + * + * @private + */ - _hueUpdated: function(){ - PubSub.publish('satUpdated',{order:this.vnode.state.info.order,currentColors:this.vnode.state.info.colors}); + _hueUpdated: function () { + PubSub.publish('satUpdated', {order: this.vnode.state.info.order, currentColors: this.vnode.state.info.colors}); this.draw(); }, - _posFromHsv: function(hsv){ - return Math.round((1-(hsv[1]/100))*this.vnode.state.canvas.height); + /** + * + * @param hsv + * @returns {number} + * @private + */ + + _posFromHsv: function (hsv) { + return Math.round((1 - (hsv[1] / 100)) * this.vnode.state.canvas.height); }, - _sFromPos: function(pos){ - return 100*(1-(pos/this.vnode.state.canvas.height)); + /** + * + * @param pos + * @returns {number} + * @private + */ + + _sFromPos: function (pos) { + return 100 * (1 - (pos / this.vnode.state.canvas.height)); } }; -export let ColorPreview = { - oncreate: function(vnode) { +/** + * + * @type {{oncreate: ColorPreview.oncreate, onupdate: ColorPreview.onupdate, view: ColorPreview.view, draw: ColorPreview.draw}} + */ + +export let ColorPreview = { + + /** + * + * @param vnode + */ + + oncreate: function (vnode) { this.order = vnode.attrs.info.order; this.colors = vnode.attrs.info.colors; this.canvas = this.el = vnode.dom; this.context2d = this.canvas.getContext('2d'); - PubSub.subscribe('satUpdated',(msg,data) =>{ - if(this.order === data.order){ + PubSub.subscribe('satUpdated', (msg, data) => { + if (this.order === data.order) { let fillColor = hsvToRgb(data.currentColors.hueValueColor).map(color => { return Math.floor(color); }); - this.context2d.fillStyle = `rgba(${fillColor[0]},${fillColor[1]},${fillColor[2]},1)`; - this.colors.currentColor = this.context2d.fillStyle; + this.context2d.fillStyle = `rgba(${fillColor[0]},${fillColor[1]},${fillColor[2]},1)`; + this.colors.currentColor = this.context2d.fillStyle; this.draw(); } }); @@ -324,174 +457,247 @@ export let ColorPreview = { /** * mithril lifecycle method + * */ - onupdate: function() { - this.draw(); + + onupdate: function () { + this.draw(); }, /** * mithril component render method + * @returns {*} */ - view: function() { - return m('canvas#color-canvas-preview', { + + view: function () { + return m('canvas#color-canvas-preview', { style: 'margin-left:10px; width: 20; height: 100;', width: 20, height: 100 - }); + }); }, - draw: function(){ + /** + * + */ + + draw: function () { let ctx = this.context2d; ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); ctx.fillStyle = this.colors.currentColor; ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); - ctx.strokeStyle='black'; - ctx.lineWidth=1; - ctx.strokeRect(0,0,this.canvas.width, this.canvas.height); + ctx.strokeStyle = 'black'; + ctx.lineWidth = 1; + ctx.strokeRect(0, 0, this.canvas.width, this.canvas.height); } }; -// Use currently selected color +/** + * Use currently selected color + * @type {{view: ColorApplyButton.view}} + */ + export let ColorApplyButton = { + /** * mithril component render method + * @param vnode + * @returns {*} */ - view: function(vnode) { + + view: function (vnode) { // store these bounds, for checking in drawLazily() - return m('button.approve-button', { + return m('button.approve-button', { style: 'display:block; width:100%;', - onclick:()=>{ + onclick: () => { vnode.attrs.info.colors.baseColor = vnode.attrs.info.colors.currentColor; vnode.attrs.info.colors.hueValueColor = rgbToHsv(hexToRgb(vnode.attrs.info.colors.baseColor)); vnode.attrs.settings.nodeColor[vnode.attrs.info.order] = vnode.attrs.info.colors.baseColor; - PubSub.publish('satUpdated',{order:vnode.attrs.info.order,currentColors:vnode.attrs.info.colors}); + PubSub.publish('satUpdated', {order: vnode.attrs.info.order, currentColors: vnode.attrs.info.colors}); } - },'Apply'); + }, 'Apply'); } }; -// Reset color to prior +/** + * Reset color to prior + * @type {{view: ColorResetButton.view}} + */ + export let ColorResetButton = { + /** * mithril component render method + * @param vnode + * @returns {*} */ - view: function(vnode) { + + view: function (vnode) { // store these bounds, for checking in drawLazily() - return m('button.reset-button', { + return m('button.reset-button', { style: 'display:block; width:100%', - onclick:()=>{ + onclick: () => { vnode.attrs.info.colors.currentColor = vnode.attrs.info.colors.baseColor; vnode.attrs.info.colors.hueValueColor = rgbToHsv(hexToRgb(vnode.attrs.info.colors.baseColor)); - PubSub.publish('satUpdated',{order:vnode.attrs.info.order,currentColors:vnode.attrs.info.colors}); - } - },'Reset'); + PubSub.publish('satUpdated', {order: vnode.attrs.info.order, currentColors: vnode.attrs.info.colors}); + } + }, 'Reset'); } }; -// Text Box to find color +/** + * Text Box to find color + * @type {{oninit: ColorBox.oninit, view: ColorBox.view, handleGesture: ColorBox.handleGesture}} + */ + export let ColorBox = { - oninit: function(vnode) { + + /** + * + * @param vnode + */ + + oninit: function (vnode) { this.canvas = this.el = vnode.dom; this.order = vnode.attrs.info.order; vnode.state.value = vnode.attrs.info.colors.currentColor; - PubSub.subscribe('satUpdated',(msg,data) =>{ - if(this.order === data.order){ + PubSub.subscribe('satUpdated', (msg, data) => { + if (this.order === data.order) { vnode.dom.value = vnode.attrs.info.colors.currentColor; } }); }, + /** * mithril component render method + * @param vnode + * @returns {*} */ - view: function(vnode) { + + view: function (vnode) { // store these bounds, for checking in drawLazily() - return m('input[type=text].color-input', { - style: 'display:block; width:100%;', - oninput: m.withAttr('value', function(value) { - try { - let code = value.match(/^#?([a-f\d]*)$/i); - let str = code[1]; - if(code[1].length === 3){ - str = `#${str[0]}${str[0]}${str[1]}${str[1]}${str[2]}${str[2]}`; - } - vnode.attrs.info.colors.currentColor = value; - vnode.attrs.info.colors.hueValueColor = rgbToHsv(hexToRgb(str)); - } catch(e) { - // expect this to fail silently, as most typing will not actually give - // a proper hex triplet/sextet + return m('input[type=text].color-input', { + style: 'display:block; width:100%;', + oninput: m.withAttr('value', function (value) { + try { + let code = value.match(/^#?([a-f\d]*)$/i); + let str = code[1]; + if (code[1].length === 3) { + str = `#${str[0]}${str[0]}${str[1]}${str[1]}${str[2]}${str[2]}`; } - }) - }); + vnode.attrs.info.colors.currentColor = value; + vnode.attrs.info.colors.hueValueColor = rgbToHsv(hexToRgb(str)); + } catch (e) { + // expect this to fail silently, as most typing will not actually give + // a proper hex triplet/sextet + } + }) + }); }, - handleGesture: function(){ + /** + * + * @returns {boolean} + */ + + handleGesture: function () { return true; } }; -// #FFFFFF ->[0-255,0-255,0-255] -export function hexToRgb(hex){ - var result = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); - return [parseInt(result[1], 16),parseInt(result[2], 16),parseInt(result[3], 16)]; +/** + * convert hex triplet to RGB + * #FFFFFF ->[0-255,0-255,0-255] + * @param hex + * @returns {*[]} + */ + +export function hexToRgb(hex) { + let result = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i); + return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)]; } -// [0-255,0-255,0-255] -> #FFFFFF -export function rgbToHex(rgb){ - return ( +/** + * convert RGB triplet to hex + * [0-255,0-255,0-255] -> #FFFFFF + * @param rgb + * @returns {string} + */ + +export function rgbToHex(rgb) { + return ( (0x100 | Math.round(rgb[0])).toString(16).substr(1) + (0x100 | Math.round(rgb[1])).toString(16).substr(1) + (0x100 | Math.round(rgb[2])).toString(16).substr(1) - ); + ); } -// [0-255,0-255,0-255] -> [0-360,0-100,0-100] -export function rgbToHsv(rgb){ - //make sure RGB values are within 0-255 range - //and convert to decimal - rgb = rgb.map(component =>{ - return Math.max(0,Math.min(255,component))/255; - }); +/** + * Convert RGB triplet ot HSV + * [0-255,0-255,0-255] -> [0-360,0-100,0-100] + * @param rgb + * @returns {*[]} + */ + +export function rgbToHsv(rgb) { + //make sure RGB values are within 0-255 range + //and convert to decimal + rgb = rgb.map(component => { + return Math.max(0, Math.min(255, component)) / 255; + }); // Conversion from RGB -> HSV colorspace - let cmin = Math.min(Math.min(rgb[0],rgb[1]),rgb[2]); - let cmax = Math.max(Math.max(rgb[0],rgb[1]),rgb[2]); - let delta = parseFloat(cmax - cmin); - let hue = 0; - if(delta === 0){ + + let cmin = Math.min(Math.min(rgb[0], rgb[1]), rgb[2]); + let cmax = Math.max(Math.max(rgb[0], rgb[1]), rgb[2]); + let delta = parseFloat(cmax - cmin); + let hue = 0; + if (delta === 0) { hue = 0; - } else if( cmax === rgb[0]){ - hue = 60*(((rgb[1]-rgb[2])/delta)); - } else if( cmax === rgb[1]){ - hue = 60*(((rgb[2]-rgb[0])/delta)+2); - } else if( cmax === rgb[2]){ - hue = 60*(((rgb[0]-rgb[1])/delta)+4); - } - if (hue < 0) hue +=360; - let sat = cmax === 0 ? 0 : (delta/cmax)*100; - let value = cmax*100; - - return [hue,sat,value]; -} - - -// [0-360,0-100,0-100] -> [0-255,0-255,0-255] -export function hsvToRgb(hsv){ - let u = 255 * (hsv[2] / 100); - let h = hsv[0]/60; - let s = hsv[1]/100; + } else if (cmax === rgb[0]) { + hue = 60 * (((rgb[1] - rgb[2]) / delta)); + } else if (cmax === rgb[1]) { + hue = 60 * (((rgb[2] - rgb[0]) / delta) + 2); + } else if (cmax === rgb[2]) { + hue = 60 * (((rgb[0] - rgb[1]) / delta) + 4); + } + if (hue < 0) hue += 360; + let sat = cmax === 0 ? 0 : (delta / cmax) * 100; + let value = cmax * 100; + + return [hue, sat, value]; +} + +/** + * Convert from HSV to RGB + * [0-360,0-100,0-100] -> [0-255,0-255,0-255] + * @param hsv + * @returns {*[]} + */ + +export function hsvToRgb(hsv) { + let u = 255 * (hsv[2] / 100); + let h = hsv[0] / 60; + let s = hsv[1] / 100; let i = Math.floor(h); - if(i < 0) i = 0; - let f = i%2 ? h-i : 1-(h-i); + if (i < 0) i = 0; + let f = i % 2 ? h - i : 1 - (h - i); let m = u * (1 - s); let n = u * (1 - s * f); switch (i) { case 6: - case 0: return [u,n,m]; - case 1: return [n,u,m]; - case 2: return [m,u,n]; - case 3: return [m,n,u]; - case 4: return [n,m,u]; - case 5: return [u,m,n]; - } + case 0: + return [u, n, m]; + case 1: + return [n, u, m]; + case 2: + return [m, u, n]; + case 3: + return [m, n, u]; + case 4: + return [n, m, u]; + case 5: + return [u, m, n]; + } } diff --git a/src/ui/menus/Feature.js b/src/ui/menus/Feature.js index b3fd3605..5d33d3ab 100644 --- a/src/ui/menus/Feature.js +++ b/src/ui/menus/Feature.js @@ -1,79 +1,116 @@ /** - * Feature - * A mithril component for displaying feature information. - */ + * Feature + * A mithril component for displaying feature information. + */ import m from 'mithril'; import PubSub from 'pubsub-js'; -import {featureUpdate, reset} from '../../topics'; +import {featureUpdate} from '../../topics'; import {mix} from '../../../mixwith.js/src/mixwith'; -import {Menu} from './Menus'; +import {Menu} from './Menu'; import {RegisterComponentMixin} from '../RegisterComponentMixin'; -export class FeatureMenu extends mix(Menu).with(RegisterComponentMixin){ +export class FeatureMenu extends mix(Menu).with(RegisterComponentMixin) { - oninit(vnode){ + /** + * + * @param vnode + */ + + oninit(vnode) { super.oninit(vnode); this.tagList = vnode.attrs.info.parent.parent.model.tags.sort(); this.settings = vnode.attrs.info.parent.parent.model.qtlGroups[vnode.attrs.order]; this.groupTags = vnode.attrs.info.tags; - this.selected = { name: this.settings.filter, index: this.tagList.indexOf(this.settings.filter)}; + this.selected = {name: this.settings.filter, index: this.tagList.indexOf(this.settings.filter)}; } - + /** * mithril component render method + * @param vnode + * @returns {*} */ + view(vnode) { - let info = vnode.attrs.info || {}; + //let info = vnode.attrs.info || {}; + //let order = vnode.attrs.order || 0; let bounds = vnode.attrs.bounds || {}; - let order = vnode.attrs.order || 0; let modal = this; modal.rootNode = vnode; - + return m('div', { - class: 'feature-menu', - style: `position:absolute; left: 0px; top: 0px; width:${bounds.width}px;height:${bounds.height}px`, - onclick: function(){console.log('what',this);} - },[this._dropDown(modal), this._applyButton(modal), this._closeButton(modal)]); + class: 'feature-menu', + style: `position:absolute; left: 0px; top: 0px; width:${bounds.width}px;height:${bounds.height}px`, + onclick: function () { + console.log('what', this); + } + }, [this._dropDown(modal), this._applyButton(modal), this._closeButton(modal)]); } - - _applyButton(modal){ - return m('button',{ - onclick: function(){ - console.log('what what', modal.settings); - modal.settings.filter = modal.selected.name; - modal.groupTags[0] = modal.selected.name; - PubSub.publish(featureUpdate, null); - modal.rootNode.dom.remove(modal.rootNode); - } - },'Apply Selection'); + + /** + * + * @param modal + * @returns {*} + * @private + */ + + _applyButton(modal) { + return m('button', { + onclick: function () { + console.log('what what', modal.settings); + modal.settings.filter = modal.selected.name; + modal.groupTags[0] = modal.selected.name; + PubSub.publish(featureUpdate, null); + modal.rootNode.dom.remove(modal.rootNode); + } + }, 'Apply Selection'); } - _closeButton(modal){ - return m('button',{ - onclick: function(){ - modal.rootNode.dom.remove(modal.rootNode); - } - },'Close'); + /** + * + * @param modal + * @returns {*} + * @private + */ + + _closeButton(modal) { + return m('button', { + onclick: function () { + modal.rootNode.dom.remove(modal.rootNode); + } + }, 'Close'); } - _dropDown(modal){ + /** + * + * @param modal + * @returns {*} + * @private + */ + + _dropDown(modal) { console.log('what inner', modal, modal.rootNode); let selector = this; - return m('select',{ - selectedIndex : selector.selected.index, - onchange: function(e){ - console.log("selected on drop",this,e); + return m('select', { + selectedIndex: selector.selected.index, + onchange: function (e) { + console.log('selected on drop', this, e); selector.selected.name = e.target.value; selector.selected.index = modal.tagList.indexOf(e.target.value); - }},[modal.tagList.map(tag => { + } + }, [modal.tagList.map(tag => { return m('option', tag); - }) - ]) + }) + ]); } - handleGesture(){ - // prevent interacting with div from propegating events - return true; - } + /** + * + * @returns {boolean} + */ + + handleGesture() { + // prevent interacting with div from propagating events + return true; + } } diff --git a/src/ui/menus/FeatureMenu.js b/src/ui/menus/FeatureMenu.js index 564ffef1..c2294275 100644 --- a/src/ui/menus/FeatureMenu.js +++ b/src/ui/menus/FeatureMenu.js @@ -8,9 +8,14 @@ import PubSub from 'pubsub-js'; import {featureUpdate} from '../../topics'; import {ColorPicker} from './ColorPicker'; - export class FeatureMenu { - constructor(data,order){ + /** + * + * @param data + * @param order + */ + + constructor(data, order) { // Setup modal position based on current placement of the actual map // layout viewport. keeps things self-contained when embedding. let viewport = document.getElementById('cmap-menu-viewport'); @@ -22,242 +27,389 @@ export class FeatureMenu { viewport.style.left = `${layoutBounds.left}px`; viewport.style.width = '95%'; viewport.style.height = `${layoutBounds.height}px`; - // Setup track and subtrack data - let model = data.parent.parent.model; - let tagList = model.tags.sort(); - let settings = {}; - let trackGroups = []; - let defaultSettings = model.qtlGroups && model.qtlGroups[order] != undefined ? {filters:model.qtlGroups[order].filters.slice(0),trackColor:model.qtlGroups[order].trackColor.slice(0)} : undefined; - if(order == undefined){ - order = model.qtlGroups ? model.qtlGroups.length : 0; - } - - if(!model.qtlGroups || model.qtlGroups[0] === undefined){ - order = 0; - settings = {filters:[tagList[0]],trackColor:['red']}; - trackGroups[0]= settings; - model.qtlGroups = []; - } else { - trackGroups = model.qtlGroups.slice(0); - if(!trackGroups[order]){ - trackGroups[order] = {filters:[tagList[0]],trackColor:['red']}; - } - settings = trackGroups[order]; + let model = data.model || data.component.model; + let tagList = model.tags.sort(); + let settings = model.tracks[order] ? data.config : model.config.qtl; + if(typeof settings.fillColor === 'string'){settings.fillColor = [settings.fillColor];} + let trackGroups = []; + let filters = settings.filters ? settings.filters.slice() : [tagList[0].slice()]; + let fillColor = settings.fillColor ? settings.fillColor.slice() : ['aqua']; + if(typeof fillColor === 'string'){fillColor = [fillColor];} + + + if(!settings.filters){ + settings.filters = filters; + } + if(!settings.fillColor){ + settings.fillColor = fillColor; } - - let selected = settings.filters.map( (item) => { - return { - name: item, - index: tagList.indexOf(item) - }; + if(!settings.title){ + settings.title = filters[0]; + } + + let defaultSettings = { + filters : settings.filters.slice(), + fillColor : settings.fillColor.slice(), + title : settings.title.slice() + }; + + settings.position = data.position; + + let selected = filters.map((item) => { + return { + name: item, + index: tagList.indexOf(item) + }; }); let trackConfig = { model: model, tagList: tagList, - settings: settings, - selected: selected, - trackGroups:trackGroups - }; + settings: settings, + selected: selected, + trackGroups: trackGroups + }; //Attach components to viewport, in general these are the close button (x in top - //right), the acutal modal contents, and the apply/close/delete button bar + //right), the actual modal contents, and the apply/close/delete button bar let controls = [ - m(_applyButton,{qtl:model.qtlGroups,track:trackGroups,order:order,reset:defaultSettings,newData:selected}), - m(_cancelButton,{qtl:model.qtlGroups,order:order,reset:defaultSettings,newData:selected}) - ]; - - if(model.qtlGroups[order] != undefined){ - controls.push(m(_removeButton,{qtl:model.qtlGroups,order:order,reset:defaultSettings,newData:selected})); + m(_applyButton, { + model:model, + config : settings, + order: order, + bioMapIndex : model.component.bioMapIndex, + reset: defaultSettings, + newData : selected + }), + m(_cancelButton, {model: model, config: settings, order: order, reset: defaultSettings}) + ]; + + if (order < model.tracks.length) { + controls.push(m(_removeButton, { + order:order, + model:model, + bioMapIndex : model.component.bioMapIndex, + })); } - - // Buld menu mithril component, then mount + + // Build menu mithril component, then mount let modalDiv = { - oncreate: function (vnode){ + oncreate: function (vnode) { vnode.dom.mithrilComponent = this; // Without this and handleGesture, clicks in modal will pass through to the underlying view }, - view: function(vnode){ - return m('div',{style:'height:100%; width:100%'},[ - m(CloseButton,{qtl:model.qtlGroups,order:order,reset:defaultSettings,newData:selected}), - m(TrackMenu,{info:trackConfig,count:0}), - m('div',{style:'text-align:center'},controls) - ] - ) + view: function () { + return m('div', {style: 'height:100%; width:100%'}, [ + m(CloseButton, {model: model, config: settings, order: order, reset: defaultSettings}), + m(TitleBox, {settings: settings}), + m(TrackMenu, {info: trackConfig, count: 0}), + m('div', {style: 'text-align:center'}, controls) + ] + ); }, - handleGesture: function(){ + handleGesture: function () { return true; } - } + }; m.mount(document.getElementById('cmap-menu-viewport'), modalDiv); } } +/** + * + * @type {{view: _removeButton.view}} + * @private + */ + export let _removeButton = { - view: function(vnode){ - return m('button',{ - onclick: - ()=>{ - vnode.attrs.qtl.splice(vnode.attrs.order,1); - PubSub.publish(featureUpdate,null); - m.redraw(); + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { + return m('button', { + onclick: + () => { + vnode.attrs.model.tracks.splice(vnode.attrs.order, 1); + PubSub.publish(featureUpdate, {mapIndex: vnode.attrs.bioMapIndex}); + m.redraw(); closeModal(); }, - style:'background:red' - },'Remove Track'); + style: 'background:red' + }, 'Remove Track'); } }; +export let TitleBox = { + /** + * + * @param vnode + * @returns {*} + */ + oninit: function (vnode) { + vnode.state.value = vnode.attrs.settings.title; + }, + view: function (vnode) { + return m('div',{},'Track title: ', + m('input[type=text].title-input', { + style: 'display:block; width:10%;', + defaultValue : vnode.attrs.settings.title, + oninput: m.withAttr('value', function (value) { + try { + vnode.attrs.settings.title = value; + } catch (e) { + // expect this to fail silently, as most typing will not actually give + // a proper hex triplet/sextet + } + }) + }) + ); + } +}; + + +/** + * + * @type {{view: _cancelButton.view}} + * @private + */ + export let _cancelButton = { - view: function(vnode){ - return m('button',{ - onclick: - ()=>{ - if(vnode.attrs.qtl && vnode.attrs.qtl[vnode.attrs.order] != undefined){ - vnode.attrs.qtl[vnode.attrs.order] = vnode.attrs.reset; + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { + return m('button', { + onclick: + () => { + vnode.attrs.config.fillColor = vnode.attrs.reset.fillColor; + vnode.attrs.config.filters = vnode.attrs.reset.filters; + vnode.attrs.config.title = vnode.attrs.reset.title; + if(vnode.attrs.order < vnode.attrs.model.tracks.length) { + vnode.attrs.model.tracks[vnode.attrs.order] = vnode.attrs.config; } closeModal(); } - },'Close'); + }, 'Cancel'); } }; +/** + * + * @type {{view: _applyButton.view}} + * @private + */ + export let _applyButton = { - view: function(vnode){ - return m('button',{ - onclick: function(){ - let order = vnode.attrs.order; - let filters = vnode.attrs.newData.map( selected => { - return selected.name; - }); - let colors = vnode.attrs.track[order].trackColor; - vnode.attrs.qtl[order] = {filters: filters.slice(0), trackColor:colors.slice(0)}; - PubSub.publish(featureUpdate,null); - m.redraw(); - closeModal(); - } - },'Apply Selection'); + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { + return m('button', { + onclick: function () { + vnode.attrs.config.filters = vnode.attrs.newData.map(data => {return data.name;}); + vnode.attrs.model.tracks[vnode.attrs.order] = vnode.attrs.config; + PubSub.publish(featureUpdate, {mapIndex: vnode.attrs.bioMapIndex}); + m.redraw(); + closeModal(); + } + }, 'Apply Selection'); } }; -/* +/** * Div with simple close X + * @type {{view: CloseButton.view}} */ -export let CloseButton = { - view: function(vnode){ + +export let CloseButton = { + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { return m('div', - { style:'text-align:right;', - onclick: - ()=>{ - if(vnode.attrs.qtl && vnode.attrs.qtl[vnode.attrs.order] != undefined){ - vnode.attrs.qtl[vnode.attrs.order] = vnode.attrs.reset; + { + style: 'text-align:right;', + onclick: + () => { + vnode.attrs.config.fillColor = vnode.attrs.reset.fillColor; + vnode.attrs.config.filters = vnode.attrs.reset.filters; + vnode.attrs.config.title = vnode.attrs.reset.title; + if(vnode.attrs.order < vnode.attrs.model.tracks.length) { + vnode.attrs.model.tracks[vnode.attrs.order] = vnode.attrs.config; + } + closeModal(); } - closeModal(); - } - },'X'); + }, 'X'); } }; /* * Mithril component * Div that contains the dropdowns and components for selecting track options + * @type {{oninit: TrackMenu.oninit, view: TrackMenu.view}} */ + export let TrackMenu = { - oninit: function (vnode){ + + /** + * + * @param vnode + */ + + oninit: function (vnode) { vnode.state = vnode.attrs; - vnode.state.hidden = []; - vnode.state.picker = []; + vnode.state.hidden = []; + vnode.state.picker = []; }, - view: function(vnode){ - let selected = vnode.state.info.selected; - let settings = vnode.state.info.settings; - this.count = 0; - - let dropdows = selected.map( (item,order)=>{ - if(settings.trackColor[order] == undefined){ - settings.trackColor[order] = settings.trackColor.slice(0,1); + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { + let selected = vnode.state.info.selected; + let settings = vnode.state.info.settings; + this.count = 0; + + + let dropdowns = selected.map((item, order) => { + if (settings.fillColor[order] === undefined) { + settings.fillColor[order] = settings.fillColor.slice(0, 1); + } + if (!vnode.state.hidden[order]) { + vnode.state.hidden[order] = 'none'; + } + if (!vnode.state.picker[order]) { + vnode.state.picker[order] = settings.fillColor[order] || 'orange'; + } + + let dropSettings = { + selected: selected, + name: settings.filters[order], + fillColor: settings.fillColor, + tags: vnode.state.info.tagList, + nodeColor: vnode.state.picker + }; + if (selected[order].index === -1) { + selected[order].index = dropSettings.tags.indexOf(dropSettings.name); } - if(!vnode.state.hidden[order]){ - vnode.state.hidden[order] = 'none'; - } - if(!vnode.state.picker[order]){ - vnode.state.picker[order] = settings.trackColor[order] || 'orange'; - } - let dropSettings = { - selected: selected, - name: settings.filters[order], - trackColor: settings.trackColor, - tags: vnode.state.info.tagList, - nodeColor: vnode.state.picker - }; - if(selected[order].index === -1){ - selected[order].index = dropSettings.tags.indexOf(dropSettings.name); - } let controls = [ - m('button',{onclick : () =>{ - selected[selected.length] = {name:vnode.state.info.tagList[0],index:0}; - }},'+') + m('button', { + onclick: () => { + selected[selected.length] = {name: vnode.state.info.tagList[0], index: 0}; + } + }, '+') ]; - if(selected.length > 1){ - controls.push(m('button',{onclick: () => { selected.splice(order,1);}},'-')); + if (selected.length > 1) { + controls.push(m('button', { + onclick: () => { + selected.splice(order, 1); + } + }, '-')); } - controls.push(m('button',{ - onclick: () => { - vnode.state.hidden[order] = vnode.state.hidden[order]==='none'? 'block':'none'; - } - },m('div', - {style:`color:${vnode.state.picker[order]}`} - ,'â– ') - )); - return [m(Dropdown,{settings:dropSettings,order:order,parentDiv:this,hidden:vnode.state.hidden}),controls]; - }); - return m('div#track-select-div',{ - style:'overflow:auto;width:100%;height:80%;' - },dropdows); + controls.push(m('button', { + onclick: () => { + vnode.state.hidden[order] = vnode.state.hidden[order] === 'none' ? 'block' : 'none'; + } + }, m('div', + {style: `color:${vnode.state.picker[order]}`} + , 'â– ') + )); + return [m(Dropdown, { + settings: dropSettings, + order: order, + parentDiv: this, + hidden: vnode.state.hidden + }), controls]; + }); + return m('div#track-select-div', { + style: 'overflow:auto;width:100%;height:80%;' + }, dropdowns); } }; /* * Mithril component * Actual dropdown selector + * @type {{oninit: Dropdown.oninit, onbeforeupdate: Dropdown.onbeforeupdate, view: Dropdown.view}} */ + export let Dropdown = { - oninit: function(vnode){ + + /** + * + * @param vnode + */ + + oninit: function (vnode) { vnode.state = vnode.attrs; }, - onbeforeupdate: function(vnode){ - if(vnode.state.count > vnode.attrs.parentDiv.count){ + /** + * + * @param vnode + */ + + onbeforeupdate: function (vnode) { + if (vnode.state.count > vnode.attrs.parentDiv.count) { vnode.attrs.parentDiv.count = vnode.state.count; } else { vnode.state.count = vnode.attrs.parentDiv.count; } }, - view: function(vnode){ + + /** + * + * @param vnode + * @returns {*} + */ + + view: function (vnode) { let order = vnode.state.order; - let settings = vnode.state.settings; - return m('div',m('select',{ - id:`selector-${order}`, - selectedIndex : settings.selected[order].index, - oninput: (e)=>{ - var selected = e.target.selectedIndex; + let settings = vnode.state.settings; + return m('div', m('select', { + id: `selector-${order}`, + selectedIndex: settings.selected[order].index, + oninput: (e) => { + let selected = e.target.selectedIndex; settings.selected[order].name = settings.tags[selected]; settings.selected[order].index = selected; - } - },[settings.tags.map(tag => { + } + }, [settings.tags.map(tag => { return m('option', tag); - }) - ]), m(ColorPicker,{settings:vnode.state.settings,order:order,hidden:vnode.state.hidden})); - } + }) + ]), m(ColorPicker, {settings: vnode.state.settings, order: order, hidden: vnode.state.hidden})); + } }; -/* + +/** * Function to close the menu-viewport and reshow the * layout viewport + * */ -export function closeModal (){ + +export function closeModal() { //reset cmap-menu-viewport vdom tree to empty state - m.mount(document.getElementById('cmap-menu-viewport'),null); - //explicity set visibility to avoid weird page interaction issues + m.mount(document.getElementById('cmap-menu-viewport'), null); + //explicitly set visibility to avoid weird page interaction issues document.getElementById('cmap-layout-viewport').style.visibility = 'visible'; document.getElementById('cmap-menu-viewport').style.display = 'none'; } diff --git a/src/ui/menus/Menu.js b/src/ui/menus/Menu.js index 450fb044..8fc1ab1e 100644 --- a/src/ui/menus/Menu.js +++ b/src/ui/menus/Menu.js @@ -1,28 +1,31 @@ /** - * LayoutBase - * A Mithril component Base class for Layouts, e.g. HorizontalLayout and - * CircosLayout. - */ + * LayoutBase + * A Mithril component Base class for Layouts, e.g. HorizontalLayout and + * CircosLayout. + */ import {Bounds} from '../../model/Bounds'; -export class Menu { - +export class Menu { // constructor() - prefer do not use in mithril components /** * mithril lifecycle callback + * @param vnode */ + oninit(vnode) { this.appState = vnode.attrs.appState; } /** * mithril lifecycle method + * @param vnode */ + oncreate(vnode) { // save a reference to this component's dom element - + this.el = vnode.dom; vnode.dom.mithrilComponent = this; this.bounds = new Bounds(vnode.dom.getBoundingClientRect()); @@ -30,7 +33,9 @@ export class Menu { /** * mithril lifecycle method + * @param vnode */ + onupdate(vnode) { this.bounds = new Bounds(vnode.dom.getBoundingClientRect()); } diff --git a/src/ui/menus/Popover.js b/src/ui/menus/Popover.js index 75e63f72..db6c97f6 100644 --- a/src/ui/menus/Popover.js +++ b/src/ui/menus/Popover.js @@ -1,110 +1,148 @@ /** - * popover - * A mithril component for displaying feature information. - */ + * popover + * A mithril component for displaying feature information. + */ import m from 'mithril'; import {mix} from '../../../mixwith.js/src/mixwith'; import {Menu} from './Menu'; import {RegisterComponentMixin} from '../RegisterComponentMixin'; -export class Popover extends mix(Menu).with(RegisterComponentMixin){ +export class Popover extends mix(Menu).with(RegisterComponentMixin) { - oninit(vnode){ + /** + * + * @param vnode + */ + + oninit(vnode) { super.oninit(vnode); } - + /** * mithril component render method + * @param vnode + * @returns {*} */ + view(vnode) { let b = vnode.attrs.domBounds || {}; - let info = vnode.attrs.info || {data:[]}; + let info = vnode.attrs.info || {data: []}; return m('div', { - class: 'biomap-info', - style: `left: ${info.left+b.left}px; top: ${info.top+b.top}px; + class: 'biomap-info', + style: `left: ${info.left + b.left}px; top: ${info.top + b.top}px; display: ${info.display};`, - },this._generateInner(info.data)); + }, this._generateInner(info.data)); } + /** + * + * @param data + * @returns {*} + * @private + */ - _generateInner(data){ - if(!data) return; + _generateInner(data) { + if (!data) return; let popover = data.map(item => { - let start = m('div', 'start: '+ item.model.coordinates.start); - let stop = m('div', 'stop: '+ item.model.coordinates.stop); - let tags = item.model.tags.length > 0 && typeof item.model.tags[0] != 'undefined' ? m('div','tags: ',item.model.tags.join('\n')) : []; - let aliases = item.model.aliases.length > 0 && typeof item.model.aliases[0] != 'undefined' ? m('div','aliases: ',item.model.aliases.join('\n')) : []; - let links = item.model.source.linkouts.length > 0 ? - m('div', {id:`links-div-${item.model.name}`}, - item.model.source.linkouts.filter(l => (! l.isLinkingService) && item.model.typeLinkedBy(l) ).map( - l => {return m('div', {}, m('a', {'target' : '_blank', 'href' : l.url.replace(/\${item.id}/, item.model.name)}, l.text));} - ).concat( - item.model.source.linkouts.some(l => {return l.isLinkingService && item.model.typeHasLinkouts;}) ? - (item.model.links == undefined ? m('img[src=images/ajax-loader.gif]') : item.model.links.map(l => {return m('div',{}, m('a', {target:'_blank', href:l.href}, l.text));})) - : [] - ) - ) : []; - - return [m(this._buttonTest(item.model),{targetId:item.model.name}), - m('div',{class:'biomap-info-data', id:`biomap-info-${item.model.name}`, style: 'display: none;'},[start,stop,tags, aliases, links]) + let start = m('div', 'start: ' + item.model.coordinates.start); + let stop = m('div', 'stop: ' + item.model.coordinates.stop); + let tags = item.model.tags.length > 0 && typeof item.model.tags[0] !== 'undefined' ? m('div', 'tags: ', item.model.tags.join('\n')) : []; + let aliases = item.model.aliases.length > 0 && typeof item.model.aliases[0] !== 'undefined' ? m('div', 'aliases: ', item.model.aliases.join('\n')) : []; + let links = item.model.source.linkouts.length > 0 ? + m('div', {id: `links-div-${item.model.name}`}, + item.model.source.linkouts.filter(l => (!l.isLinkingService) && item.model.typeLinkedBy(l)).map( + l => { + return m('div', {}, m('a', { + 'target': '_blank', + 'href': l.url.replace(/\${item.id}/, item.model.name) + }, l.text)); + } + ).concat( + item.model.source.linkouts.some(l => { + return l.isLinkingService && item.model.typeHasLinkouts; + }) ? + (item.model.links === undefined ? m('img[src=images/ajax-loader.gif]') : item.model.links.map(l => { + return m('div', {}, m('a', {target: '_blank', href: l.href}, l.text)); + })) + : [] + ) + ) : []; + + return [m(this._buttonTest(item.model), {targetId: item.model.name}), + m('div', { + class: 'biomap-info-data', + id: `biomap-info-${item.model.name}`, + style: 'display: none;' + }, [start, stop, tags, aliases, links]) ]; }); - - return m('div',{},popover); + + return m('div', {}, popover); } - _buttonTest(feature){ - var Links = { - fetch: function() { - var url; - return feature.source.linkouts.filter(l => l.isLinkingService && feature.tags.includes(l.featuretype)).map(l => { - url = l.url; - url = url.replace(/\${item\.id}/, feature.name); - return m.request({ - method: 'GET', - url: url, - }) - .then(function(result) { - feature.links = result; - }); - }); - } - }; + /** + * + * @param feature + * @returns {{view: view}} + * @private + */ + _buttonTest(feature) { + let Links = { + fetch: function () { + let url; + return feature.source.linkouts.filter(l => l.isLinkingService && feature.tags.includes(l.featuretype)).map(l => { + url = l.url; + url = url.replace(/\${item\.id}/, feature.name); + return m.request({ + method: 'GET', + url: url, + }) + .then(function (result) { + feature.links = result; + }); + }); + } + }; - return{ - view: function(vnode){ + return { + view: function (vnode) { let targetName = `biomap-info-${vnode.attrs.targetId}`; - return m('div', { - class:'biomap-info-name', - onclick: function() { - let target = document.getElementById(targetName); - target.style.display = target.style.display == 'none' ? 'block' : 'none'; - if (feature.links == undefined) { - if (feature.source.linkouts.some(l => {return l.isLinkingService && feature.typeHasLinkouts;})) { - let p = Links.fetch(); - if (p != undefined) { - p[0].then(vnode.redraw); - } - } - else { - feature.links = []; - vnode.redraw; + return m('div', { + class: 'biomap-info-name', + onclick: function () { + let target = document.getElementById(targetName); + target.style.display = target.style.display === 'none' ? 'block' : 'none'; + if (feature.links === undefined) { + if (feature.source.linkouts.some(l => { + return l.isLinkingService && feature.typeHasLinkouts; + })) { + let p = Links.fetch(); + if (p !== undefined) { + p[0].then(vnode.redraw); } } + else { + feature.links = []; + vnode.redraw(); + } } - }, vnode.attrs.targetId); + } + }, vnode.attrs.targetId); } }; } + /** + * + * @returns {boolean} + */ - - - handleGesture(){ - // prevent interacting with div from propegating events - return true; - } + handleGesture() { + // prevent interacting with div from propagating events + console.log('popover gesture!'); + return true; + } } diff --git a/src/ui/tools/AddMapButton.js b/src/ui/tools/AddMapButton.js index 68bf9f58..f0522040 100644 --- a/src/ui/tools/AddMapButton.js +++ b/src/ui/tools/AddMapButton.js @@ -8,8 +8,11 @@ export class AddMapButton { // constructor() - prefer do not use in mithril components /** - * mithril render callback - */ + * mithril render callback + * @param vnode + * @return {*} + */ + view(vnode) { const attrs = { onclick: vnode.attrs.onclick diff --git a/src/ui/tools/ConfigurationButton.js b/src/ui/tools/ConfigurationButton.js new file mode 100644 index 00000000..055d3fd9 --- /dev/null +++ b/src/ui/tools/ConfigurationButton.js @@ -0,0 +1,24 @@ +/** + * A mithril component of Add Map button + */ +import m from 'mithril'; + +export class ConfigurationButton { + + // constructor() - prefer do not use in mithril components + + /** + * mithril render callback + * @param vnode + * @returns {*} + */ + view(vnode) { + const attrs = { + onclick: vnode.attrs.onclick + }; + return m('button', attrs, [ + m('i.material-icons', 'mode_edit'), + 'Configuration' + ]); + } +} diff --git a/src/ui/tools/ConfigurationDialog.js b/src/ui/tools/ConfigurationDialog.js new file mode 100644 index 00000000..c3df926a --- /dev/null +++ b/src/ui/tools/ConfigurationDialog.js @@ -0,0 +1,131 @@ +/** + * A mithril component for configuration import/export + */ +import m from 'mithril'; +import {featureUpdate} from '../../topics'; +import PubSub from 'pubsub-js'; + +export class ConfigurationDialog { + + // constructor() - prefer do not use in mithril components + + /** + * mithril lifecycle method + */ + /** + * + * @param vnode + */ + + oninit(vnode) { + this.model = vnode.attrs.model; + let cd = {}; + this.model.bioMaps.forEach(bioMap => { + cd[bioMap.name] = { + config: bioMap.config, + name: bioMap.name, + qtlGroups: bioMap.qtlGroups, + source: bioMap.source.id + }; + }); + + ConfigData.base = JSON.stringify(cd, null, 2); + ConfigData.updated = JSON.stringify(cd, null, 2); + + this.onDismiss = vnode.attrs.onDismiss; + this.selection = null; + } + + /** + * event handler for cancel button. + * @param evt + * @private + */ + + _onCancel(evt) { + evt.preventDefault(); + this.onDismiss(evt); + } + + /** + * event handler for use new configuration button + * @param evt + * @private + */ + + _onUpdated(evt) { + let newConfig = JSON.parse(ConfigData.updated); + let finalConfig = []; + this.model.allMaps.forEach(map => { + for (let name in newConfig) { + if (newConfig.hasOwnProperty(name) && name === map.name && newConfig[name].source === map.source.id) { + console.log() + let item = map; + item.config = newConfig[name].config; + if(newConfig[name].config.tracks) { + let tracks = JSON.parse(JSON.stringify(newConfig[name].config.tracks)); + delete newConfig[name].config.tracks; + item.tracks = tracks; + } + finalConfig.push(item); + } + } + }); + this.model.bioMaps = finalConfig; + for (let i = 0; i < finalConfig.length; i++) { + PubSub.publish(featureUpdate, {mapIndex: i}); + } + this.onDismiss(evt); + } + + /** + * mithril component render callback. + * @returns {*} + */ + + view() { + //const allMaps = this.model.allMaps || []; + return m('div.cmap-map-addition-dialog', [ + m('h5', 'Configuration Details'), + m('form', [ + m('textarea', { + style: 'width:50%;height:600%', + value: ConfigData.updated, + onchange: function (e) { + e.preventDefault(); + ConfigData.updated = String(e.currentTarget.value); + } + } + ) + ]), + m('button', { + class: 'button', + onclick: evt => this._onUpdated(evt) + }, [ + m('i.material-icons', 'mode_edit'), + 'Use new configuration' + ] + ), + m('button.button', {onclick: evt => this._onCancel(evt)}, [ + m('i.material-icons', 'cancel'), + 'Cancel' + ]) + ]); + } +} + +/** + * + * @type {{base: string, updated: string, setBase: ConfigData.setBase, setUpdated: ConfigData.setUpdated}} + */ + +let ConfigData = { + base: '', + updated: '', + setBase: function (value) { + ConfigData.base = value; + }, + setUpdated: function (value) { + ConfigData.updated = value; + } +}; diff --git a/src/ui/tools/FilterButton.js b/src/ui/tools/FilterButton.js index 7b24f410..755dc178 100644 --- a/src/ui/tools/FilterButton.js +++ b/src/ui/tools/FilterButton.js @@ -3,26 +3,28 @@ */ import m from 'mithril'; -export class FilterButton { +export class FilterButton { // constructor() - prefer do not use in mithril components /** - * mithril render callback - */ + * mithril render callback + * @returns {*} + */ + view() { const attrs = { onclick: evt => this._onClick(evt) }; - return m('button', attrs , [ + return m('button', attrs, [ m('i.material-icons', 'filter_list'), 'Filter' ]); } /** - * button event handler - */ + * button event handler + */ _onClick() { } } diff --git a/src/ui/tools/LayoutPicker.js b/src/ui/tools/LayoutPicker.js index cef95cd5..c56241bd 100644 --- a/src/ui/tools/LayoutPicker.js +++ b/src/ui/tools/LayoutPicker.js @@ -9,57 +9,62 @@ import {layout} from '../../topics'; import {HorizontalLayout} from '../../ui/layout/HorizontalLayout'; import {CircosLayout} from '../../ui/layout/CircosLayout'; - -export class LayoutPicker { +export class LayoutPicker { // constructor() - prefer do not use in mithril components /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { this.appState = vnode.attrs.appState; } /** * mithril component render method + * @returns {*} */ + view() { return m('fieldset', [ - m('legend', 'layout:'), - m('label', { for: 'horizontal-radio'}, [ - m('input', { - type: 'radio', - name: 'layout', - value: HorizontalLayout, - id: 'horizontal-radio', - checked: this.appState.tools.layout === HorizontalLayout, - onchange: e => this.onchange(e) - }), - 'horizontal' - ]), - m('label', { for: 'circos-radio'}, [ - m('input', { - type: 'radio', - name: 'layout', - value: CircosLayout, - id: 'circos-radio', - checked: this.appState.tools.layout === CircosLayout, - onchange: e => this.onchange(e) - }), - 'circos' - ]) - ] + m('legend', 'layout:'), + m('label', {for: 'horizontal-radio'}, [ + m('input', { + type: 'radio', + name: 'layout', + value: HorizontalLayout, + id: 'horizontal-radio', + checked: this.appState.tools.layout === HorizontalLayout, + onchange: e => this.onchange(e) + }), + 'horizontal' + ]), + m('label', {for: 'circos-radio'}, [ + m('input', { + type: 'radio', + name: 'layout', + value: CircosLayout, + id: 'circos-radio', + checked: this.appState.tools.layout === CircosLayout, + onchange: e => this.onchange(e) + }), + 'circos' + ]) + ] ); } /** * mithril event handler + * @param e */ + onchange(e) { let l = e.target.value; this.appState.layout = l; e.redraw = false; - PubSub.publish(layout, { evt: e, layout: l }); + PubSub.publish(layout, {evt: e, layout: l}); } } diff --git a/src/ui/tools/MapAdditionDialog.js b/src/ui/tools/MapAdditionDialog.js index 21bd81d0..bcb739d7 100644 --- a/src/ui/tools/MapAdditionDialog.js +++ b/src/ui/tools/MapAdditionDialog.js @@ -9,7 +9,9 @@ export class MapAdditionDialog { /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { this.model = vnode.attrs.model; this.onDismiss = vnode.attrs.onDismiss; @@ -18,7 +20,10 @@ export class MapAdditionDialog { /** * event handler for cancel button. + * @param evt + * @private */ + _onCancel(evt) { evt.preventDefault(); this.onDismiss(evt); @@ -26,7 +31,10 @@ export class MapAdditionDialog { /** * event handler for add-on-right button + * @param evt + * @private */ + _onAddRight(evt) { const i = this.model.bioMaps.length; this.model.addMap(this.selection, i); @@ -36,7 +44,10 @@ export class MapAdditionDialog { /** * event handler for add-on-left button + * @param evt + * @private */ + _onAddLeft(evt) { this.model.addMap(this.selection, 0); evt.preventDefault(); @@ -45,15 +56,21 @@ export class MapAdditionDialog { /** * event handler for radio button change. + * @param evt + * @param map + * @private */ + _onSelection(evt, map) { evt.preventDefault(); this.selection = map; } /** - * mithril component render callback. - */ + * mithril component render callback. + * @returns {*} + */ + view() { const allMaps = this.model.allMaps || []; return m('div.cmap-map-addition-dialog', [ @@ -61,16 +78,16 @@ export class MapAdditionDialog { m('form', [ m('table.u-full-width', [ m('thead', - m('tr', [ m('th', 'Data Source'), m('th', 'Available Maps') ]) + m('tr', [m('th', 'Data Source'), m('th', 'Available Maps')]) ), m('tbody', - this.model.sources.map( source => { + this.model.sources.map(source => { return m('tr', [ m('td', source.id), - m('td', allMaps.filter( map => { - return (map.source === source && - this.model.bioMaps.indexOf(map) === -1); - }).map( map => { + m('td', allMaps.filter(map => { + return (map.source === source && + this.model.bioMaps.indexOf(map) === -1); + }).map(map => { return m('label', [ m('input[type="radio"]', { name: `maps4${source.id}`, @@ -88,7 +105,7 @@ export class MapAdditionDialog { ]) ]), m('button', { - disabled: this.selection ? false : true, + disabled: !this.selection, class: this.selection ? 'button-primary' : 'button', onclick: evt => this._onAddLeft(evt) }, [ @@ -96,8 +113,8 @@ export class MapAdditionDialog { 'Add Map On Left' ] ), - m('button.button', { - disabled: this.selection ? false : true, + m('button.button', { + disabled: !this.selection, class: this.selection ? 'button-primary' : 'button', onclick: evt => this._onAddRight(evt) }, [ @@ -105,7 +122,7 @@ export class MapAdditionDialog { 'Add Map On Right' ] ), - m('button.button', { onclick: evt => this._onCancel(evt) }, [ + m('button.button', {onclick: evt => this._onCancel(evt)}, [ m('i.material-icons', 'cancel'), 'Cancel' ]) diff --git a/src/ui/tools/MapRemovalDialog.js b/src/ui/tools/MapRemovalDialog.js index c5a8ebc4..21521843 100644 --- a/src/ui/tools/MapRemovalDialog.js +++ b/src/ui/tools/MapRemovalDialog.js @@ -11,7 +11,9 @@ export class MapRemovalDialog { /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { this.model = vnode.attrs.model; this.onDismiss = vnode.attrs.onDismiss; @@ -20,20 +22,25 @@ export class MapRemovalDialog { /** * event handler for cancel button + * @param evt + * @private */ - onCancel(evt) { + + _onCancel(evt) { evt.preventDefault(); this.onDismiss(evt); } /** * event handler for remove button + * @param evt + * @private */ - onRemove(evt) { - const filtered = this.model.bioMaps.filter( bioMap => { + + _onRemove(evt) { + this.model.bioMaps = this.model.bioMaps.filter(bioMap => { return this.selection.indexOf(bioMap) === -1; }); - this.model.bioMaps = filtered; PubSub.publish(mapRemoved, this.selection); evt.preventDefault(); this.onDismiss(evt); @@ -41,10 +48,13 @@ export class MapRemovalDialog { /** * event handler for checkbox + * @param bioMap + * @private */ - onToggleSelection(bioMap) { + + _onToggleSelection(bioMap) { const i = this.selection.indexOf(bioMap); - if(i === -1) { + if (i === -1) { this.selection.push(bioMap); } else { @@ -53,33 +63,35 @@ export class MapRemovalDialog { } /** - * mithril render callback - */ + * mithril render callback + * @returns {*} + */ + view() { const haveSelection = this.selection.length > 0; const plural = this.selection.length > 1; return m('div.cmap-map-removal-dialog', [ m('h5', plural ? 'Remove Maps' : 'Remove Map'), m('form', [ - this.model.bioMaps.map( bioMap => { + this.model.bioMaps.map(bioMap => { return m('label.cmap-map-name', [ m('input[type="checkbox"]', { checked: this.selection.indexOf(bioMap) !== -1, - onclick: () => this.onToggleSelection(bioMap) + onclick: () => this._onToggleSelection(bioMap) }), - m('span.label-body', bioMap.uniqueName ) + m('span.label-body', bioMap.uniqueName) ]); }), m('button', { class: haveSelection ? 'button-primary' : 'button', - disabled: ! haveSelection, + disabled: !haveSelection, autocomplete: 'off', // firefox workaround for disabled state - onclick: evt => this.onRemove(evt) + onclick: evt => this._onRemove(evt) }, [ m('i.material-icons', 'remove_circle_outline'), 'Remove Selected' ]), - m('button.button', { onclick: evt => this.onCancel(evt) }, [ + m('button.button', {onclick: evt => this._onCancel(evt)}, [ m('i.material-icons', 'cancel'), 'Cancel' ]) diff --git a/src/ui/tools/RemoveMapButton.js b/src/ui/tools/RemoveMapButton.js index ccb99b07..cfdea54b 100644 --- a/src/ui/tools/RemoveMapButton.js +++ b/src/ui/tools/RemoveMapButton.js @@ -3,13 +3,16 @@ */ import m from 'mithril'; -export class RemoveMapButton { +export class RemoveMapButton { // constructor() - prefer do not use in mithril components /** - * mithril render callback - */ + * mithril render callback + * @param vnode + * @returns {*} + */ + view(vnode) { const attrs = { onclick: vnode.attrs.onclick diff --git a/src/ui/tools/ResetButton.js b/src/ui/tools/ResetButton.js index fab035a1..7fa9f41d 100644 --- a/src/ui/tools/ResetButton.js +++ b/src/ui/tools/ResetButton.js @@ -12,7 +12,9 @@ export class ResetButton { /** * mithril render callback + * @returns {*} */ + view() { return m('button', { onclick: evt => this._onClick(evt) @@ -24,7 +26,10 @@ export class ResetButton { /** * reset button event handler + * @param evt + * @private */ + _onClick(evt) { PubSub.publish(reset, null); // subscribers to the reset topic may m.redraw if they need to; suppress diff --git a/src/ui/tools/Tools.js b/src/ui/tools/Tools.js index edec5443..9542ba51 100644 --- a/src/ui/tools/Tools.js +++ b/src/ui/tools/Tools.js @@ -1,23 +1,28 @@ /** - * A mithril component of the UI tools in a div (toolbar). - */ + * A mithril component of the UI tools in a div (toolbar). + */ import m from 'mithril'; import {ResetButton} from './ResetButton'; import {RemoveMapButton} from './RemoveMapButton'; import {AddMapButton} from './AddMapButton'; +import {ConfigurationButton} from './ConfigurationButton'; +import {UploadButton} from './UploadButton'; //import {FilterButton} from './FilterButton'; import {MapRemovalDialog} from './MapRemovalDialog'; import {MapAdditionDialog} from './MapAdditionDialog'; +import {ConfigurationDialog} from './ConfigurationDialog'; +import {UploadDialog} from './UploadDialog'; - -export class Tools { +export class Tools { // constructor() - prefer do not use in mithril components /** * mithril lifecycle method + * @param vnode */ + oninit(vnode) { this.appState = vnode.attrs.appState; this.currentDialog = vnode.attrs.dialog; @@ -25,7 +30,9 @@ export class Tools { /** * mithril component render method + * @returns {*} */ + view() { return m('div.cmap-tools', [ m('div.cmap-toolbar.cmap-hbox', [ @@ -36,6 +43,12 @@ export class Tools { }), m(RemoveMapButton, { onclick: () => this.currentDialog = MapRemovalDialog + }), + m(ConfigurationButton, { + onclick: () => this.currentDialog = ConfigurationDialog + }), + m(UploadButton, { + onclick: () => this.currentDialog = UploadDialog }) ]), this.currentDialog && m(this.currentDialog, { diff --git a/src/ui/tools/UploadButton.js b/src/ui/tools/UploadButton.js new file mode 100644 index 00000000..bcfabca9 --- /dev/null +++ b/src/ui/tools/UploadButton.js @@ -0,0 +1,25 @@ +/** + * A mithril component of Add Map button + */ +import m from 'mithril'; + +export class UploadButton { + + // constructor() - prefer do not use in mithril components + + /** + * mithril render callback + * @param vnode + * @returns {*} + */ + + view(vnode) { + const attrs = { + onclick: vnode.attrs.onclick + }; + return m('button', attrs, [ + m('i.material-icons', 'input'), + 'Add Data' + ]); + } +} diff --git a/src/ui/tools/UploadDialog.js b/src/ui/tools/UploadDialog.js new file mode 100644 index 00000000..40344cbf --- /dev/null +++ b/src/ui/tools/UploadDialog.js @@ -0,0 +1,212 @@ +/** + * A mithril component for map removal dialog + */ +import m from 'mithril'; +import {DataSourceModel} from '../../model/DataSourceModel'; + +export class UploadDialog { + + // constructor() - prefer do not use in mithril components + + /** + * mithril lifecycle method + * @param vnode + */ + + oninit(vnode) { + this.model = vnode.attrs.model; + this.onDismiss = vnode.attrs.onDismiss; + UploadData.new = false; + UploadData.setName(''); + UploadData.file = ''; + this.selection = null; + } + + /** + * event handler for cancel button. + * @param evt + * @private + */ + + _onCancel(evt) { + evt.preventDefault(); + this.onDismiss(evt); + } + + /** + * event handler for add-on-right button + * @param evt + * @private + */ + + _onAddData(evt) { + let sources = []; + if (!this.selection) { + this.selection = { + id: UploadData.newName, + filters: [], + linkouts: [], + method: 'GET', + url: UploadData.file !== '' ? UploadData.file : UploadData.loc, + config: {}, + parseResult: {data: []} + }; + } + + const oURL = this.selection.url; + this.selection.url = UploadData.file !== '' ? UploadData.file : UploadData.loc; + let cfg = [this.selection]; + + let promises = cfg.map(cfg => { + let dsm = new DataSourceModel(cfg); + sources.push(dsm); + return dsm.load(); + }); + + Promise.all(promises).then(() => { + sources.forEach(src => { + // change names to indicate uploaded data + if (UploadData.new) { + this.model.sources.push(src); + } + src.parseResult.data.forEach(data => data.feature_type_acc = 'Uploaded_' + data.feature_type_acc); + // update parseResults and all maps to reflect new data + this.selection.parseResult.data = this.selection.parseResult.data.concat(src.parseResult.data); + this.model.allMaps = this.model.sources.map(src => Object.values(src.bioMaps)).concatAll(); + // update active view models to show new data + this.model.bioMaps.forEach(activeMap => { + this.model.allMaps.filter(map => { + return ((map.name === activeMap.name && + activeMap.source.id === map.source.id)); + }).forEach(match => { + activeMap.features = match.features; + activeMap.tags = match.tags; + }); + }); + }); + this.selection.url = oURL; + }).catch(err => { + const msg = `While loading data source, ${err}`; + console.error(msg); + console.trace(); + alert(msg); + }); + + evt.preventDefault(); + this.onDismiss(evt); + } + + /** + * event handler for radio button change. + * @param evt + * @param map + * @private + */ + + _onSelection(evt, map) { + evt.preventDefault(); + this.selection = map; + UploadData.toggleNew(this.selection); + } + + /** + * mithril component render callback. + * @returns {*} + */ + + view() { + //const allMaps = this.model.allMaps || []; + return m('div.cmap-map-addition-dialog', [ + m('h5', 'Add Map'), + m('p', 'Currently only one file may be added at a time. If both a URL and a local file are provided, preference will be given to the local file.'), + m('form', [ + m('table.u-full-width', [ + m('thead', [ + m('tr', [m('th', 'URL'), m('th', m('input[type=text]', { + oninput: m.withAttr('value', UploadData.setLoc), + value: UploadData.loc, + style: 'width:60%;' + }))]) + , m('tr', [m('th', 'Local File'), m('th', m('input[type=file]', { + onchange: m.withAttr('files', UploadData.setFile), + file: UploadData.files + }))])] + ), + m('tbody', + m('tr', [ + m('td', 'Target Map Set'), + m('td', [ + m('label', [ + m('input[type="radio"]', { + name: 'maps4new', + checked: UploadData.new, + value: 'newMap', + onchange: (evt) => this._onSelection(evt, null) + }), m('input[type=text]', { + oninput: m.withAttr('value', UploadData.setName), + value: UploadData.newName + }) + ]) + ].concat(this.model.sources.map(map => { + return m('label', [ + m('input[type="radio"]', { + name: `maps4${map.id}`, + checked: this.selection === map, + value: map.id, + onchange: (evt) => this._onSelection(evt, map) + }), + m('span[class="label-body"]', map.id) + ]); + }) + ) + ) + ]) + ) + ]) + ]), + m('button', { + //disabled unless a selection is made, or a new set is selected *and* there is a location or file state) + disabled: !((this.selection || UploadData.new) && (UploadData.loc !== '' || UploadData.file !== '')), + class: this.selection || UploadData.new ? 'button-primary' : 'button', + onclick: evt => this._onAddData(evt) + }, [ + m('i.material-icons', 'input'), + 'Add Data to Map' + ] + ), + m('button.button', {onclick: evt => this._onCancel(evt)}, [ + m('i.material-icons', 'cancel'), + 'Cancel' + ]) + ]); + } +} + +/** + * + * @type {{loc: string, file: string, newName: string, new: boolean, setLoc: UploadData.setLoc, setName: UploadData.setName, setFile: UploadData.setFile, toggleNew: UploadData.toggleNew}} + */ + +let UploadData = { + loc: '', + file: '', + newName: '', + new: false, + setLoc: function (value) { + UploadData.loc = value; + }, + setName: function (value) { + UploadData.newName = value; + }, + setFile: function (files) { + let reader = new FileReader(); + reader.onload = function (e) { + UploadData.file = e.target.result; + }; + reader.readAsDataURL(files[0]); + }, + toggleNew: function (selection) { + UploadData.new = !selection; + } +}; + diff --git a/src/util/CanvasUtil.js b/src/util/CanvasUtil.js index 08c6c940..c04443bd 100644 --- a/src/util/CanvasUtil.js +++ b/src/util/CanvasUtil.js @@ -1,24 +1,46 @@ /** - * Helper functions for calculating canvas points. + * @file Helper functions for calculating canvas points. */ -// Takes a point on a map and translates it pixel coordinates on the current canvas -export function translateScale(point, baseScale, newScale){ - return ((baseScale.stop - baseScale.start)*(point-newScale.start)/(newScale.stop-newScale.start)+baseScale.start) - baseScale.start; +/** + * Takes a point on a map and translates it from the newScale to the baseScale scale + * @param point - Map point in terms of new scale + * @param baseScale - largest and smallest possible values of the scale + * @param newScale - largest and smallest values of the adjusted scale + * @param {boolean} invert - is the scale to be drawn "flipped" + * @returns {number} point converted from location on new scale to location on base scale + */ + +export function translateScale(point, baseScale, newScale, invert) { + let loc = ((baseScale.stop - baseScale.start) * (point - newScale.start) / (newScale.stop - newScale.start) + baseScale.start) - baseScale.start; + if (invert) { + loc = (baseScale.start + baseScale.stop) - loc; + } + return loc; } -// takes an event and translates the event coordinates to canvas coordinates -export function pageToCanvas(evt, canvas){ - function getOffset( el ) { - var _x = 0; - var _y = 0; - while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) { +/** + * Takes an event and translates the event coordinates to canvas coordinates + * here because webkit events vs mozilla events vs ie events don't all provide + * the same data + * + * @param evt - dom event + * @param canvas - target canvas + * @returns {{x: number, y: number}} location translated from page event coordinates to canvas coordinates. + */ + +export function pageToCanvas(evt, canvas) { + function getOffset(el) { + let _x = 0; + let _y = 0; + while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { _x += el.offsetLeft - el.scrollLeft; _y += el.offsetTop - el.scrollTop; el = el.offsetParent; } - return { top: _y, left: _x }; + return {top: _y, left: _x}; } + let pageOffset = getOffset(canvas); return { 'x': evt.srcEvent.pageX - pageOffset.left, diff --git a/src/util/concatAll.js b/src/util/concatAll.js index 1beac5bd..6d22cbd0 100644 --- a/src/util/concatAll.js +++ b/src/util/concatAll.js @@ -1,9 +1,11 @@ /** - * concatAll() aka flattenDeep(), based on http://reactivex.io/learnrx/ + * @description concatAll() aka flattenDeep(), based on http://reactivex.io/learnrx/ + * @return {array} concatenated array. */ -Array.prototype.concatAll = function() { - var results = []; - this.forEach(function(subArray) { + +Array.prototype.concatAll = function () { + let results = []; + this.forEach(function (subArray) { results.push.apply(results, subArray); }); return results; diff --git a/src/util/isNil.js b/src/util/isNil.js index 3e30ed44..6733075c 100644 --- a/src/util/isNil.js +++ b/src/util/isNil.js @@ -1,6 +1,7 @@ /** - * Helper function for detecting null or undefined. + * @description Helper function for detecting null or undefined. */ + const isNil = (o) => o === null || o === undefined; export {isNil}; diff --git a/test/canvas/geometry/ruler.test.js b/test/canvas/geometry/ruler.test.js index 709ca773..8095f27e 100644 --- a/test/canvas/geometry/ruler.test.js +++ b/test/canvas/geometry/ruler.test.js @@ -2,9 +2,9 @@ import {expect} from 'chai'; import {Bounds} from '../../../src/model/Bounds'; import {Ruler} from '../../../src/canvas/geometry/Ruler'; -describe('Ruler test', function() { +describe('Ruler test', function () { - it('constructor works', function() { + it('constructor works', function () { let bounds = new Bounds({ top: 1, bottom: 11, @@ -14,13 +14,18 @@ describe('Ruler test', function() { height: 10 }); let parent = {}; - let model = { - view:{ - base:{ + let bioMap = { + config: { + rulerColor: 'blue', + rulerWidth: 10, + rulerSpacing: 5 + }, + view: { + base: { start: 0, stop: 100 }, - visible:{ + visible: { start: 0, stop: 100 }, @@ -28,23 +33,23 @@ describe('Ruler test', function() { pixelScaleFactor: 1 } }; - parent.bounds = new Bounds({top:0,left:0,width:20,height:20}); + parent.bounds = new Bounds({top: 0, left: 0, width: 20, height: 20}); parent.backbone = {}; parent.backbone.bounds = bounds; - let ruler = new Ruler({parent,bioMap:model}); + let ruler = new Ruler({parent, bioMap: bioMap}); let rulerBounds = new Bounds({ top: parent.bounds.top, - left: bounds.left -15, + left: bounds.left - 15, width: 10, height: bounds.height, allowSubpixel: false }); expect(ruler.parent).to.equal(parent); - expect(ruler.mapCoordinates).to.equal(model.view); - expect(ruler.pixelScaleFactor).to.equal(model.view.pixelScaleFactor); + expect(ruler.mapCoordinates).to.equal(bioMap.view); + expect(ruler.pixelScaleFactor).to.equal(bioMap.view.pixelScaleFactor); expect(ruler.bounds).to.eql(rulerBounds); }); - it('get visible', function() { + it('get visible', function () { let bounds = new Bounds({ top: 1, bottom: 11, @@ -55,12 +60,17 @@ describe('Ruler test', function() { }); let parent = {}; let model = { - view:{ - base:{ + config: { + rulerColor: 'blue', + rulerWidth: 10, + rulerSpacing: 5 + }, + view: { + base: { start: 0, stop: 100 }, - visible:{ + visible: { start: 0, stop: 100 }, @@ -68,20 +78,19 @@ describe('Ruler test', function() { pixelScaleFactor: 1 } }; - parent.bounds = new Bounds({top:0,left:0,width:20,height:20}); + parent.bounds = new Bounds({top: 0, left: 0, width: 20, height: 20}); parent.backbone = {}; parent.backbone.bounds = bounds; - let ruler = new Ruler({parent,bioMap:model}); + let ruler = new Ruler({parent, bioMap: model}); let rulerBounds = new Bounds({ top: parent.bounds.top, - left: bounds.left -15, + left: bounds.left - 15, width: 10, height: bounds.height, allowSubpixel: false }); - expect(ruler.visible).to.eql({data:ruler}); + expect(ruler.visible).to.eql({data: ruler}); }); - }); diff --git a/test/canvas/layout/BioMap.js b/test/canvas/layout/BioMap.js index 8deee96b..f0a49b5b 100644 --- a/test/canvas/layout/BioMap.js +++ b/test/canvas/layout/BioMap.js @@ -1,63 +1,71 @@ import {expect} from 'chai'; import mq from '../../ui/mithrilQuerySetup'; import {Bounds} from '../../../src/model/Bounds'; -import {BioMap} from '../../../src/canvas/layout/BioMap'; +import {BioMap} from '../../../src/canvas/canvas/BioMap'; -describe('BioMap test', function() { - describe('constructor', function() { - it('should construct a new node', function() { +describe('BioMap test', function () { + describe('constructor', function () { + it('should construct a new node', function () { let gestureRegex = { - pan: new RegExp('^pan'), - pinch: new RegExp('^pinch'), - tap: new RegExp('^tap'), - wheel: new RegExp('^wheel') - }; - let params = baseParams(); - // mock up test constructor to prevent _layout propegation blocking - // tests - let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - let node = new testBioMap(params); + pan: new RegExp('^pan'), + pinch: new RegExp('^pinch'), + tap: new RegExp('^tap'), + wheel: new RegExp('^wheel') + }; + let params = baseParams(); + // mock up test constructor to prevent _layout propegation blocking + // tests + let testBioMap = BioMap; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + let node = new testBioMap(params); - expect(node.model.visible).to.eql(params.bioMapModel.coordinates); - expect(node.model.view.base).to.eql(params.bioMapModel.coordinates); - expect(node.model.view.visible).to.eql(params.bioMapModel.coordinates); + expect(node.model.visible).to.eql(params.bioMapModel.coordinates); + expect(node.model.view.base).to.eql(params.bioMapModel.coordinates); + expect(node.model.view.visible).to.eql(params.bioMapModel.coordinates); expect(node.appState).to.equal(params.appState); expect(node.verticalScale).to.equal(0); - expect(node.backbone).to.equal(null); - expect(node.featureMarks).to.eql([]); - expect(node.featureLabels).to.eql([]); + expect(node.backbone).to.equal(null); + expect(node.featureMarks).to.eql([]); + expect(node.featureLabels).to.eql([]); expect(node._gestureRegex).to.eql(gestureRegex); }); }); - describe('custom getters', function() { - describe('get visible', function() { - it('should return visible noded if children have visible', function() { - let p1 = baseParams(); - let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - let node = new testBioMap(p1); - node.children[0] = {visible:"visibleTest"}; - expect(node.visible).to.eql(["visibleTest"]); - }); - it('should return nothing if no children are visible', function() { - let p1 = baseParams(); - let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - let node = new testBioMap(p1); - expect(node.visible).to.eql([]); - }); - }); - describe('get hitMap', function() { - it('should return rbush tree of visible objects', function() { - let p1 = baseParams(); - let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - let node = new testBioMap(p1); - expect(node.hitMap).to.eql(node.locMap); - }); - }); + describe('custom getters', function () { + describe('get visible', function () { + it('should return visible noded if children have visible', function () { + let p1 = baseParams(); + let testBioMap = BioMap; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + let node = new testBioMap(p1); + node.children[0] = {visible: 'visibleTest'}; + expect(node.visible).to.eql(['visibleTest']); + }); + it('should return nothing if no children are visible', function () { + let p1 = baseParams(); + let testBioMap = BioMap; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + let node = new testBioMap(p1); + expect(node.visible).to.eql([]); + }); + }); + describe('get hitMap', function () { + it('should return rbush tree of visible objects', function () { + let p1 = baseParams(); + let testBioMap = BioMap; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + let node = new testBioMap(p1); + expect(node.hitMap).to.eql(node.locMap); + }); + }); }); // // describe('mithril lifecycle events', function() { @@ -156,7 +164,7 @@ describe('BioMap test', function() { // }); // }); // - describe('private methods', function() { + describe('private methods', function () { // describe('_onZoom(evt)', function() { // it('should increase zoom on negative deltaY', function() { // let p1 = baseParams(); @@ -224,55 +232,72 @@ describe('BioMap test', function() { // }); // }); - describe('_onTap(evt)', function() { - it('should be able to calculate a tap event', function() { + describe('_onTap(evt)', function () { + it('should be able to calculate a tap event', function () { let p1 = baseParams(); let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - testBioMap.prototype._draw = function(layoutBounds){return true;}; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + testBioMap.prototype._draw = function (layoutBounds) { + return true; + }; let node = new testBioMap(p1); - let evt = {srcEvent:{ pageX: 0, pageY: 0}}; - //let node.canvas = {offsetLeft:0, scrollLeft:0,offsetTop:0,scrollTop:0,offsetParent:null}; - node.backbone = {loadLabelMap : function(){return true;} }; - document.getElementsByClassName = function(){return [];}; + let evt = {srcEvent: {pageX: 0, pageY: 0}}; + //let node.canvas = {offsetLeft:0, scrollLeft:0,offsetTop:0,scrollTop:0,offsetParent:null}; + node.backbone = { + loadLabelMap: function () { + return true; + } + }; + document.getElementsByClassName = function () { + return []; + }; let tap = node._onTap(evt); expect(tap).to.equal(true); }); - }); + }); - describe('_loadHitMap()', function() { - it('should load a new hit map', function() { + describe('_loadHitMap()', function () { + it('should load a new hit map', function () { let p1 = baseParams(); let testBioMap = BioMap; - testBioMap.prototype._layout = function(layoutBounds){return true;}; - testBioMap.prototype._draw = function(layoutBounds){return true;}; + testBioMap.prototype._layout = function (layoutBounds) { + return true; + }; + testBioMap.prototype._draw = function (layoutBounds) { + return true; + }; let node = new testBioMap(p1); - let hit = {minX:1,maxX:1,minY:1,maxY:1,data:'test'} - node.addChild({hitMap:hit}); - node._loadHitMap(); - //let node.canvas = {offsetLeft:0, scrollLeft:0,offsetTop:0,scrollTop:0,offsetParent:null}; + let hit = {minX: 1, maxX: 1, minY: 1, maxY: 1, data: 'test'}; + node.addChild({hitMap: hit}); + node._loadHitMap(); + //let node.canvas = {offsetLeft:0, scrollLeft:0,offsetTop:0,scrollTop:0,offsetParent:null}; expect(node.locMap.all()).to.eql([hit]); }); - }); - }); + }); + }); }); -const baseParams = function() { - let bounds = new Bounds({ +const baseParams = function () { + let bounds = new Bounds({ top: 1, - left: 10, + left: 10, width: 10, height: 10 }); return { bioMapModel: { - coordinates:{ - start: 1, - stop: 100 - } - }, + coordinates: { + start: 1, + stop: 100 + }, + config: { + rulerSteps: 100 + } + }, appState: {}, - layoutBounds: bounds + layoutBounds: bounds }; -} +}; diff --git a/test/canvas/node/sceneGraphNodeBase.test.js b/test/canvas/node/sceneGraphNodeBase.test.js index ccfb8c2d..1af569a3 100644 --- a/test/canvas/node/sceneGraphNodeBase.test.js +++ b/test/canvas/node/sceneGraphNodeBase.test.js @@ -2,9 +2,9 @@ import {expect} from 'chai'; import {Bounds} from '../../../src/model/Bounds'; import {SceneGraphNodeBase} from '../../../src/canvas/node/SceneGraphNodeBase'; -describe('SceneGraphNodeBase test', function() { - describe('constructor', function() { - it('should construct a new node', function() { +describe('SceneGraphNodeBase test', function () { + describe('constructor', function () { + it('should construct a new node', function () { let bounds = new Bounds({ top: 1, bottom: 11, @@ -28,8 +28,8 @@ describe('SceneGraphNodeBase test', function() { }); }); - describe('custom getters', function() { - it('should get children', function() { + describe('custom getters', function () { + it('should get children', function () { let p1 = baseParams('parent'); let p2 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); @@ -38,10 +38,10 @@ describe('SceneGraphNodeBase test', function() { expect(parentNode.children.length).to.equal(1); expect(parentNode.children[0]).to.equal(childNode); }); - it('should get bounds', function() { + it('should get bounds', function () { let p1 = baseParams('parent'); let parentNode = new SceneGraphNodeBase(p1); - let b1 = new Bounds ({ + let b1 = new Bounds({ top: 0, bottom: 1, left: 0, @@ -52,20 +52,20 @@ describe('SceneGraphNodeBase test', function() { parentNode._bounds = b1; expect(parentNode.bounds).to.equal(b1); }); - it('should get rotation', function() { + it('should get rotation', function () { let p1 = baseParams('parent'); let parentNode = new SceneGraphNodeBase(p1); parentNode._rotation = 90; expect(parentNode.rotation).to.equal(90); }); - it('should get tags', function() { + it('should get tags', function () { let p1 = baseParams('parent'); let parentNode = new SceneGraphNodeBase(p1); parentNode._tags[0] = 'testTag'; expect(parentNode.tags).to.eql(['testTag']); }); - describe('get globalBounds', function() { - it('should work with existing parent', function() { + describe('get globalBounds', function () { + it('should work with existing parent', function () { let parentNode = parentChildGenerator(); let childNode = parentNode.children[0]; parentNode.children.push(childNode); @@ -77,14 +77,14 @@ describe('SceneGraphNodeBase test', function() { expect(result.left).to.equal(childNode.bounds.left + parentNode.bounds.left); expect(result.right).to.equal(childNode.bounds.right + parentNode.bounds.left); }); - it('should work with no parent', function() { + it('should work with no parent', function () { let p1 = baseParams('testNode'); let parentNode = new SceneGraphNodeBase(p1); expect(parentNode.globalBounds).to.eql(parentNode.bounds); }); }); - it('should get visible from children', function() { + it('should get visible from children', function () { let parentNode = parentChildGenerator(); let childNode = parentNode.children[0]; let visNode = { @@ -99,7 +99,7 @@ describe('SceneGraphNodeBase test', function() { expect(parentNode.visible).to.eql(childNode.locMap.all()); }); - it('should get hitMap from children', function() { + it('should get hitMap from children', function () { let p1 = baseParams('testNode'); let parentNode = new SceneGraphNodeBase(p1); let visNode = { @@ -109,13 +109,13 @@ describe('SceneGraphNodeBase test', function() { maxY: 2, data: 'empty' }; - parentNode.addChild({hitMap:[visNode]}) + parentNode.addChild({hitMap: [visNode]}); expect(parentNode.hitMap).to.eql([visNode]); }); }); - describe('custom setters', function() { - it('should set children', function() { + describe('custom setters', function () { + it('should set children', function () { let p1 = baseParams('testNode'); let p2 = baseParams('childNode'); let parentNode = new SceneGraphNodeBase(p1); @@ -125,10 +125,10 @@ describe('SceneGraphNodeBase test', function() { expect(parentNode.children).to.eql([childNode]); }); - it('should set bounds', function() { + it('should set bounds', function () { let p1 = baseParams('testNode'); let parentNode = new SceneGraphNodeBase(p1); - let b1 = new Bounds ({ + let b1 = new Bounds({ top: 0, bottom: 1, left: 0, @@ -140,26 +140,26 @@ describe('SceneGraphNodeBase test', function() { parentNode.bounds = b1; expect(parentNode.bounds).to.equal(b1); }); - it('should set rotation', function() { + it('should set rotation', function () { let p1 = baseParams('parent'); let parentNode = new SceneGraphNodeBase(p1); expect(parentNode.rotation).to.equal(45); parentNode.rotation = 90; expect(parentNode.rotation).to.equal(90); }); - it('should set tags', function() { + it('should set tags', function () { let p1 = baseParams('parent'); let parentNode = new SceneGraphNodeBase(p1); expect(parentNode.tags).to.eql(['parent']); parentNode.tags[0] = 'testTag'; expect(parentNode.tags).to.eql(['testTag']); }); - + }); - describe('public methods', function() { - describe('translatePointToGlobal({x,y})', function() { - it('should translate given {x,y} point to global coordinates', function() { + describe('public methods', function () { + describe('translatePointToGlobal({x,y})', function () { + it('should translate given {x,y} point to global coordinates', function () { let parentNode = parentChildGenerator(); let childNode = parentNode.children[0]; let point = childNode.translatePointToGlobal({x: 3, y: 8}); @@ -167,19 +167,25 @@ describe('SceneGraphNodeBase test', function() { }); }); - describe('draw(ctx)', function() { - it('should not throw an error when invoked', function() { + describe('draw(ctx)', function () { + it('should not throw an error when invoked', function () { let p1 = baseParams('parent'); let p2 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); - let childNode = {draw: function() {return true;}} + let childNode = { + draw: function () { + return true; + } + }; parentNode.addChild(childNode); - expect(function() {parentNode.draw()}).to.not.throw(); + expect(function () { + parentNode.draw(); + }).to.not.throw(); }); }); - describe('removeChild(node)', function() { - it('should remove the passed node from its parent', function() { + describe('removeChild(node)', function () { + it('should remove the passed node from its parent', function () { let p1 = baseParams('parent'); let p2 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); @@ -193,60 +199,62 @@ describe('SceneGraphNodeBase test', function() { expect(childNode.parent).to.equal(null); }); - it('should not throw an error is passed node has no parent', function() { + it('should not throw an error is passed node has no parent', function () { let p2 = baseParams('child'); let childNode = new SceneGraphNodeBase(p2); - expect(function() {childNode.removeChild(childNode)}).to.not.throw(); + expect(function () { + childNode.removeChild(childNode); + }).to.not.throw(); expect(childNode.parent).to.equal(null); }); }); - describe('addChild(node))', function() { - it('should add new child to new parent node', function() { + describe('addChild(node))', function () { + it('should add new child to new parent node', function () { let p1 = baseParams('parent'); let p2 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); let childNode = new SceneGraphNodeBase(p2); - expect(parentNode.children.length).to.equal(0); - parentNode.addChild(childNode); - - expect(parentNode.children.length).to.equal(1); - expect(parentNode.children[0]).to.equal(childNode); - expect(childNode.parent).to.equal(parentNode); - }); + expect(parentNode.children.length).to.equal(0); + parentNode.addChild(childNode); - it('should not duplicate already existing children', function() { + expect(parentNode.children.length).to.equal(1); + expect(parentNode.children[0]).to.equal(childNode); + expect(childNode.parent).to.equal(parentNode); + }); + + it('should not duplicate already existing children', function () { let p1 = baseParams('parent'); let p2 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); let childNode = new SceneGraphNodeBase(p2); - expect(parentNode.children.length).to.equal(0); - parentNode.addChild(childNode); + expect(parentNode.children.length).to.equal(0); + parentNode.addChild(childNode); parentNode.addChild(childNode); - expect(parentNode.children.length).to.equal(1); - expect(parentNode.children[0]).to.equal(childNode); - expect(childNode.parent).to.equal(parentNode); - }); - - it('should transfer child node between two parent nodes', function() { + expect(parentNode.children.length).to.equal(1); + expect(parentNode.children[0]).to.equal(childNode); + expect(childNode.parent).to.equal(parentNode); + }); + + it('should transfer child node between two parent nodes', function () { let p1 = baseParams('parentOriginal'); let p2 = baseParams('parentNew'); let p3 = baseParams('child'); let parentNode = new SceneGraphNodeBase(p1); - let secondParent = new SceneGraphNodeBase(p2); + let secondParent = new SceneGraphNodeBase(p2); let childNode = new SceneGraphNodeBase(p3); - parentNode.addChild(childNode); - secondParent.addChild(childNode); - expect(parentNode.children.length).to.equal(0); - expect(secondParent.children.length).to.equal(1); - expect(secondParent.children[0]).to.equal(childNode); - expect(childNode.parent).to.equal(secondParent); - }); - }); + parentNode.addChild(childNode); + secondParent.addChild(childNode); + expect(parentNode.children.length).to.equal(0); + expect(secondParent.children.length).to.equal(1); + expect(secondParent.children[0]).to.equal(childNode); + expect(childNode.parent).to.equal(secondParent); + }); + }); }); }); -const parentChildGenerator = function() { +const parentChildGenerator = function () { let parentNode = new SceneGraphNodeBase({ parent: null, bounds: new Bounds({ @@ -273,7 +281,7 @@ const parentChildGenerator = function() { return parentNode; }; -const baseParams = function(tag) { +const baseParams = function (tag) { let bounds = new Bounds({ top: 1, bottom: 11, @@ -287,5 +295,5 @@ const baseParams = function(tag) { tags: [tag], rotation: 45, parent: null - } + }; }; diff --git a/test/canvas/node/sceneGraphNodeCanvas.test.js b/test/canvas/node/sceneGraphNodeCanvas.test.js index f58ef588..e4261258 100644 --- a/test/canvas/node/sceneGraphNodeCanvas.test.js +++ b/test/canvas/node/sceneGraphNodeCanvas.test.js @@ -3,19 +3,19 @@ import mq from '../../ui/mithrilQuerySetup'; import {Bounds} from '../../../src/model/Bounds'; import {SceneGraphNodeCanvas} from '../../../src/canvas/node/SceneGraphNodeCanvas'; -describe('SceneGraphNodeCanvas test', function() { - describe('constructor', function() { - it('should construct a new node', function() { +describe('SceneGraphNodeCanvas test', function () { + describe('constructor', function () { + it('should construct a new node', function () { let params = { model: 'model', appState: 'appState' }; let gestureRegex = { - pan: new RegExp('^pan'), - pinch: new RegExp('^pinch'), - tap: new RegExp('^tap'), - wheel: new RegExp('^wheel') - }; + pan: new RegExp('^pan'), + pinch: new RegExp('^pinch'), + tap: new RegExp('^tap'), + wheel: new RegExp('^wheel') + }; let node = new SceneGraphNodeCanvas(params); expect(node.model).to.equal(params.model); expect(node.appState).to.equal(params.appState); @@ -24,151 +24,167 @@ describe('SceneGraphNodeCanvas test', function() { }); }); - describe('custom getters', function() { - describe('get selected', function() { - it('should return true if this canvas is seleted', function() { - let p1 = baseParams(); - let parentNode = new SceneGraphNodeCanvas(p1); - parentNode.appState.selection.bioMaps[0] = parentNode; - expect(parentNode.selected).to.equal(true); - }); - it('should return false if canvas is not selected', function() { - let p1 = baseParams(); - let parentNode = new SceneGraphNodeCanvas(p1); - expect(parentNode.selected).to.equal(false); - }); - }); + describe('custom getters', function () { + describe('get selected', function () { + it('should return true if this canvas is seleted', function () { + let p1 = baseParams(); + let parentNode = new SceneGraphNodeCanvas(p1); + parentNode.appState.selection.bioMaps[0] = parentNode; + expect(parentNode.selected).to.equal(true); + }); + it('should return false if canvas is not selected', function () { + let p1 = baseParams(); + let parentNode = new SceneGraphNodeCanvas(p1); + expect(parentNode.selected).to.equal(false); + }); + }); }); - describe('mithril lifecycle events', function() { - it('should generate approprate output', function() { + describe('mithril lifecycle events', function () { + it('should generate approprate output', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let domBounds = new Bounds({ - top: 1, - bottom: 11, - left: 10, - right: 20, - width: 10, - height: 10 - }); - let out = mq(parentNode,{domBounds: domBounds}); + let domBounds = new Bounds({ + top: 1, + bottom: 11, + left: 10, + right: 20, + width: 10, + height: 10 + }); + let out = mq(parentNode, {domBounds: domBounds}); expect(out.vnode.tag).to.eql(parentNode); - out.should.have('canvas'); - out.should.have('.cmap-canvas'); - out.should.have('.cmap-biomap'); + out.should.have('canvas'); + out.should.have('.cmap-canvas'); + out.should.have('.cmap-biomap'); }); }); - describe('public methods', function() { - describe('draw(ctx)', function() { - it('should return if no context', function() { + describe('public methods', function () { + describe('draw(ctx)', function () { + it('should return if no context', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - expect(function() {parentNode.draw()}).to.not.throw(); + expect(function () { + parentNode.draw(); + }).to.not.throw(); }); - it('should return if no bounds', function() { + it('should return if no bounds', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - parentNode.context2d = { - clearRect : ()=>{return true;}, - save : () => {return true;}, - restore : () => {return true;}, - }; - expect(function() {parentNode.draw()}).to.not.throw(); + parentNode.context2d = { + clearRect: () => { + return true; + }, + save: () => { + return true; + }, + restore: () => { + return true; + }, + }; + expect(function () { + parentNode.draw(); + }).to.not.throw(); }); - it('should propegate if both domBounds and context', function() { + it('should propegate if both domBounds and context', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - parentNode.context2d = { - clearRect : ()=>{return true;}, - save : () => {return true;}, - restore : () => {return true;}, - }; - parentNode.canvas = {width:10, height:10}; - let domBounds = new Bounds({ - top: 1, - left: 10, - width: 10, - height: 10 - }); - parentNode.domBounds = domBounds; - parentNode.bounds = domBounds; - parentNode.draw(); - expect(parentNode.lastDrawnCanvasBounds).to.eql(domBounds); + parentNode.context2d = { + clearRect: () => { + return true; + }, + save: () => { + return true; + }, + restore: () => { + return true; + }, + }; + parentNode.canvas = {width: 10, height: 10}; + let domBounds = new Bounds({ + top: 1, + left: 10, + width: 10, + height: 10 + }); + parentNode.domBounds = domBounds; + parentNode.bounds = domBounds; + parentNode.draw(); + expect(parentNode.lastDrawnCanvasBounds).to.eql(domBounds); }); }); - describe('handleGesture(evt)', function() { - it('should recognise pan', function() { + describe('handleGesture(evt)', function () { + it('should recognise pan', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'pan'}; + let evt = {type: 'pan'}; expect(parentNode.handleGesture(evt)).to.eql(parentNode._onPan(evt)); }); - it('should recognise tap', function() { + it('should recognise tap', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'tap'}; + let evt = {type: 'tap'}; expect(parentNode.handleGesture(evt)).to.eql(parentNode._onTap(evt)); }); - it('should recognise wheel', function() { + it('should recognise wheel', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'wheel'}; + let evt = {type: 'wheel'}; expect(parentNode.handleGesture(evt)).to.eql(parentNode._onZoom(evt)); }); - it('should recognise pinch', function() { + it('should recognise pinch', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'pinch'}; + let evt = {type: 'pinch'}; expect(parentNode.handleGesture(evt)).to.eql(parentNode._onZoom(evt)); }); - it('should ignore invalid event', function() { + it('should ignore invalid event', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'quack'}; + let evt = {type: 'quack'}; expect(parentNode.handleGesture(evt)).to.equal(false); }); - }); - }); + }); + }); - describe('private methods', function() { - describe('_onZoom(evt)', function() { - it('should propegate zoom event', function() { + describe('private methods', function () { + describe('_onZoom(evt)', function () { + it('should propegate zoom event', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'quack'}; + let evt = {type: 'quack'}; expect(parentNode._onZoom(evt)).to.equal(false); }); }); - describe('_onTap(evt)', function() { - it('should not block tap event', function() { + describe('_onTap(evt)', function () { + it('should not block tap event', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'quack'}; + let evt = {type: 'quack'}; expect(parentNode._onTap(evt)).to.equal(false); }); }); - describe('_onPan(evt)', function() { - it('should not block pan event if direction is not provided', function() { + describe('_onPan(evt)', function () { + it('should not block pan event if direction is not provided', function () { let p1 = baseParams(); let parentNode = new SceneGraphNodeCanvas(p1); - let evt = {type:'quack'}; + let evt = {type: 'quack'}; expect(parentNode._onPan(evt)).to.equal(false); }); }); - }); + }); }); -const baseParams = function() { +const baseParams = function () { return { - model: {}, - appState: { selection: { bioMaps:[] } } - } + model: {}, + appState: {selection: {bioMaps: []}} + }; }; diff --git a/test/canvas/node/sceneGraphNodeGroup.test.js b/test/canvas/node/sceneGraphNodeGroup.test.js index 6209072b..74a849d4 100644 --- a/test/canvas/node/sceneGraphNodeGroup.test.js +++ b/test/canvas/node/sceneGraphNodeGroup.test.js @@ -1,10 +1,10 @@ import {expect} from 'chai'; import {Bounds} from '../../../src/model/Bounds'; -import {Group} from '../../../src/canvas/node/SceneGraphNodeGroup'; +import {SceneGraphNodeGroup} from '../../../src/canvas/node/SceneGraphNodeGroup'; -describe('SceneGraphNodeGroup test', function() { - describe('constructor', function() { - it('should create a new group', function() { +describe('SceneGraphNodeGroup test', function () { + describe('constructor', function () { + it('should create a new group', function () { let bounds = new Bounds({ top: 1, bottom: 11, @@ -20,7 +20,7 @@ describe('SceneGraphNodeGroup test', function() { tags: ['test'], rotation: 45 }; - let node = new Group(params); + let node = new SceneGraphNodeGroup(params); expect(node.parent).to.equal(parent); expect(node.bounds).eql(bounds); expect(node.tags).eql(['test']); diff --git a/test/canvas/node/sceneGraphNodeTrack.test.js b/test/canvas/node/sceneGraphNodeTrack.test.js index 1cb6f302..fd62d63a 100644 --- a/test/canvas/node/sceneGraphNodeTrack.test.js +++ b/test/canvas/node/sceneGraphNodeTrack.test.js @@ -2,8 +2,8 @@ import {expect} from 'chai'; import {Bounds} from '../../../src/model/Bounds'; import {SceneGraphNodeTrack} from '../../../src/canvas/node/SceneGraphNodeTrack'; -describe('SceneGraphNodeTrack test', function() { - it('constructor works', function() { +describe('SceneGraphNodeTrack test', function () { + it('constructor works', function () { let bounds = new Bounds({ top: 1, bottom: 11, @@ -19,7 +19,7 @@ describe('SceneGraphNodeTrack test', function() { tags: ['test'], rotation: 45 }; - let node = new SceneGraphNodeTrack (params); + let node = new SceneGraphNodeTrack(params); expect(node.parent).to.equal(parent); expect(node.bounds).eql(bounds); expect(node.tags).eql(['test']); diff --git a/test/model/AppModel.test.js b/test/model/AppModel.test.js index 57110108..db4a2285 100644 --- a/test/model/AppModel.test.js +++ b/test/model/AppModel.test.js @@ -6,9 +6,9 @@ import {AppModel} from '../../src/model/AppModel'; const config = require('../../cmap.json'); -describe('AppModel test', function() { +describe('AppModel test', function () { - it('constructor works', function() { + it('constructor works', function () { const model = new AppModel(); expect(model).to.have.property('sources') .that.is.an('array'); diff --git a/test/model/BioMapModel.test.js b/test/model/BioMapModel.test.js index c6ccb8df..f9a7ee2f 100644 --- a/test/model/BioMapModel.test.js +++ b/test/model/BioMapModel.test.js @@ -2,18 +2,16 @@ import {expect} from 'chai'; import {BioMapModel} from '../../src/model/BioMapModel'; import {DataSourceModel} from '../../src/model/DataSourceModel'; - - let model; const params = { name: 'Pv01', features: [], - coordinates: { start: 42, stop: 142 }, - source: new DataSourceModel( { id: 'test'} ) + coordinates: {start: 42, stop: 142}, + source: new DataSourceModel({id: 'test'}) }; -describe('BioMapModel test', function() { - it('constructor works', function() { +describe('BioMapModel test', function () { + it('constructor works', function () { model = new BioMapModel(params); expect(model).to.have.property('source') .that.is.an('object'); @@ -27,11 +25,11 @@ describe('BioMapModel test', function() { expect(model.coordinates.stop).to.equal(142); }); - it('length getter works', function() { + it('length getter works', function () { expect(model.length).to.equal(100); }); - it('uniqueName getter works', function() { + it('uniqueName getter works', function () { expect(model).to.have.property('uniqueName') .that.is.a('string'); expect(model.uniqueName).to.equal('test/Pv01'); diff --git a/test/model/Bounds.test.js b/test/model/Bounds.test.js index be13c42a..2aacc030 100644 --- a/test/model/Bounds.test.js +++ b/test/model/Bounds.test.js @@ -1,7 +1,7 @@ import {expect, assert} from 'chai'; import {Bounds} from '../../src/model/Bounds'; -describe('Bounds test', function() { +describe('Bounds test', function () { let params = { top: 1, bottom: 11, @@ -11,7 +11,7 @@ describe('Bounds test', function() { height: 10 }; - it('constructor works', function() { + it('constructor works', function () { let b = new Bounds(params); expect(b.top).eql(params.top); expect(b.bottom).eql(params.bottom); @@ -21,7 +21,7 @@ describe('Bounds test', function() { expect(b.height).eql(params.height); }); - it('constructor calculates missing width, height', function() { + it('constructor calculates missing width, height', function () { let missingParams = Object.assign({}, params); delete missingParams.width; delete missingParams.height; @@ -30,7 +30,7 @@ describe('Bounds test', function() { expect(b.height).to.equal(10); }); - it('constructor calculates missing bottom, right', function() { + it('constructor calculates missing bottom, right', function () { let missingParams = Object.assign({}, params); delete missingParams.bottom; delete missingParams.right; @@ -39,73 +39,73 @@ describe('Bounds test', function() { expect(b.right).to.equal(20); }); - it('ignores x and y properties from DOMRect', function() { - let paramsWithExtras = Object.assign({ x: -1, y: -1 }, params); + it('ignores x and y properties from DOMRect', function () { + let paramsWithExtras = Object.assign({x: -1, y: -1}, params); let b = new Bounds(paramsWithExtras); expect(b.x).eql(undefined); expect(b.y).eql(undefined); }); - it('equals()', function() { + it('equals()', function () { let b1 = new Bounds(params); let b2 = new Bounds(params); assert(Bounds.equals(b1, b2)); assert(b1.equals(b2)); params.width = 7; b2 = new Bounds(params); - assert(! b1.equals(b2)); + assert(!b1.equals(b2)); }); - it('equals() rounds to pixel', function() { - let paramsWithExtras = Object.assign({ width: params.width + 0.1}, params); + it('equals() rounds to pixel', function () { + let paramsWithExtras = Object.assign({width: params.width + 0.1}, params); let b1 = new Bounds(paramsWithExtras); let b2 = new Bounds(params); assert(Bounds.equals(b1, b2)); assert(b1.equals(b2)); }); - it('equals() handles nils', function() { + it('equals() handles nils', function () { let b = new Bounds(params); - [null, undefined].forEach( nil => { - assert(! Bounds.equals(nil, b)); - assert(! Bounds.equals(b, nil)); - assert(! Bounds.equals(nil, nil)); + [null, undefined].forEach(nil => { + assert(!Bounds.equals(nil, b)); + assert(!Bounds.equals(b, nil)); + assert(!Bounds.equals(nil, nil)); }); }); - it('emptyArea()', function() { + it('emptyArea()', function () { let b = new Bounds({top: 10, left: 10, width: 0, height: 0}); assert(b.isEmptyArea); b = new Bounds({top: 10, left: 10, width: 100, height: 90}); - assert(! b.isEmptyArea); + assert(!b.isEmptyArea); b = new Bounds({top: 10, left: 10, width: 100, height: 0}); assert(b.isEmptyArea); }); - it('area()', function() { + it('area()', function () { let b = new Bounds({top: 10, left: 10, width: 100, height: 90}); expect(b.area).to.equal(9000); }); - it('areaEquals()', function() { + it('areaEquals()', function () { let b = new Bounds({top: 10, left: 10, width: 10, height: 2}); let bp = new Bounds({top: 10, left: 10, width: 5, height: 4}); assert(Bounds.areaEquals(b, bp)); expect(b.areaEquals(bp)); }); - it('areaEquals() rounds to pixel', function() { + it('areaEquals() rounds to pixel', function () { let b = new Bounds({top: 10, left: 10, width: 10.5, height: 2}); let bp = new Bounds({top: 10, left: 10, width: 10, height: 2}); expect(b.areaEquals(bp)); }); - it('areaEquals() handles nils', function() { + it('areaEquals() handles nils', function () { let b = new Bounds(params); - [null, undefined].forEach( nil => { - assert(! Bounds.areaEquals(nil, b)); - assert(! Bounds.areaEquals(b, nil)); - assert(! Bounds.areaEquals(nil, nil)); + [null, undefined].forEach(nil => { + assert(!Bounds.areaEquals(nil, b)); + assert(!Bounds.areaEquals(b, nil)); + assert(!Bounds.areaEquals(nil, nil)); }); }); }); diff --git a/test/model/DataSourceModel.test.js b/test/model/DataSourceModel.test.js index 0f4311f9..2da2be48 100644 --- a/test/model/DataSourceModel.test.js +++ b/test/model/DataSourceModel.test.js @@ -3,10 +3,10 @@ import {DataSourceModel} from '../../src/model/DataSourceModel'; const config = require('../../cmap.json'); -describe('DataSourceModel test', function() { +describe('DataSourceModel test', function () { - it('constructor works', function() { - config.sources.forEach( params => { + it('constructor works', function () { + config.sources.forEach(params => { const model = new DataSourceModel(params); expect(model).to.have.property('method') .that.is.a('string'); @@ -21,7 +21,7 @@ describe('DataSourceModel test', function() { // TODO: test the load() function which will cause an http fetch. - it('deserialize works', function() { + it('deserialize works', function () { const model = new DataSourceModel(config.sources[0]); model.deserialize(data); expect(model).to.have.property('parseResult') @@ -30,12 +30,12 @@ describe('DataSourceModel test', function() { expect(model.parseResult.data).to.have.lengthOf(9); }); - it('includeRecord equals', function() { + it('includeRecord equals', function () { let model; config.sources[1].filters = [{ - column : 'feature_type', - operator : 'equals', - value : 'gene_test' + column: 'feature_type', + operator: 'equals', + value: 'gene_test' }]; model = new DataSourceModel(config.sources[1]); model.deserialize(data); @@ -43,13 +43,13 @@ describe('DataSourceModel test', function() { expect(model.parseResult.data).to.have.lengthOf(1); }); - it('includeRecord not', function() { + it('includeRecord not', function () { let model; config.sources[1].filters = [{ - column : 'feature_type', + column: 'feature_type', not: true, - operator : 'equals', - value : 'gene_test' + operator: 'equals', + value: 'gene_test' }]; model = new DataSourceModel(config.sources[1]); model.deserialize(data); @@ -57,12 +57,12 @@ describe('DataSourceModel test', function() { expect(model.parseResult.data).to.have.lengthOf(8); }); - it('includeRecord regex', function() { + it('includeRecord regex', function () { let model; config.sources[1].filters = [{ - column : 'feature_type', - operator : 'regex', - value : '^g.+' + column: 'feature_type', + operator: 'regex', + value: '^g.+' }]; model = new DataSourceModel(config.sources[1]); model.deserialize(data); @@ -70,16 +70,16 @@ describe('DataSourceModel test', function() { expect(model.parseResult.data).to.have.lengthOf(8); }); - it('includeRecord multiple filters', function() { + it('includeRecord multiple filters', function () { let model; config.sources[1].filters = [{ - column : 'map_name', - operator : 'equals', - value : 'Pv01' + column: 'map_name', + operator: 'equals', + value: 'Pv01' }, { - column : 'feature_type', - operator : 'regex', - value : '_test$' + column: 'feature_type', + operator: 'regex', + value: '_test$' }]; model = new DataSourceModel(config.sources[1]); model.deserialize(data); @@ -89,13 +89,13 @@ describe('DataSourceModel test', function() { }); const data = -"map_name map_start map_stop feature_start feature_stop feature_name feature_type\n" + -"Pv01 0 69.6073 13.3417 13.3469 phavu.Phvul.001G073700 gene\n" + -"Pv01 0 69.6073 1.33705 1.34065 phavu.Phvul.001G011400 gene\n" + -"Pv01 0 69.6073 13.4007 13.4024 phavu.Phvul.001G073800 gene\n" + -"Pv01 0 69.6073 1.34167 1.34511 phavu.Phvul.001G011500 gene\n" + -"Pv01 0 69.6073 13.4293 13.4313 phavu.Phvul.001G073900 gene\n" + -"Pv01 0 69.6073 1.34743 1.35232 phavu.Phvul.001G011600 gene\n" + -"Pv01 0 69.6073 13.4815 13.5003 phavu.Phvul.001G074000 gene\n" + -"Pv01 0 69.6073 13.5095 13.5121 phavu.Phvul.001G074100 gene_test\n" + -"Pv01 0 69.6073 13.5127 13.52 phavu.Phvul.001G074200 xyz_test\n"; + 'map_name map_start map_stop feature_start feature_stop feature_name feature_type\n' + + 'Pv01 0 69.6073 13.3417 13.3469 phavu.Phvul.001G073700 gene\n' + + 'Pv01 0 69.6073 1.33705 1.34065 phavu.Phvul.001G011400 gene\n' + + 'Pv01 0 69.6073 13.4007 13.4024 phavu.Phvul.001G073800 gene\n' + + 'Pv01 0 69.6073 1.34167 1.34511 phavu.Phvul.001G011500 gene\n' + + 'Pv01 0 69.6073 13.4293 13.4313 phavu.Phvul.001G073900 gene\n' + + 'Pv01 0 69.6073 1.34743 1.35232 phavu.Phvul.001G011600 gene\n' + + 'Pv01 0 69.6073 13.4815 13.5003 phavu.Phvul.001G074000 gene\n' + + 'Pv01 0 69.6073 13.5095 13.5121 phavu.Phvul.001G074100 gene_test\n' + + 'Pv01 0 69.6073 13.5127 13.52 phavu.Phvul.001G074200 xyz_test\n'; diff --git a/test/model/Feature.test.js b/test/model/Feature.test.js index cf39b3a0..8ce0db66 100644 --- a/test/model/Feature.test.js +++ b/test/model/Feature.test.js @@ -1,61 +1,62 @@ import {expect} from 'chai'; import {Feature, featuresInCommon} from '../../src/model/Feature'; -describe('Feature test', function() { +describe('Feature test', function () { let params = { - name: 'test feature', - tags: ['hilite', 'etc'], - aliases: ['foo', 'feature 123'], + source :{}, coordinates: { start: 10, stop: 10 - } + }, + name: 'test feature', + tags: ['hilite', 'etc'], + aliases: ['foo', 'feature 123'], }; - it('constructor works', function() { + it('constructor works', function () { let f = new Feature(params); expect(f).eql(params); }); - it('length()', function() { + it('length()', function () { let f = new Feature(params); expect(f.length).to.equal(0); let p1 = Object.assign(params, { aliases: [], - coordinates: { start: 100, stop: 142 } + coordinates: {start: 100, stop: 142} }); f = new Feature(p1); expect(f.length).to.equal(42); }); - it('featuresInCommon()', function() { + it('featuresInCommon()', function () { let i, features1 = [], features2 = []; for (i = 1; i <= 10; i++) { let name = `feature ${i}`; - let p1 = Object.assign(params, { name }); + let p1 = Object.assign(params, {name}); features1.push(new Feature(p1)); } for (i = 8; i <= 15; i++) { let name = `feature ${i}`; - let p1 = Object.assign(params, { name }); + let p1 = Object.assign(params, {name}); features2.push(new Feature(p1)); } let res = featuresInCommon(features1, features2); expect(res.length).to.equal(3); }); - it('featuresInCommon() with aliases', function() { + it('featuresInCommon() with aliases', function () { let i, features1 = [], features2 = []; for (i = 1; i <= 10; i++) { let name = `feature ${i}`; let aliases = [`foo ${i}`, `bar ${i}`]; - let p = Object.assign(params, { name, aliases }); + let p = Object.assign(params, {name, aliases}); features1.push(new Feature(p)); } for (i = 8; i <= 15; i++) { let name = `misnamed feature xxx${i}`; let aliases = [`foo ${i}`, `bling ${i}`]; - let p = Object.assign(params, { name, aliases }); + let p = Object.assign(params, {name, aliases}); features2.push(new Feature(p)); } let res = featuresInCommon(features1, features2); diff --git a/test/ui/UI_test.js b/test/ui/UI_test.js index f9ac4864..2a59ce1e 100644 --- a/test/ui/UI_test.js +++ b/test/ui/UI_test.js @@ -5,6 +5,7 @@ describe('UI component', function() { it('should generate appropriate output', function() { let component = new UI(); let out = mq(component); - out.should.have('div.cmap-layout.cmap-vbox > div.cmap-layout-viewport.cmap-hbox'); + out.should.have('div.cmap-layout.cmap-vbox'); + out.should.have('div.cmap-layout-viewport.cmap-hbox'); }); }); diff --git a/test/ui/cmap.test.js b/test/ui/cmap.test.js index cbe1c7bb..58dc636a 100644 --- a/test/ui/cmap.test.js +++ b/test/ui/cmap.test.js @@ -2,8 +2,8 @@ import './mithrilQuerySetup'; import {CMAP} from '../../src/ui/CMAP'; import {assert} from 'chai'; -describe('CMAP class', function() { - it('constructor works', function() { +describe('CMAP class', function () { + it('constructor works', function () { let cmap = new CMAP(); assert(cmap); }); diff --git a/test/ui/elementsFromPoint_test.js b/test/ui/elementsFromPoint_test.js index be84a296..a7b5e30c 100644 --- a/test/ui/elementsFromPoint_test.js +++ b/test/ui/elementsFromPoint_test.js @@ -1,8 +1,8 @@ import {assert} from 'chai'; import '../../src/polyfill/index'; -describe('elementsFromPoint polyfill', function() { - it('function exists document.elementsFromPoint()', function() { +describe('elementsFromPoint polyfill', function () { + it('function exists document.elementsFromPoint()', function () { assert(document.elementsFromPoint); }); }); diff --git a/test/ui/mithrilQuerySetup.js b/test/ui/mithrilQuerySetup.js index f904e27f..cb66527a 100644 --- a/test/ui/mithrilQuerySetup.js +++ b/test/ui/mithrilQuerySetup.js @@ -2,6 +2,6 @@ global.window = Object.assign( require('mithril/test-utils/domMock.js')(), require('mithril/test-utils/pushStateMock')(), ); -const mock = require( 'mithril/test-utils/browserMock' )(global); +const mock = require('mithril/test-utils/browserMock')(global); global.document = mock.document; export default require('mithril-query'); diff --git a/test/ui/tools/Reset_test.js b/test/ui/tools/Reset_test.js index 838c4067..8d6e6d37 100644 --- a/test/ui/tools/Reset_test.js +++ b/test/ui/tools/Reset_test.js @@ -7,9 +7,9 @@ import PubSub from 'pubsub-js'; import {ResetButton} from '../../../src/ui/tools/ResetButton'; import {reset as resetTopic} from '../../../src/topics'; -describe('Reset button', function() { +describe('Reset button', function () { - it('should generate appropriate output', function() { + it('should generate appropriate output', function () { const component = new ResetButton(); const out = mq(component); out.should.have('button'); @@ -17,9 +17,9 @@ describe('Reset button', function() { out.should.contain('Reset'); }); - it('should publish a PubSub reset event', function() { + it('should publish a PubSub reset event', function () { // eslint-disable-next-line no-unused-vars - const p = new Promise( (resolve, reject) => { + const p = new Promise((resolve, reject) => { const component = new ResetButton(); const out = mq(component); PubSub.subscribe(resetTopic, resolve); diff --git a/test/util/concatAll.test.js b/test/util/concatAll.test.js index 1fac5596..e508827a 100644 --- a/test/util/concatAll.test.js +++ b/test/util/concatAll.test.js @@ -1,14 +1,14 @@ import {assert, expect} from 'chai'; import '../../src/util/concatAll'; -describe('concatAll test', function() { - it('ok', function() { +describe('concatAll test', function () { + it('ok', function () { assert(Array.prototype.concatAll); const input = [ - [1,2,3], - ['foo','bar','bla'], + [1, 2, 3], + ['foo', 'bar', 'bla'], ]; - const expected = [ 1, 2, 3, 'foo', 'bar', 'bla' ]; + const expected = [1, 2, 3, 'foo', 'bar', 'bla']; const output = input.concatAll(); expect(output).to.eql(expected); }); diff --git a/test/util/isNil.test.js b/test/util/isNil.test.js index 7e038a34..4c2d0b7e 100644 --- a/test/util/isNil.test.js +++ b/test/util/isNil.test.js @@ -1,13 +1,13 @@ import {assert} from 'chai'; import {isNil} from '../../src/util/isNil'; -describe('isNil test', function() { - it('ok', function() { - assert(! isNil(0)); - assert(! isNil('')); - assert(! isNil('foo')); - assert(! isNil(123)); - assert(! isNil({})); +describe('isNil test', function () { + it('ok', function () { + assert(!isNil(0)); + assert(!isNil('')); + assert(!isNil('foo')); + assert(!isNil(123)); + assert(!isNil({})); assert(isNil(null)); assert(isNil(undefined)); });