Seisplotjs Tutorial

Realtime Data

See it live in tutorial7.html .

Now for something else completely different. A realtime plot can be a crowd pleaser, but it is considered very rude to rapidly request the same seismogram over and over from the FDSN dataselect web service, and so we will use a web socket to the IRIS ringserver using the DataLink protocol. If you run your own ringserver and wish to configure it to allow websocket access, some additional information is here .

First we need to set up a bunch of variables we will use to keep track of the realtime data. The timerInterval is set so that hopefully it updates the display just often enough to move the image over by one pixel. We will also need an error handling function.


const matchPattern = `CO_BIRD_00_HH./MSEED`;
document.querySelector('span#channel').textContent = matchPattern;
const duration = sp.luxon.Duration.fromISO('PT5M');
const timeWindow = new sp.util.durationEnd(duration, sp.luxon.DateTime.utc());
const seisPlotConfig = new sp.seismographconfig.SeismographConfig();
seisPlotConfig.wheelZoom = false;
seisPlotConfig.isYAxisNice = false;
seisPlotConfig.linkedTimeScale.offset = sp.luxon.Duration.fromMillis(-1*duration.toMillis());
seisPlotConfig.linkedTimeScale.duration = duration;
seisPlotConfig.linkedAmplitudeScale = new sp.scale.IndividualAmplitudeScale();
seisPlotConfig.doGain = true;
let graphList = new Map();
let numPackets = 0;
let paused = false;
let stopped = true;
let redrawInProgress = false;
let realtimeDiv = document.querySelector("div#realtime");
let rect = realtimeDiv.getBoundingClientRect();
let timerInterval = duration.toMillis()/
                    (rect.width-seisPlotConfig.margin.left-seisPlotConfig.margin.right);
while (timerInterval < 50) { timerInterval *= 2;}

const errorFn = function(error) {
  console.assert(false, error);
  if (datalink) {datalink.close();}
  document.querySelector("p#error").textContent = "Error: "+error;
};

And a function to handle each datalink packet as it arrives. In this case all datalink packets should contain a single miniseed record, but there is nothing in the datalink protocol that prevents sending other types of data as the payload.


const packetHandler = function(packet) {
  if (packet.isMiniseed()) {
    numPackets++;
    document.querySelector("span#numPackets").textContent = numPackets;
    let seisSegment = sp.miniseed.createSeismogramSegment(packet.asMiniseed());
    const codes = seisSegment.codes();
    let seisPlot = graphList.get(codes);
    if ( ! seisPlot) {
        let seismogram = new sp.seismogram.Seismogram( [ seisSegment ]);
        let seisData = sp.seismogram.SeismogramDisplayData.fromSeismogram(seismogram);
        seisData.alignmentTime = sp.luxon.DateTime.utc();
        seisPlot = new sp.seismograph.Seismograph([seisData], seisPlotConfig);
        realtimeDiv.appendChild(seisPlot);
        graphList.set(codes, seisPlot);
        console.log(`new plot: ${codes}`)
      } else {
        seisPlot.seisData[0].append(seisSegment);
        seisPlot.recheckAmpScaleDomain();
      }
      seisPlot.draw();
  } else {
    console.log(`not a mseed packet: ${packet.streamId}`)
  }
};

Now we create the actual Datalink connection to the IRIS ringserver .


// wss://thecloud.seis.sc.edu/ringserver/datalink
// wss://rtserve.iris.washington.edu/datalink
const datalink = new sp.datalink.DataLinkConnection(
    "wss://rtserve.iris.washington.edu/datalink",
    packetHandler,
    errorFn);

Here is the timer that will periodically refresh the displays.


let timer = window.setInterval(function(elapsed) {
  if ( paused || redrawInProgress) {
    return;
  }
  redrawInProgress = true;
  window.requestAnimationFrame(timestamp => {
    try {
      const now = sp.luxon.DateTime.utc();
      graphList.forEach(function(graph, key) {
        graph.seisData.forEach(sdd => {
          sdd.alignmentTime = now;
        });
        graph.calcTimeScaleDomain();
        graph.calcAmpScaleDomain();
        graph.draw();
      });
    } catch(err) {
      console.assert(false, err);
    }
    redrawInProgress = false;
  });

  }, timerInterval);

We wire up the pause button.


document.querySelector("button#pause").addEventListener("click", function(evt) {
  togglePause( );
});

let togglePause = function() {
  paused = ! paused;
  if (paused) {
    document.querySelector("button#pause").textContent = "Play";
  } else {
    document.querySelector("button#pause").textContent = "Pause";
  }
}

And wire up the disconnect button


document.querySelector("button#disconnect").addEventListener("click", function(evt) {
  toggleConnect();
});

function addToDebug(message) {
  const debugDiv = document.querySelector("div#debug");
  if (!debugDiv) { return; }
  const pre = debugDiv.appendChild(document.createElement("pre"));
  const code = pre.appendChild(document.createElement("code"));
  code.textContent = message;
}

let toggleConnect = function() {
  stopped = ! stopped;
  if (stopped) {
    if (datalink) {
      datalink.endStream();
      datalink.close();
    }
    document.querySelector("button#disconnect").textContent = "Reconnect";
  } else {
    if (datalink) {
      datalink.connect()
      .then(serverId => {
        addToDebug(`id response: ${serverId}`);
        return datalink.match(matchPattern);
      }).then(response => {
        addToDebug(`match response: ${response}`)
        if (response.isError()) {
          addToDebug(`response is not OK, ignore... ${response}`);
        }
        return datalink.infoStatus();
      }).then(response => {
        addToDebug(`info status response: ${response}`);
        return datalink.infoStreams();
      }).then(response => {
        addToDebug(`info streams response: ${response}`)
        return datalink.positionAfter(timeWindow.start);
      }).then(response => {
        if (response.isError()) {
          addToDebug(`Oops, positionAfter response is not OK, ignore... ${response}`);
          // bail, ignore, or do something about it...
        }
        return datalink.stream();
      }).catch( function(error) {
        let errMsg = `${error}`;
        if (error.cause && error.cause instanceof sp.datalink.DataLinkResponse) {
          errMsg = `${error}, ${errMsg.cause}`;
        }
        addToDebug("Error: " +errMsg);
        console.assert(false, error);
      });
    }
    document.querySelector("button#disconnect").textContent = "Disconnect";
  }
}

And then we start it going!


toggleConnect();

See it live in tutorial7.html .

Previous: Helicorder

Next: ...and more