├── .gitignore ├── testData ├── dip00.su └── dip00.segy ├── bower.json ├── package.json ├── LICENSE ├── app ├── menu.js ├── chart.css ├── d3psd.js ├── autopicker.js ├── app.js └── d3seis.js ├── README.md ├── index.html └── suServer.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /testData/dip00.su: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanandak/d3-su-picker/HEAD/testData/dip00.su -------------------------------------------------------------------------------- /testData/dip00.segy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanandak/d3-su-picker/HEAD/testData/dip00.segy -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psql-app", 3 | "description": "", 4 | "main": "index.html", 5 | "license": "MIT", 6 | "homepage": "", 7 | "ignore": [ 8 | "**/.*", 9 | "node_modules", 10 | "bower_components", 11 | "test", 12 | "tests" 13 | ], 14 | "dependencies": { 15 | "angular": "^1.6.2", 16 | "d3": "^4.6.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "psql-app", 3 | "version": "0.1.0", 4 | "main": "index.html", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/sanandak/d3-su-picker.git" 8 | }, 9 | "author": "Sridhar Anandakrishnan ", 10 | "license": "MIT", 11 | "single-instance": true, 12 | "window": { 13 | "title": "PSU d3 picker", 14 | "width": 900, 15 | "height": 1000, 16 | "toolbar": true 17 | }, 18 | "chromium-args": "--child-clean-exit", 19 | "dependencies": { 20 | "cardinal-spline-js": "^2.3.7", 21 | "d3": "^4.6.0", 22 | "d3-peaks": "0.0.1", 23 | "sprintf-js": "^1.0.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sridhar Anandakrishnan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | exports.initMenu = function () { 2 | console.log('in initmenu') 3 | var win = global.gui.Window.get(); 4 | var menubar = new global.gui.Menu({type: 'menubar'}); 5 | 6 | menubar.createMacBuiltin('PSQL') 7 | win.menu = menubar; 8 | 9 | var fileMenu = new global.gui.Menu(); 10 | fileMenu.append(new global.gui.MenuItem({ 11 | label: 'New', 12 | click: function () { 13 | console.log('new file'); 14 | } 15 | })); 16 | fileMenu.append(new global.gui.MenuItem({ 17 | label: 'Open', 18 | key: 'O', 19 | modifiers: 'cmd' 20 | //click: function () { 21 | // console.log('open file'); 22 | //} 23 | })); 24 | fileMenu.append(new global.gui.MenuItem({ 25 | label: 'Save', 26 | key: 'S', 27 | modifiers: 'cmd' 28 | })); 29 | 30 | menubar.append(new global.gui.MenuItem({ 31 | label: 'File', 32 | submenu: fileMenu 33 | })); 34 | 35 | var devMenu = new global.gui.Menu(); 36 | devMenu.append(new global.gui.MenuItem({ 37 | label: 'Open DevTools', 38 | click: function () { 39 | win.showDevTools(); 40 | } 41 | })); 42 | 43 | menubar.append(new global.gui.MenuItem({ 44 | label: 'DevTools', 45 | submenu: devMenu 46 | })); 47 | 48 | //win.showDevTools(); 49 | console.log('menubar', menubar); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/chart.css: -------------------------------------------------------------------------------- 1 | axis path, 2 | .axis line { 3 | fill: none; 4 | stroke: #000; 5 | shape-rendering: crispEdges; 6 | } 7 | 8 | .ctext { 9 | font-family: monospace; 10 | font-size: 14; 11 | text-anchor: middle; 12 | 13 | } 14 | 15 | .tick { 16 | font: 10px sans-serif; 17 | } 18 | 19 | .line { 20 | fill: none; 21 | stroke: #000; 22 | stroke-width: .5px; 23 | } 24 | 25 | .fline { 26 | fill: none; 27 | stroke: #000; 28 | stroke-width: .5px; 29 | } 30 | 31 | .ffills { 32 | fill: url(#focus-area-gradient) 33 | } 34 | 35 | /* 36 | .ffills { 37 | clip-path: url(#clip); 38 | } 39 | .fline { 40 | clip-path: url(#clip); 41 | } 42 | */ 43 | 44 | .cursor { 45 | fill: #d2e7d2; 46 | fill-opacity: 0.4; 47 | stroke: #228b22; 48 | stroke-width: 1px; 49 | } 50 | 51 | .markpts { 52 | fill: red; 53 | stroke: none; 54 | r: 3px; 55 | } 56 | 57 | .markline { 58 | fill: none; 59 | stroke-width: 1px; 60 | stroke: red; 61 | } 62 | 63 | .overlay { 64 | fill: none; 65 | stroke: none; 66 | pointer-events: all; 67 | } 68 | 69 | .nmo { 70 | stroke: black; 71 | stroke-width: 0.5px; 72 | fill: none; 73 | } 74 | .psd { 75 | stroke: black; 76 | stroke-width: 1px; 77 | fill: none; 78 | } 79 | 80 | 81 | .zoom { 82 | cursor: move; 83 | fill: none; 84 | pointer-events: all; 85 | } 86 | 87 | .picks { 88 | pointer-events: all; 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-su-picker 2 | 3 | A d3-based SU (Seismic Unix) viewer and picker. The viewer is built 4 | on `nwjs`, a technology that let's one run web-based applications on 5 | the desktop. The display is 6 | through `d3`, a javascript library for visualization. 7 | 8 | This tool allows one to _pick_ the times of events of interest in a 9 | seismic file and save the picks to a text file. 10 | 11 | ## suServer - a python script to provide data 12 | 13 | Javascript does not have a segy reader, thus we use python to read su 14 | and segy files and serve them to the picker through `WebSockets`. 15 | 16 | ## Installation 17 | 18 | Download nwjs (http://nwjs.io) 19 | 20 | Install `node` (http://nodejs.org), which includes `npm` (the node 21 | package manager) 22 | 23 | Install `bower` (another package manager(?)) 24 | 25 | npm install bower -g 26 | 27 | git clone https://github.com/sanandak/d3-su-picker 28 | cd d3-su-picker 29 | # install the required packages 30 | bower install 31 | npm install 32 | 33 | Install python3 3.5 or less (preferably through anaconda (http://continuum.io)) 34 | 35 | conda config --add channels conda-forge 36 | conda install obspy 37 | conda install websockets 38 | 39 | ## Requirements 40 | 41 | These are installed by `npm` and `bower` 42 | - d3js v4 43 | - sprintf-js 44 | - angularjs 45 | 46 | These are installed by `conda` (`pip` may work - untested) 47 | - obspy 48 | - websockets 49 | - numpy and scipy are installed with obspy 50 | 51 | ## Usage 52 | 53 | /path/to/nwjs /path/to/d3-su-picker 54 | 55 | Buttons to open an SU file and to save picks. 56 | 57 | The viewer displays the first _ensemble_ (by default, the first `ffid` - field file id). 58 | 59 | Keyboard commands: 60 | - `j` and `k` move the cursor forward and back in time by one sample. 61 | - `J` and `K` move the cursor by 10 samples 62 | - `h` and `l` move the cursor to the next/previous trace 63 | - `H` and `L` move the cursor by 10 traces 64 | - `z` and `Z` zoom in and out in time 65 | - `t` and `T` zoom in and out in space (fewer and more traces are displayed) 66 | - `p` picks the time of the cursor 67 | - `d` deletes the pick 68 | 69 | The saved picks file is a JSON file that is an array of traces. 70 | - sufile: file name of the picked data 71 | - picktime: when the picking was done 72 | - picks: an array of picks, with +/- 100 samps around the pick 73 | 74 | Each picks member is a _trace_ object with the following fields: 75 | - `pickT`- the time of the pick 76 | - `pickIdx` - the sample number 77 | - 'pickVal` - the value 78 | - `tracl` - see SU docs for the definition of these fields 79 | - `ens` - the ffid or cdp ensemble number 80 | - `samps` (an array) - 100 samps before the pick and 100 after 81 | 82 | Each member of `samps` is an object with `t` and `v` fields: `{t: 0, v: .01}` 83 | 84 | ## TODO 85 | 86 | - Read SEG-Y files. 87 | - Read pick files. 88 | - populate pick file more fully 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /app/d3psd.js: -------------------------------------------------------------------------------- 1 | angular.module('psqlApp') 2 | .directive('d3Psd', [function() { 3 | function link(scope, element, attr) { 4 | var margins = { 5 | left: 50, 6 | right: 30, 7 | top: 30, 8 | bottom: 30 9 | }, 10 | w = 400 - margins.left - margins.right, 11 | h = 300 - margins.top - margins.bottom; 12 | 13 | var data = scope.data; 14 | console.log('in d3 psd', data); 15 | var psd = [] 16 | for(var i=0; i 0) { 10 | compr = function(a,b){return b-a;}; // return largest 11 | } else { 12 | compr = function(a,b){return a-b;}; // return smallest 13 | } 14 | // search for index of largest or smallest 15 | let pkI = d3.scan(a.slice(lims[0], lims[1]), compr) + lims[0]; 16 | // .reduce((max, p, i) => p > max.val ? {val:p, idx:i} : max, {val:a[0], idx:0}); 17 | return pkI; 18 | }; 19 | /* return the indices of all zero crossing of array a */ 20 | const getZC = function(a, st, en) { 21 | const zc=[]; 22 | for(let i=st+1; i<=en; i++) { 23 | if(Math.sign(a[i]) != Math.sign(a[i-1])) zc.push(i); 24 | } 25 | return zc; 26 | //idx = b.reduce((zc, p, i, a) => i>0 ? (Math.sign(a[i]) != Math.sign(a[i-1]) ? zc.push(i) : zc) : 0, 0); 27 | } 28 | 29 | var autop = function(trc, cursI, dt) { 30 | let a0 = trc.samps.map(x=>[x.t,x.v]); // convert to 2d array [[x1,y1], [x2,y2], ...] 31 | // and convert to flattened [x1,y1,x2,y2,...] 32 | let arr = [].concat(...a0); // ... is the "spread" operator 33 | let v0 = arr.filter((v,i) => i%2==1); 34 | let t0 = arr.filter((v,i) => i%2==0); 35 | 36 | let a10 = getCurvePoints(arr, 0.5, 10); // interpolate by x10 37 | 38 | // interpolated value and time arrays 39 | let v10 = a10.filter((v,i) => i%2==1); // get ys 40 | let t10 = a10.filter((t,i) => i%2==0); // get ts 41 | 42 | //let interp = d3.interpolateBasis(vArr); 43 | //let v10 = d3.quantize(interp, vArr.length * 10); // x10 interp 44 | let cursI10 = cursI * 10; 45 | 46 | let sign = 1; 47 | if(trc.samps[cursI].v < 0.) { // i'm in a trough 48 | sign = -1; 49 | v10.forEach((x,i,a) => a[i]=-x); // negate value arrays 50 | v0.forEach((x,i,a) => a[i]=-x); 51 | } 52 | 53 | // get peak/troughs of the interpolated array 54 | let zcpre = getZC(v10, cursI10-100, cursI10); // all zc prior to me 55 | let zcpost = getZC(v10, cursI10, cursI10+100); // and after me 56 | // the two zero crossing before me bound the prev trough 57 | let trfpre = getEx(v10, zcpre.slice(-2), -1); 58 | // and the zero crossings after me are the next trough 59 | let trfpost = getEx(v10, zcpost.slice(0,2), -1); 60 | // peak value between the last zero crossing prior to me and the first one after. 61 | let pk = getEx(v10, [zcpre[zcpre.length-1], zcpost[0]], 1) 62 | let picks10 = { 63 | peakVal: sign*v10[pk], peakTime: t10[pk], 64 | trough0Val: sign*v10[trfpre], trough0Time: t10[trfpre], 65 | trough1Val: sign*v10[trfpost], trough1Time: t10[trfpost] 66 | } 67 | 68 | // get peak/troughs of the interpolated array 69 | zcpre = getZC(v0, cursI-10, cursI); // all zc prior to me 70 | zcpost = getZC(v0, cursI, cursI+10); // and after me 71 | // the two zero crossing before me bound the prev trough 72 | trfpre = getEx(v0, zcpre.slice(-2), -1); 73 | // and the zero crossings after me are the next trough 74 | trfpost = getEx(v0, zcpost.slice(0,2), -1); 75 | // peak value between the last zero crossing prior to me and the first one after. 76 | pk = getEx(v0, [zcpre[zcpre.length-1], zcpost[0]], 1) 77 | let picks = { 78 | peakVal: sign*v0[pk], peakTime: t0[pk], 79 | trough0Val: sign*v0[trfpre], trough0Time: t0[trfpre], 80 | trough1Val: sign*v0[trfpost], trough1Time: t0[trfpost] 81 | } 82 | console.log(picks); 83 | console.log(picks10); //pk, trfpre, trfpost, zcpre, zcpost); 84 | //let ppInt = {peakVal: pk.val, peakTime: t10[pk.idx], troughVal: trf.val, troughTime: t10[trf.idx]}; 85 | //console.log(ppInt); 86 | 87 | console.log('in autop', trc.samps[cursI], cursI); 88 | } 89 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | d3-based SU Picker 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |

SU Picker v.{{mainctrl.version}}

22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 | Wait for suServer to start... 38 |
39 | 40 |
41 |
42 |

Current file: {{mainctrl.basename}}

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
dt / ns{{mainctrl.dt}}s / {{mainctrl.ns}}
FFIDs / Min / Max{{mainctrl.nens}} / {{mainctrl.ens0}} / {{mainctrl.ensN}}
Curr FFID{{mainctrl.currEnsNum}}
57 | 58 |

59 | Decimate:
60 | Start Time:
61 | End Time:
62 |

63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 |
71 |

72 | 73 | Flo: {{mainctrl.flo}} Hz, Fhi: {{mainctrl.fhi}} Hz. 74 |

75 |
76 |
77 |

78 | Flo: 79 | Fhi: 80 |

81 |
82 |
83 | 84 |
85 |
86 | 89 |
90 |
91 | 94 |
95 |
96 | 97 |
98 | 99 |
100 | 101 |
102 |

Keys

103 |

Cursor motion

104 | h / l - left/right; 105 | j / k - down/up. 106 | Capital versions move by 10x; i.e. H moves by 107 | 10 traces where h move by 1. 108 |

109 |

Display

110 | z / Z - zoom in or out in time; 111 | t / T - zoom in or out in offset; 112 | y / Y - increase trace amplitudes up or down. 113 |

114 |

Picking

115 | p / d - pick/delete pick; 116 |

117 |

Mouse

118 | Mouse click sets NMO anchor point; mouse position defines NMO velocity; 119 | mouse out of window to cancel NMO line. 120 | 121 |
122 | 123 | 124 |
125 | 126 | 127 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | /* d3-su-picker - pick arrivals in su dara 2 | 3 | * 1. start suServer.py - an obspy backend that reads the segy 4 | * files and returns data as a json string 5 | * 2. a "controller" that connect to the server 6 | * 3. "directives" that plot the data 7 | */ 8 | 9 | 'use strict'; 10 | const _version = '0.2.0'; 11 | 12 | var fs = require('fs'); 13 | var path = require('path'); 14 | var sprintf = require('sprintf-js').sprintf; 15 | var cryp = require('crypto'); 16 | const cexec = require('child_process').execFile; 17 | var suio = require('segy-js'); 18 | const globX = 42; 19 | 20 | var app = angular.module('psqlApp', []); 21 | 22 | /* mainctrl - angularjs controller that starts suserver 23 | * and provides data to the directives 24 | */ 25 | 26 | app.controller('MainCtrl', ['$scope', function($scope) { 27 | 28 | self = this; 29 | self.filename = null; 30 | self.version = _version; 31 | var pickedTraces; 32 | var wsURL = 'ws://localhost:9191/websocket'; 33 | var ws; 34 | 35 | /* variables that appear on the front page or are provided to 36 | * directives */ 37 | self.wsIsOpen = false; 38 | self.nens = 0; 39 | self.ens0 = null; 40 | self.ensN = null; 41 | self.dt = null; 42 | self.currEns = null; 43 | self.currEnsNum = null; 44 | self.psd={freqs:[], psd:[]}; 45 | 46 | self.fnyq=100; 47 | self.flo=null; 48 | self.fhi=null; 49 | self.startT=null; 50 | self.endT=null; 51 | self.decimate=false; 52 | 53 | /* start and periodically check for the server */ 54 | 55 | function startws() { 56 | ws = new WebSocket(wsURL); 57 | ws.onopen = function() { 58 | self.wsIsOpen = true; 59 | console.log('ws opened'); 60 | $scope.$apply(); 61 | }; 62 | ws.onerror = function() { 63 | console.log('ws err'); 64 | }; 65 | } 66 | function checkws() { 67 | //console.log('checking ws state'); 68 | if(!ws || ws.readyState === WebSocket.CLOSED) { 69 | startws(); 70 | } 71 | } 72 | checkws(); 73 | setInterval(checkws, 5000); 74 | 75 | var hdrsByEnsemble; 76 | var servMsg; 77 | 78 | // when the user chooses "open" button, this function is called 79 | // it doesn't actually get the file - it calls the openfile 80 | // element 81 | self.open = function() { 82 | // this function will "click" the (invisible) openfile 83 | // button on the page, which will get the filename 84 | // that filename is sent to the server 85 | var chooser = document.getElementById('openfile'); 86 | chooser.addEventListener('change', function() { 87 | var filepath = this.value; 88 | var flist = this.files;// from Files API 89 | 90 | // same file - ignore 91 | if (self.filename == filepath) {return;} 92 | 93 | self.filename = filepath; 94 | self.basename = path.basename(self.filename); 95 | 96 | /* 97 | var nsdt = suio.getNSDT(self.filename); 98 | console.log(nsdt); 99 | nsdt.then(function(x) { 100 | console.log('nsdt', x); 101 | }, function(e) { 102 | console.error('fail', e); 103 | }); 104 | console.log(suio.readSU(self.filename)); 105 | */ 106 | 107 | // get hdrs only 108 | servMsg = {cmd:'getSegyHdrs', filename:filepath}; 109 | ws.send(JSON.stringify(servMsg)); 110 | ws.onmessage = function(evt) { 111 | var msg = JSON.parse(evt.data); 112 | var segyHdrs = JSON.parse(msg['segy']); 113 | //console.log(msg['cmd'], segyHdrs) 114 | 115 | // group into ensembles 116 | hdrsByEnsemble = d3.nest() 117 | .key(function(d) {return d.ffid;}) 118 | .entries(segyHdrs.hdrs); 119 | // returns [{key:'0', values:[trc0, trc1, ...]}] 120 | 121 | self.nens = hdrsByEnsemble.length; 122 | self.ens0 = hdrsByEnsemble[0].key; 123 | self.ensN = hdrsByEnsemble[self.nens-1].key; 124 | self.dt = segyHdrs.dt; 125 | self.ns = segyHdrs.ns; 126 | self.startT = 0.; 127 | self.endT = (self.ns-1) * self.dt; 128 | self.fnyq = 1.0/(2*self.dt); 129 | self.flo=0.0; 130 | self.fhi=self.fnyq/2; 131 | self.currEns = null; 132 | 133 | console.log(self.nens, self.ens0, self.ensN, self.dt, self.currEns, self.fnyq); 134 | $scope.$apply(); 135 | }; 136 | }) 137 | // presses the actual openfile (hidden) button 138 | chooser.click(); 139 | } 140 | 141 | // when the user chooses "save", this function is called 142 | self.save = function() { 143 | //console.log('savemenu click'); 144 | var chooser = document.getElementById('savefile'); 145 | //console.log(chooser); 146 | chooser.addEventListener('change', function() { 147 | var filepath = this.value; 148 | console.log(filepath); 149 | console.log(pickedTraces); 150 | fs.writeFileSync(filepath, 151 | JSON.stringify({ 152 | 'sufile':self.filename, 153 | 'picktime':new Date(), 154 | 'picks': pickedTraces 155 | })); 156 | // fs.writeFileSync(filepath, JSON.stringify(pickedTraces)); 157 | }) 158 | chooser.click(); 159 | }; 160 | 161 | // when the next and prev buttons are pressed... 162 | self.next = function() { 163 | //console.log("next ens") 164 | // first time... 165 | if(self.currEns === null) {self.currEns = 0;} 166 | else if(++self.currEns == self.nens) {self.currEns = 0;} 167 | 168 | var ens = hdrsByEnsemble[self.currEns].key; 169 | self.currEnsNum = ens; 170 | console.log('ens', self.currEns, ens); 171 | servMsg = {cmd:'getEnsemble', 172 | ensemble: ens, 173 | t1: self.startT, 174 | t2: self.endT, 175 | decimate: self.decimate}; 176 | 177 | ws.send(JSON.stringify(servMsg)); 178 | // get the PSD 179 | servMsg = {cmd:"getPSD", ensemble: ens}; 180 | ws.send(JSON.stringify(servMsg)); 181 | 182 | ws.onmessage = function(evt) { 183 | var msg = JSON.parse(evt.data); 184 | console.log(msg['cmd']) 185 | // FIXME - this should be 'getEnsemble' not 'segy' 186 | if(msg['cmd'] == 'segy') { 187 | var segy = JSON.parse(msg['segy']) 188 | self.data=segy; 189 | } else if(msg['cmd'] == 'getPSD') { 190 | console.log('psd msg', msg); 191 | var psd = msg['psd']; 192 | var freqs = msg['freqs'] 193 | self.psd = {'freqs': freqs, 'psd':psd}; 194 | console.log(self.psd); 195 | } 196 | $scope.$apply(); 197 | } 198 | }; 199 | 200 | self.prev = function() { 201 | console.log("prev ens") 202 | if(self.currEns === null) {self.currEns = self.nens - 1;} 203 | else if(--self.currEns == -1) {self.currEns = self.nens-1;} 204 | var ens = hdrsByEnsemble[self.currEns].key; 205 | self.currEnsNum = ens; 206 | console.log('ens', ens); 207 | 208 | // FIXME - this is repeat of the "next" block 209 | servMsg = {cmd:'getEnsemble', 210 | ensemble: ens, 211 | t1: self.startT, 212 | t2: self.endT, 213 | decimate: self.decimate}; 214 | 215 | ws.send(JSON.stringify(servMsg)); 216 | // get the PSD 217 | servMsg = {cmd:"getPSD", ensemble: ens}; 218 | ws.send(JSON.stringify(servMsg)); 219 | 220 | ws.onmessage = function(evt) { 221 | var msg = JSON.parse(evt.data); 222 | console.log(msg['cmd']) 223 | // FIXME - this should be 'getEnsemble' not 'segy' 224 | if(msg['cmd'] == 'segy') { 225 | var segy = JSON.parse(msg['segy']) 226 | self.data=segy; 227 | } else if(msg['cmd'] == 'getPSD') { 228 | console.log('psd msg', msg); 229 | var psd = msg['psd']; 230 | var freqs = msg['freqs'] 231 | self.psd = {'freqs': freqs, 'psd':psd}; 232 | console.log(self.psd); 233 | } 234 | $scope.$apply(); 235 | }; 236 | } 237 | 238 | // handle the filter value changes... 239 | self.checkVal = false; 240 | self.fcheck = function() { 241 | console.log('check:', self.checkVal); 242 | console.log('f', self.flo, self.fhi); 243 | 244 | var ens = hdrsByEnsemble[self.currEns].key; 245 | console.log('ens', self.currEns, ens); 246 | servMsg = {cmd:'getEnsemble', 247 | ensemble: ens, 248 | flo: self.flo, 249 | fhi: self.fhi, 250 | t1: self.startT, 251 | t2: self.endT, 252 | decimate: self.decimate}; 253 | 254 | ws.send(JSON.stringify(servMsg)); 255 | ws.onmessage = function(evt) { 256 | var msg = JSON.parse(evt.data); 257 | console.log(msg['cmd']); 258 | // FIXME - this should be 'getEnsemble' not 'segy' 259 | if(msg['cmd'] === 'segy') { 260 | var segy = JSON.parse(msg['segy']); 261 | self.data=segy; 262 | //console.log(msg['cmd'], segy) 263 | self.data=segy; 264 | $scope.$apply(); 265 | } 266 | } 267 | }; 268 | 269 | // when the directive updates picks, this function is called 270 | // to update the local variable pickedTraces... 271 | self.setpicks = function(picks) { 272 | pickedTraces = picks; 273 | } 274 | }]); 275 | -------------------------------------------------------------------------------- /suServer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | NAME 4 | suServer - websocket server for su data 5 | 6 | RETURNS 7 | returns a json string 8 | """ 9 | 10 | from datetime import datetime 11 | import sys 12 | import asyncio 13 | import json 14 | import websockets 15 | import numpy as np 16 | import scipy.signal as sig 17 | #import pprint 18 | 19 | #print("loading obspy...") 20 | from obspy.io.segy.segy import _read_su 21 | #print("... done.") 22 | 23 | # FIXME - this only works with 3.5, not 3.6 24 | if sys.version_info == (3, 6): 25 | raise "** must use python v3.4 or 3.5" 26 | 27 | 28 | 29 | class Segy(object): 30 | """Local version of the SU/SEGY file""" 31 | def __init__(self): 32 | self.filename = None 33 | self.hdrs = None 34 | self.traces = None 35 | self.nsamps = 0 36 | self.dt = 0 37 | self.segyfile = None 38 | 39 | def getPSD(self, ens): 40 | """ 41 | Calculate and return average power spectral density for ensemble 42 | `ens` using Welch's method 43 | """ 44 | print('in getpsd, ens=', ens, self) 45 | if not self.segyfile: 46 | raise Exception("File not opened") 47 | enstrcs = [t for t in self.segyfile.traces if t.header.original_field_record_number == ens] 48 | psds = [sig.welch(t.data, fs=1/self.dt, scaling='spectrum') for t in enstrcs] 49 | # psds is [(f,psd), (f,psd)...] 50 | freqs = psds[0][0] 51 | psdonly = [p for (f, p) in psds] 52 | psdonly = np.array(psdonly).transpose() 53 | psdavg = np.mean(psdonly, 1) 54 | print(freqs.shape, psdavg.shape) 55 | return(freqs, psdavg) 56 | 57 | def getTrc(self, trcNum=0): 58 | """ Get trace `trcNum` and return a python dict""" 59 | pass 60 | 61 | segy = Segy() 62 | #print(segy) 63 | 64 | def getTrc(t, headonly=True, decimate=False, t1=-1, t2=-1, flo=None, fhi=None): 65 | """Convert a segy trace to a python dict 66 | 67 | An obspy SegyTrace t is converted to a python dict with optional 68 | filtering of the data. 69 | 70 | Parameters: 71 | ----------- 72 | t : SegyTrc object 73 | headonly : bool, optional 74 | If True, only return the trace headers, with `samps` set to an 75 | empty array. 76 | flo, fhi : number 77 | If `flo` and `fhi` are specified, filter the data before return. 78 | t1, t2 : number 79 | If `t1` and `t2` are specified > 0, window the data and return only those 80 | 81 | Returns: 82 | -------- 83 | dict 84 | Dictionary with keys `tracl`, `tracr`, `ffid`, `offset`, and `samps` 85 | (at a minimum; see the code for the full list) 86 | 87 | """ 88 | dt = t.header.sample_interval_in_ms_for_this_trace/(1000.*1000.) 89 | nsamps = t.header.number_of_samples_in_this_trace 90 | samps = [] 91 | 92 | ampscale = 1 93 | if not headonly: 94 | # because I used headonly in the original open, 95 | # the data are read anew from file every time data is 96 | # referenced (check this) 97 | d = t.data # read from file? 98 | #print('in gettrc', decimate) 99 | if decimate: 100 | #print('decimate', decimate, dt) 101 | try: 102 | d = sig.decimate(d, q=10, zero_phase=True) 103 | dt = dt*10 104 | except: 105 | print('decimate failed') 106 | return 107 | 108 | if t1 > 0 and t2 > 0: 109 | i1 = int(t1/dt) 110 | i2 = int(t2/dt) 111 | d = d[i1:i2] 112 | tarr=np.arange(i1*dt,(i2+1)*dt,dt) 113 | #print(i1,i2, dt,len(d), len(tarr)) 114 | 115 | else: 116 | tarr=np.arange(0,nsamps*dt,dt) 117 | 118 | max = np.max(d) 119 | min = np.min(d) 120 | ampscale = (max-min) 121 | if ampscale != 0: 122 | d /= (max-min) 123 | 124 | #print("amp", ampscale, max, min) 125 | 126 | # if a filter is requested... 127 | if flo and fhi: 128 | fnyq = 0.5/dt 129 | #print(flo, fhi, fnyq) 130 | if flo>fnyq or fhi>fnyq: 131 | raise Exception('invalid frequencies') 132 | b,a = sig.butter(8,[flo/fnyq, fhi/fnyq], 'bandpass') 133 | # print(flo, fhi, fnyq, b, a) 134 | 135 | y = sig.filtfilt(b, a, d) 136 | # .tolist needed so that json can serialize it (it can't hand numpy arrays) 137 | varr = y.tolist() 138 | else: # otherwise use raw data 139 | varr = d.tolist() 140 | 141 | # create the samps array 142 | samps = [{'t':t,'v':v} for (t,v) in zip(tarr,varr)] 143 | 144 | trc = {"tracl": t.header.trace_sequence_number_within_line, 145 | "tracr": t.header.trace_sequence_number_within_segy_file, 146 | "ffid": t.header.original_field_record_number, 147 | "offset": t.header.distance_from_center_of_the_source_point_to_the_center_of_the_receiver_group, 148 | "ampscale" : "{}".format(ampscale), 149 | "nsamps": len(samps), 150 | "dt": dt, 151 | "samps": samps} 152 | return trc 153 | 154 | def handleMsg(msgJ): 155 | """Process the message in msgJ. 156 | 157 | Parameters: 158 | msgJ: dict 159 | Dictionary with command sent from client 160 | 161 | Returns: 162 | string 163 | JSON string with command response 164 | 165 | Commands are of the form: 166 | {'cmd' : 'getCCC', 'param0': 'param0val', ...} 167 | 168 | Response is a string of the form (note that JSON is picky that keys 169 | and strings should be enclosed in double quotes: 170 | '{"cmd" : "getCmd", "cmd" : ""}' 171 | 172 | {'cmd':'getHello'} -> {"cmd":"getHello", "hello": "world"} 173 | 174 | {'cmd':'getSegyHdrs', filename: f} -> 175 | {"cmd":"getSegyHdrs", "segyhdrs": 176 | {ns:nsamps, dt:dt: hdrs:[hdr1, hdr2...]}} 177 | 178 | FIXME FIXME - this currently returns "segy", not "ensemble" as the key 179 | WARNING - you must call getSegyHdrs first 180 | flo and fhi are optional. If they are not present, no filtering 181 | {'cmd':'getEnsemble', filename:f, ensemble:n, [flo:flo, fhi: fhi]} -> 182 | {"cmd":"getEnsemble", "segy": 183 | {ns:nsamps, dt:dt: traces:[trc1, trc2...]}} 184 | """ 185 | print('msgJ: {}'.format(msgJ)) 186 | if msgJ['cmd'].lower() == 'getsegyhdrs': 187 | filename = msgJ['filename'] 188 | print('getting segyhdr >{}<, filename: {}'.format(msgJ, filename)) 189 | 190 | t0 =datetime.now() 191 | if segy.filename != filename: 192 | # new file - open it 193 | try: 194 | s = _read_su(filename, headonly=True) 195 | segy.filename = filename 196 | segy.segyfile = s 197 | except: 198 | ret = json.dumps({"cmd":"readSegy", "error": "Error reading file {}".format(filename)}) 199 | return ret 200 | print("ntrcs = {}".format(len(segy.segyfile.traces))) 201 | 202 | hdrs = [getTrc(t, headonly=True) for t in segy.segyfile.traces] 203 | nsamps = segy.segyfile.traces[0].header.number_of_samples_in_this_trace 204 | dt = segy.segyfile.traces[0].header.sample_interval_in_ms_for_this_trace/(1000.*1000.) 205 | segy.nsamps = nsamps 206 | segy.dt = dt 207 | segy.hdrs = hdrs 208 | 209 | ret = json.dumps({"cmd": "readSegyHdrs", 210 | "segy" : json.dumps({"dt":dt, "ns":nsamps, 211 | "filename": segy.filename, 212 | "hdrs":hdrs})}) 213 | return ret 214 | 215 | if msgJ['cmd'].lower() == 'getensemble': 216 | print('getting ens', msgJ) 217 | if segy.segyfile is None: 218 | ret = json.dumps({"cmd":"getEnsemble", "error": "Error reading ensemble"}) 219 | return ret 220 | 221 | decimate = False 222 | try: 223 | ens = int(msgJ['ensemble']) 224 | try: 225 | decimate = msgJ['decimate'] 226 | print('dec t', decimate) 227 | except: 228 | decimate=False 229 | print('dec f', decimate) 230 | 231 | try: 232 | t1 = float(msgJ['t1']) 233 | t2 = float(msgJ['t2']) 234 | except: 235 | t1=-1 236 | t2=-1 237 | try: 238 | flo = float(msgJ['flo']) 239 | fhi = float(msgJ['fhi']) 240 | print(flo, fhi) 241 | traces = [getTrc(t,headonly=False, decimate=decimate, t1=t1, t2=t2, flo=flo, fhi=fhi) for t in segy.segyfile.traces if t.header.original_field_record_number == ens] 242 | except: 243 | print('err filt') 244 | traces = [getTrc(t,headonly=False,decimate=decimate, t1=t1,t2=t2) for t in segy.segyfile.traces if t.header.original_field_record_number == ens] 245 | except: 246 | print('err ens', ens, decimate) 247 | ret = json.dumps({"cmd":"getEnsemble", "error": "Error reading ensemble number"}) 248 | return ret 249 | print("ens = {} ntrc={}".format(ens, len(traces))) 250 | # dt/nsamps could change from the original due to decimation 251 | dt = traces[0]["dt"] 252 | nsamps = traces[0]["nsamps"] 253 | print('dt, nsamps', dt, nsamps) 254 | #print(json.dumps(traces[0])) 255 | ret = json.dumps({"cmd": "segy", 256 | "segy" : json.dumps({"dt":dt, "ns":nsamps, 257 | "filename": segy.filename, 258 | "traces":traces})}) 259 | return ret 260 | 261 | if msgJ["cmd"].lower() == "getpsd": 262 | if segy.segyfile is None: 263 | ret = json.dumps({"cmd":"getpsd", 264 | "error": "Error reading ensemble"}) 265 | return ret 266 | try: 267 | ens = int(msgJ['ensemble']) 268 | print(ens) 269 | (f,psd) = segy.getPSD(ens) 270 | except: 271 | print('err ens/psd') 272 | ret = json.dumps({"cmd":"getPSD", 273 | "error": "Error reading ensemble number"}) 274 | return ret 275 | 276 | # FIXME - this should be cmd:getPSD, psd:{...} 277 | return json.dumps({"cmd":"getPSD", "ensemble":ens, 278 | "filename": segy.filename, 279 | "freqs":f.tolist(), "psd":psd.tolist()}) 280 | 281 | if msgJ["cmd"].lower() == "gethello": 282 | ret = json.dumps({"cmd": "hello", "hello": "world"}) 283 | return ret 284 | 285 | #async def api(ws, path): 286 | # all this is stolen from the websockets tutorial. 287 | @asyncio.coroutine 288 | def api(ws, path): 289 | while True: 290 | try: 291 | # msg = await ws.recv() 292 | # get a websockets string 293 | msg = yield from ws.recv() 294 | print('msg', msg) 295 | try: 296 | msgJ = json.loads(msg) 297 | except json.decoder.JSONDecodeError: 298 | print("error decoding msg >{}<".format(msg)) 299 | continue 300 | 301 | print("got json msgJ >{}<".format(msgJ)) 302 | # and handle it... 303 | retJ = handleMsg(msgJ) 304 | 305 | #print(retJ) 306 | # and return the response to the client 307 | yield from ws.send(retJ) 308 | # await ws.send(retJ) 309 | 310 | except websockets.ConnectionClosed: 311 | print('connection closed') 312 | return 313 | 314 | ss = websockets.serve(api, 'localhost', 9191) 315 | # all this is stolen from the websockets tutorial. 316 | try: 317 | print("ready...") 318 | sys.stdout.flush() 319 | asyncio.get_event_loop().run_until_complete(ss) 320 | asyncio.get_event_loop().run_forever() 321 | except KeyboardInterrupt: 322 | print("bye") 323 | 324 | -------------------------------------------------------------------------------- /app/d3seis.js: -------------------------------------------------------------------------------- 1 | /* -*- indent-tabs-mode:nil; -*- 2 | * d3-ricker will generate 2 windows: the main and zoom windows 3 | * and allow picking 4 | */ 5 | 6 | /* jshint latedef:nofunc */ 7 | /*eslint-env node*/ 8 | /*global d3*/ 9 | /*eslint-no-undef: "error"*/ 10 | /*eslint no-console: ["error", {allow: ["log"]}]*/ 11 | angular.module('psqlApp') 12 | .directive('d3Seis', [function() { 13 | function link(scope, element, attr) { 14 | 'use strict'; 15 | // size of the focus window (500x600) 16 | var margins2 = {left: 50,right: 30,top: 30,bottom: 30}, 17 | w2 = 800 - margins2.left - margins2.right, 18 | h2 = 600 - margins2.top - margins2.bottom; 19 | 20 | var data; 21 | var dt,traces, currEns, ensIdx, pickedTraces = []; 22 | var firstTrc, lastTrc, ntrcs, npts, cursI, cursT, cursTrc; 23 | var tracesByEnsemble; 24 | var displayScale = 1; // scale the traces in the "focus" panel by this 25 | 26 | /* init - label traces with unique id and sort */ 27 | // generate the y- (time) scale for the main window 28 | var tmin, tmax; 29 | var tScale = d3.scaleLinear(); 30 | var tScale2 = d3.scaleLinear(); 31 | var oScale2 = d3.scaleLinear() 32 | .range([0,w2]); 33 | 34 | var firstTime = true; 35 | 36 | //console.log(data.traces); 37 | data = scope.data; 38 | init(); 39 | 40 | console.log('dt = ', dt, 'ntrcs =', ntrcs, 'npts =', npts); 41 | 42 | /* form of the data: 43 | self.data = {dt: dt, 44 | traces: [ 45 | {ffid: 0, offset: x, samps:[{t:..,v:..},{t:..,v:..},..]} 46 | {ffid: 0, offset: y, samps:[{t:..,v:..},{t:..,v:..},..]} 47 | {ffid: 0, offset: z, samps:[{t:..,v:..},{t:..,v:..},..]} 48 | ... 49 | ]}; 50 | 51 | */ 52 | 53 | // generate the y- (time) scale for the main window 54 | tmin = d3.min(traces[0].samps, function(d){return d.t;}); 55 | tmax = d3.max(traces[0].samps, function(d){return d.t;}); 56 | 57 | console.log('t', traces[0], tmin, tmax); 58 | 59 | var tst0 = traces[0].samps.map(x=>x.v); // get just values 60 | var intrp = d3.interpolateBasis(tst0); 61 | var tst1 = d3.quantize(intrp, tst0.length*10); // over samp & spline interplate 62 | var zc = tst1.map((x)=> Math.sign(x) < 0 ? -1 : 1); 63 | 64 | 65 | /* the core of the display is here, and repeated 4 times - 2 sets 66 | * of `lines` in the main and focus windows, and 2 sets of 67 | * `areas`. The logic is the same in all cases: 68 | * - .selectAll to get a set of lines 69 | * - .data "join" the selection with the traces 70 | * (with the 2nd argument to make them unique with .id) 71 | * - draw the line with the 'd' attr. 72 | * - shift and shrink it to its right location 73 | */ 74 | 75 | var focus = d3.select(element[0]) 76 | .append('svg') 77 | .attr('width', w2 + margins2.left + margins2.right) 78 | .attr('height', h2 + margins2.top + margins2.bottom) 79 | .append('g') 80 | .attr('transform', 'translate(' + margins2.left + ',' + margins2.top + ')'); 81 | 82 | var vScale2 = d3.scaleLinear().range([0, w2]); 83 | tScale2 = d3.scaleLinear().range([0, h2]); 84 | var oAxis2 = d3.axisTop(oScale2); 85 | 86 | tScale2.domain([tmin, tmax]); 87 | vScale2.domain([-1, 1]); 88 | 89 | var tAxis2 = d3.axisLeft(tScale2); 90 | 91 | // stolen from bl.ocks.org 92 | // gradient fill from -1 to 1 93 | // from 0->25%, red, from 25-50%, gradient from red to white, and so on. 94 | focus.append('linearGradient') 95 | .attr('id', 'area-gradient') 96 | .attr('gradientUnits', 'userSpaceOnUse') 97 | .attr('y1', 0).attr('x1', vScale2(-1)) 98 | .attr('y2', 0).attr('x2', vScale2(1)) 99 | .selectAll('stop') 100 | .data([{ 'offset': '0%', color: 'red'}, 101 | { 'offset': '25%', color: 'red'}, 102 | { 'offset': '50%', color: 'white'}, 103 | { 'offset': '50%', color: 'white'}, 104 | { 'offset': '75%', color: 'blue'}, 105 | { 'offset': '100%', color: 'blue'} 106 | ]) 107 | .enter() 108 | .append('stop') 109 | .attr('offset', function(d) {return d.offset;}) 110 | .attr('stop-color', function(d) {return d.color;}); 111 | 112 | // slightly different gradient (FIXME - needed?) 113 | // 0-50%, red->white, 50-100%, white->blue 114 | focus.append('linearGradient') 115 | .attr('id', 'focus-area-gradient') 116 | .attr('gradientUnits', 'userSpaceOnUse') 117 | .attr('y1', 0).attr('x1', vScale2(-1)) 118 | .attr('y2', 0).attr('x2', vScale2(1)) 119 | .selectAll('stop') 120 | .data([{'offset': '0%',color: 'red'}, 121 | {'offset': '50%',color: 'white'}, 122 | {'offset': '100%',color: 'blue'}]) 123 | .enter() 124 | .append('stop') 125 | .attr('offset', function(d) {return d.offset;}) 126 | .attr('stop-color', function(d) {return d.color;}); 127 | 128 | /* Focus 129 | * plot the zoomed in data 130 | */ 131 | 132 | var focusLine = d3.line() 133 | // .curve(d3.curveMonotoneY) 134 | .x(function(d) { return vScale2(displayScale * d.v);}) 135 | .y(function(d) { return tScale2(d.t);}) 136 | .curve(d3.curveBasis); 137 | //.curve(d3.curveCardinal.tension(0.3)); 138 | 139 | var focusArea = d3.area() 140 | .y(function(d) {return tScale2(d.t);}) 141 | .x0(vScale2(0)) 142 | // .curve(d3.curveMonotoneY) 143 | .x1(function(d) {return vScale2(displayScale * d.v);}); 144 | 145 | 146 | 147 | var cursorText; 148 | var cText = focus.append('g') 149 | .append('text') 150 | .attr('id', 'ctext') 151 | .attr('class', 'ctext') 152 | .attr('transform', 'translate(' + w2 / 2 + ',' + (h2 + margins2.top) + ')') 153 | .text(cursorText); 154 | 155 | var cursor = focus.append('g') 156 | .style('display', null); 157 | cursor.append('circle') 158 | .attr('id', 'cursor') 159 | .attr('class', 'cursor') 160 | .attr('r', 6); 161 | //.attr('cy', tScale2(cursT)) 162 | //.attr('cx', vScale2(displayScale * traces[cursTrc].samps[cursI].v) / ntrcsFoc) 163 | //.attr('transform', 'translate(' + xofsFoc + ',0)'); 164 | 165 | /* mark line */ 166 | var marks = []; 167 | var mark = focus.append('g') 168 | .attr('id', 'markPts'); 169 | var markline = focus.append('g') 170 | .append('path') 171 | .attr('class', 'markline') 172 | .attr('id', 'markLine'); 173 | var marklinefn = d3.line() 174 | .x(function(d){ return w2 / ntrcsFoc * (0.5 + d.tracens - firstTrc);}) 175 | .y(function(d){ return tScale2(d.markT);}); 176 | 177 | /* draw focus traces */ 178 | 179 | focus.append('defs').append('clipPath') 180 | .attr('id', 'clip') 181 | .append('rect') 182 | .attr('width', w2) 183 | .attr('height', h2); 184 | 185 | var clipping = focus.append('g') 186 | .attr('clip-path', 'url(#clip)'); 187 | 188 | // console.log(vScale2.domain(), vScale2.range()); 189 | firstTrc = 0; 190 | lastTrc = traces.length - 1; 191 | var ntrcsFoc = lastTrc - firstTrc + 1; 192 | var xofsFoc; 193 | 194 | 195 | var updateFocusLines = function() { 196 | ntrcsFoc = lastTrc - firstTrc + 1; 197 | //console.log('update lines', lastTrc, firstTrc, ntrcsFoc); 198 | var l = d3.selectAll('.flineg') 199 | .selectAll('.fline') 200 | .data(traces.slice(firstTrc, lastTrc + 1), function(d) { 201 | return d.id; 202 | }); 203 | // draw the current set of lines 204 | // update existing 205 | // console.log(l); 206 | l.transition() 207 | .attr('d', function(d) { 208 | return focusLine(d.samps); 209 | }) 210 | .attr('transform', function(d, i) { 211 | xofsFoc = (w2 / ntrcsFoc * i); 212 | // console.log('update line',i, xofsFoc); 213 | return 'translate(' + xofsFoc + ',0) scale(' + 1 / ntrcsFoc + ',1)'; 214 | }); 215 | // remove any old ones 216 | l.exit().remove(); 217 | 218 | // and add new ones... 219 | l.enter() 220 | .append('path') 221 | .attr('class', 'fline') 222 | .attr('d', function(d) { 223 | return focusLine(d.samps); 224 | }) 225 | .attr('transform', function(d, i) { 226 | xofsFoc = (w2 / ntrcsFoc * i); 227 | return 'translate(' + xofsFoc + ',0) scale(' + 1 / ntrcsFoc + ',1)'; 228 | }); 229 | }; 230 | 231 | var updateFocusAreas = function() { 232 | ntrcsFoc = lastTrc - firstTrc + 1; 233 | var l = d3.selectAll('.fareag') 234 | .selectAll('.ffills') 235 | .data(traces.slice(firstTrc, lastTrc + 1), function(d) { 236 | return d.id; 237 | }); 238 | l.attr('fill-opacity', function(d,i) { 239 | // console.log('fill', i, cursTrc-firstTrc); 240 | return ((i===(cursTrc-firstTrc)) ? 1 : 0.3); 241 | }) 242 | .attr('d', function(d) {return focusArea(d.samps);}) 243 | .attr('transform', function(d, i) { 244 | xofsFoc = (w2 / ntrcsFoc * i); 245 | return 'translate(' + xofsFoc + ',0) scale(' + 1 / ntrcsFoc + ',1)'; 246 | }); 247 | l.enter() 248 | .append('path') 249 | .attr('class', 'ffills') 250 | .attr('d', function(d) {return focusArea(d.samps);}) 251 | .attr('fill-opacity', function(d,i) {return ((i===(cursTrc-firstTrc)) ? 1 : 0.3);}) 252 | .attr('transform', function(d, i) { 253 | xofsFoc = (w2 / ntrcsFoc * i); 254 | return 'translate(' + xofsFoc + ',0) scale(' + 1 / ntrcsFoc + ',1)'; 255 | }); 256 | l.exit().remove(); 257 | }; 258 | 259 | var fareag = clipping.append('g') 260 | .attr('class', 'fareag'); 261 | updateFocusAreas(); 262 | 263 | var flineg = clipping.append('g') 264 | .attr('class', 'flineg'); 265 | updateFocusLines(); 266 | focus.append('g') 267 | .attr('class', 'axis x-axis') 268 | .call(oAxis2.ticks(5)); 269 | // .call(vAxis2) 270 | focus.append('g') 271 | .attr('class', 'axis y-axis') 272 | .call(tAxis2); 273 | 274 | /* define the pick line vars */ 275 | var pickg = focus.append('g'); 276 | 277 | var updateCursor = function() { 278 | // redraw the cursor 279 | ntrcsFoc = lastTrc - firstTrc + 1; 280 | xofsFoc = (w2 / ntrcsFoc * (cursTrc - firstTrc)); 281 | //sampNo = bisectT(traces[cursTrc].samps, cursT); 282 | cursT = traces[cursTrc].samps[cursI].t; 283 | // console.log('updateCursor', xofsFoc, sampNo, cursI); 284 | cursor.select('#cursor') 285 | .attr('cy', tScale2(cursT)) 286 | .attr('cx', vScale2(displayScale * traces[cursTrc].samps[cursI].v) / ntrcsFoc) 287 | .attr('transform', 'translate(' + xofsFoc + ',0)'); 288 | 289 | cursorText = sprintf('ensemble %s trace: %d time: %.3f value: %+.3f', 290 | currEns, cursTrc, cursT, traces[cursTrc].samps[cursI].v * traces[cursTrc].ampscale); 291 | focus.select('#ctext') 292 | .text(cursorText); 293 | }; 294 | 295 | var updatePicks = function() { 296 | // and the picks 297 | var r = vScale2.range(); 298 | ntrcsFoc = lastTrc - firstTrc + 1; 299 | var trchtFoc = Math.abs(r[0] - r[1]) / ntrcsFoc; 300 | // console.log(pickedTraces, currEns, 301 | //pickedTraces.filter(function(d){return d.ens==+currEns;})); 302 | // only choose those picks that are part of this ensemble 303 | // the +currEns converts "1" to 1 304 | var p = pickg.selectAll('.picks') 305 | .data(pickedTraces.filter(function(d){return d.ens===+currEns;}), 306 | function(d) {return d.tracr;}); 307 | 308 | //console.log('update picks', p); 309 | p.exit() 310 | .remove(); 311 | p.transition() 312 | .duration(1000) 313 | .attr('y', function(d) {return tScale2(d.pickT);}) 314 | .attr('x', vScale2(0)/ntrcsFoc - trchtFoc/2) 315 | .attr('width', trchtFoc) 316 | .attr('transform', function(d) { 317 | xofsFoc = (w2 / ntrcsFoc * (d.tracens - firstTrc)); 318 | return 'translate(' + xofsFoc + ',0)'; 319 | }); 320 | 321 | p.enter() 322 | .append('rect') 323 | .attr('class', 'picks') 324 | .attr('y', function(d) {return tScale2(d.pickT);}) 325 | .attr('x', vScale2(0)/ntrcsFoc - trchtFoc/2) 326 | .attr('width', trchtFoc) 327 | .attr('height', 2) 328 | .attr('transform', function(d) { 329 | xofsFoc = (w2 / ntrcsFoc * (d.tracens - firstTrc)); 330 | return 'translate(' + xofsFoc + ',0)'; 331 | }); 332 | }; 333 | 334 | cursT = traces[cursTrc].samps[cursI].t; 335 | xofsFoc = w2 / ntrcsFoc * (cursTrc - firstTrc); 336 | 337 | /* handle key interactions */ 338 | var idx; 339 | d3.select('body') 340 | .on('keydown', function() { 341 | //console.log(d3.event.key, d3.event.code); 342 | // scale traces up and down 343 | if (d3.event.key === 'y' || d3.event.key === 'Y') { 344 | var scl = d3.event.shiftKey ? 0.5 : 2; 345 | 346 | displayScale *= scl; 347 | updateFocusLines(); 348 | updateFocusAreas(); 349 | } 350 | 351 | switch (d3.event.key) { 352 | case 'a': 353 | autop(traces[cursTrc], cursI, dt); 354 | break; 355 | 356 | // move cursor up/down 357 | case 'j': 358 | case 'k': 359 | case 'J': 360 | case 'K': 361 | case 'ArrowUp': 362 | case 'ArrowDown': 363 | 364 | // calculate the new cursor position 365 | var td = tScale2.domain(); 366 | var tdlen = td[1] - td[0]; 367 | var tdi = td.map(function(d) {return Math.floor(d/dt);}); 368 | var tdilen10 = Math.floor((tdi[1] - tdi[0])/10); 369 | tdilen10 = tdilen10===0 ? 1 : tdilen10; 370 | 371 | var incr = d3.event.shiftKey ? tdilen10: 1; 372 | incr *= (d3.event.code === 'KeyK' || d3.event.code == 'ArrowUp') ? -1 : 1; 373 | cursI += incr; 374 | if(cursI<0) {cursI = 0;} 375 | if(cursI>=npts){cursI = npts-1;} 376 | cursT = traces[cursTrc].samps[cursI].t; 377 | 378 | // if it has moved off screen, recalculate the domain 379 | if (cursT <= td[0] || cursT >= td[1]) { 380 | if (cursT <= td[0]) { 381 | td[0] -= tdlen/4; 382 | if(td[0] < tmin) {td[0] = tmin;} 383 | td[1] = td[0] + tdlen - dt; 384 | } else { 385 | td[1] += tdlen/4; 386 | if(td[1] > tmax) {td[1] = tmax - dt;} 387 | td[0] = td[1] - tdlen + dt; 388 | } 389 | 390 | //console.log('td1',td); 391 | tScale2.domain(td); 392 | focus.select('.y-axis') 393 | .transition() 394 | .duration(1000) 395 | .ease(d3.easeQuad) 396 | .call(tAxis2); 397 | focus.selectAll('.fline') 398 | .transition() 399 | .duration(1000) 400 | .ease(d3.easeQuad) 401 | .attr('d', function(d) { 402 | return focusLine(d.samps); 403 | }); 404 | focus.selectAll('.ffills') 405 | .transition() 406 | .duration(1000) 407 | .ease(d3.easeQuad) 408 | .attr('d', function(d) { 409 | return focusArea(d.samps); 410 | }); 411 | updatePicks(); 412 | } 413 | // and redraw the cursor 414 | updateCursor(); 415 | break; 416 | 417 | // move cursor to prev/next trace 418 | case 'h': 419 | case 'l': 420 | case 'H': 421 | case 'L': 422 | case 'ArrowLeft': 423 | case 'ArrowRight': 424 | // console.log('hl key', d3.event.code, d3.event.shiftKey, cursTrc, firstTrc, lastTrc); 425 | // calculate the new trace for the cursor 426 | var trcincr = d3.event.shiftKey ? 10 : 1; 427 | trcincr *= (d3.event.code === 'KeyL' || d3.event.code === 'ArrowRight') ? 1 : -1; 428 | cursTrc = Math.min(cursTrc + trcincr, lastTrc); 429 | cursTrc = Math.max(cursTrc, firstTrc); 430 | 431 | //var i = bisectT(traces[cursTrc].samps, cursT); 432 | xofsFoc = (w2 / ntrcsFoc * (cursTrc - firstTrc)); 433 | //console.log('jk key', d3.event.code, d3.event.shiftKey, cursTrc, firstTrc, lastTrc, i, xofsFoc); 434 | 435 | updateCursor(); 436 | 437 | // make the cursor-trace opaque and dim other traces 438 | fareag.selectAll('.ffills') 439 | .attr('fill-opacity', function(d,i) { 440 | // console.log('hl', i, cursTrc, firstTrc); 441 | return ((i===cursTrc-firstTrc) ? 1 : 0.3); 442 | }); 443 | break; 444 | 445 | // remove the current pick 446 | case 'd': 447 | // search for this trc in the pickedtraces 448 | // use the globally unique tracr header (FIXME-is this true?) 449 | idx = pickedTraces.map(function(d) {return d.tracr;}) 450 | .indexOf(traces[cursTrc].tracr); 451 | if(idx >= 0) {pickedTraces.splice(idx,1);} // remove it 452 | 453 | // console.log(idx, pickedTraces); 454 | updatePicks(); 455 | 456 | // tell the controller about the changes. 457 | scope.setpicks({picks: pickedTraces}); 458 | break; 459 | 460 | // pick the current value 461 | case 'p': 462 | var r = vScale2.range(); 463 | ntrcsFoc = lastTrc - firstTrc + 1; 464 | var trchtFoc = (r[1] - r[0]) / ntrcsFoc; 465 | 466 | var trc = traces[cursTrc]; 467 | var pickT = trc.samps[cursI].t; 468 | var pickVal = trc.samps[cursI].v; 469 | var zc0 = -1, zc1 = -1, zc2 = -1, zc3=0; 470 | // If a pulse is xxx + - - - - + + + + + - - - - + xxx 471 | // zero crossings are: 0 1 2 3 472 | // search backwards for zc1 and then backwards for zc0 473 | for(var j=cursI; j>cursI-100; j--) { 474 | if(trc.samps[j].v * pickVal < 0) { 475 | zc1 = j; 476 | for(var k=zc1; k>zc1-100; k--) { 477 | if(trc.samps[k].v * pickVal > 0) { 478 | zc0 = k; 479 | break; 480 | } 481 | } 482 | break; 483 | } 484 | } 485 | for(var j=cursI; j 0) { 490 | zc3 = k; 491 | break; 492 | } 493 | break; 494 | } 495 | } 496 | } 497 | var pkmaxI = null, 498 | pkmax; 499 | if(zc1 > 0 && zc2 > 0) { 500 | var local = trc.samps.slice(zc1, zc2); 501 | // scan finds index of peak 502 | var localIdx = d3.scan(local, function(a,b){return b.v-a.v;}); 503 | pkmax = local[localIdx]; 504 | pkmaxI = localIdx + zc1; 505 | } 506 | 507 | // console.log(pickVal, cursI, cursT, zc0, zc1, zc2, zc3, pkmaxI, pkmax); 508 | //console.log(pickVal, cursI, cursT, trchtFoc); 509 | 510 | var ampscale = trc.ampscale; 511 | // deep copy of trc.samps (other methods only copy a reference) 512 | var samps = JSON.parse(JSON.stringify(trc.samps.slice(cursI-100, cursI+100))); 513 | samps.forEach(function(d){d.v *= ampscale;}); 514 | var newpk = { 515 | ffid: trc.ffid, 516 | offset: trc.offset, 517 | tracr: trc.tracr, 518 | id: trc.id, 519 | // used to plot the pick in x 520 | tracens: trc.tracens, 521 | ens: trc.ffid, 522 | pickT: pickT, 523 | pickIdx: cursI, 524 | pickVal: trc.samps[cursI].v * ampscale, 525 | autopickIdx: pkmaxI, 526 | autopickVal: pkmax.v * ampscale, 527 | autopickT: pkmax.t, 528 | // copy the 100 samps around the pick? 529 | samps: samps 530 | }; 531 | 532 | // am I re-picking? Search for cursTrc id in pickedTraces 533 | // that is unique even if offset or tracr etc aren't set? 534 | idx=-1; 535 | var id=trc.id; 536 | idx = pickedTraces.map(function(d) {return d.id;}) 537 | .indexOf(id); 538 | if (idx >= 0) { // found it... 539 | pickedTraces[idx] = newpk; 540 | } else { // new pick 541 | pickedTraces.push(newpk); 542 | } 543 | /* sort...*/ 544 | pickedTraces.sort(function(a,b) {return a.tracens - b.tracens;}); 545 | 546 | //console.log(pickedTraces); 547 | // tell the controller about it... 548 | scope.setpicks({picks: pickedTraces}); 549 | //console.log('pick', firstTrc, pickedTraces); 550 | 551 | /* draw a "mark" line between picks */ 552 | var bisectM = d3.bisector(function(d) {return d.tracens;}).left; 553 | marks = []; 554 | for(var i=0; i_ 720 | //data.traces.forEach(function(d, i) { 721 | //d.id = cryp.randomBytes(5).toString('hex'); 722 | //d.id = d.ffid + '_' + i; 723 | //d.tracens = i; 724 | //}); 725 | //console.log(traces[0], traces[1]) 726 | 727 | // group into ensembles 728 | tracesByEnsemble = d3.nest() 729 | .key(function(d) { 730 | return d.ffid; 731 | }) 732 | .entries(data.traces); 733 | // returns [{key:'0', values:[trc0, trc1, ...]}] 734 | 735 | // add a "trace number within ensemble" header word 736 | // and uniquify the trace by labeling with _tracens 737 | for(var i=0; i