diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..6a30ef3
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,16 @@
+kind: pipeline
+type: docker
+name: default
+
+steps:
+ - name: deploy
+ image: docker:dind
+ commands:
+ - apk add --upgrade npm bash findutils rsync sed
+ - WORKDIR="/var/docker-web/apps/$DRONE_REPO_NAME"
+ - rm -rf $WORKDIR
+ - mkdir $WORKDIR
+ - rsync -av --exclude ./node_modules /drone/src/ $WORKDIR
+ - cd $WORKDIR
+ - npm ci
+ - bash /var/docker-web/src/cli.sh up $DRONE_REPO_NAME
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..acc5fc8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:lts-alpine
+
+# install simple http server for serving static content
+RUN npm install -g http-server
+
+# make the 'app' folder the current working directory
+WORKDIR /app
+
+# copy both 'package.json' and 'package-lock.json' (if available)
+COPY package*.json ./
+
+# install project dependencies
+RUN npm install
+
+# copy project files and folders to the current working directory (i.e. 'app' folder)
+COPY . .
+
+# build app for production with minification
+RUN npm run build
+
+EXPOSE 8080
+CMD [ "http-server", "dist" ]
diff --git a/README.md b/README.md
index 9f5fba0..572b909 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,18 @@
-# virages.io
+
+
+
+
+ virages
+
-## DESIGN / FEATURES :
+## BASE FILES
-- mobile first -> scroll first
-- FEAT :: zoom
-- FEAT :: blur / focus
-- TOOL :: weaver's glass
-- TOOL :: "duality" slider-mask
-- TOOL :: themes
-- TOOL :: specific metas (paint, COLORS (auto gen from photos ?), catégories or tags or both ? ), paper, paint, nuancier,
-- TOOL :: class by 'sketchbook, a sketchbook as a sticker'logo ET/OU a name.'
-- TOOL :: Layer; overlay several photos of the paint and add a cursor-switch, looking-glass, desktop light spot
-- VIDEO :: joconde traveling
-- TOOL :: influence desc
-- TOOL :: pistes de lectures alphabet (numérique et lettres)
+config.sh
+docker-compose.yml
+logo.svg
+
+## EXTRA FILES
+
+nginx.conf
+post-install.sh
+pre-install.sh
diff --git a/config.sh b/config.sh
new file mode 100755
index 0000000..cadd14d
--- /dev/null
+++ b/config.sh
@@ -0,0 +1,4 @@
+export DOMAIN="virages.io"
+export PORT="7835"
+export PORT_EXPOSED="8080"
+export REDIRECTIONS="" # example.$MAIN_DOMAIN->/route $MAIN_DOMAIN->url /route->/another-route /route->url
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100755
index 0000000..e1c31ac
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,20 @@
+services:
+
+ virages:
+ image: alpine
+ container_name: virages
+ restart: unless-stopped
+ ports:
+ - $PORT:$PORT_EXPOSED
+ volumes:
+ - "${MEDIA_DIR}:/mnt/media"
+ environment:
+ VIRTUAL_HOST: "${DOMAIN}"
+ LETSENCRYPT_HOST: "${DOMAIN}"
+ PUID: "${PUID}"
+ PGID: "${PGID}"
+
+networks:
+ default:
+ name: dockerweb
+ external: true
diff --git a/logo.svg b/logo.svg
new file mode 100644
index 0000000..8cff9dc
--- /dev/null
+++ b/logo.svg
@@ -0,0 +1,25 @@
+
diff --git a/src/assets/plugins/openseadragon-measure.js b/src/assets/plugins/openseadragon-measure.js
new file mode 100644
index 0000000..81d1f84
--- /dev/null
+++ b/src/assets/plugins/openseadragon-measure.js
@@ -0,0 +1,1358 @@
+/**
+ * DexieWrapper.js
+ *
+ * Database wrapper that helps access data via the plugin
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class DexieWrapper {
+
+ db; // Dexie.js object to access the database
+ plugin; // reference to the plugin
+
+ /**
+ * constructor
+ *
+ * Opens the database, will work whether it exists already or not
+ *
+ * @param {OSDMeasure} plugin
+ */
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.db = new Dexie("database");
+ this.db.version(3).stores({
+ measurements: `
+ id,
+ image,
+ p1x, p1y,
+ p2x, p2y,
+ name,
+ color`
+ });
+ this.db.open();
+ }
+
+ /**
+ * clear:
+ *
+ * Removes all entries from the database
+ */
+ clear() {
+ this.db.measurements.clear();
+ }
+
+ /**
+ * getAllMeasurements:
+ *
+ * Gets all the measurements from the database
+ *
+ * @returns a list of all stored measurements
+ */
+ async getAllMeasurements(imageIdentifier) {
+ let measurements = [];
+ // query all measurements related to the image
+ let result = await this.db.measurements.where("image").equals(imageIdentifier).toArray();
+ for (let i = 0; i < result.length; i++) {
+ let measurement = new Measurement(
+ new Point(result[i].p1x, result[i].p1y, result[i].color, this.plugin.fabricCanvas , this.imageIdentifier),
+ new Point(result[i].p2x, result[i].p2y, result[i].color, this.plugin.fabricCanvas , this.imageIdentifier),
+ result[i].name, result[i].color, this.plugin.conversionFactor, this.plugin.units, this.plugin.fabricCanvas ,this.imageIdentifier
+ );
+ measurement.id = result[i].id;
+ measurements.push(measurement);
+ }
+ return measurements;
+ }
+
+ /**
+ * removeMeasurement:
+ *
+ * Removes the measurement passed in from the database
+ *
+ * @param {Measurement} measurement
+ */
+ async removeMeasurement(measurement,imageIdentifier) {
+ await this.db.measurements.delete(measurement.id,imageIdentifier);
+ }
+
+ /**
+ * saveAll:
+ *
+ * Saves an entire list of measurements
+ *
+ * @param {Measurement []} measurements
+ */
+ saveAll(measurements,imageIdentifier) {
+ for (let i = 0; i < measurements.length; i++) {
+ this.saveMeasurement(measurements[i],imageIdentifier);
+ }
+ }
+
+ /**
+ * saveMeasurement:
+ *
+ * Saves a measurement into the database
+ * Measurements that already exist are replaced with their new versions
+ *
+ * @param {Measurement} measurement: the measurement object to store
+ */
+ saveMeasurement(measurement,imageIdentifier) {
+ this.db.measurements.put({
+ id: measurement.id,
+ image: imageIdentifier,
+ p1x: measurement.p1.x,
+ p1y: measurement.p1.y,
+ p2x: measurement.p2.x,
+ p2y: measurement.p2.y,
+ name: measurement.name,
+ color: measurement.color
+ });
+ }
+}
+
+/*
+ * OSDMeasure.js
+ *
+ * Plugin for OpenSeadragon that allows for measuring
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ *
+ * Requires OpenSeadragon, Fabric.js, and
+ * the OpenSeadragon Fabric.js Overlay plugin
+ */
+
+class OSDMeasure {
+
+ /**
+ * APIs used by the plugin
+ */
+ viewer; // the OpenSeadragon viewer
+ overlay; // the fabric.js overlay, contains the canvas
+ fabricCanvas; // the fabric.js canvas to draw shapes on
+ db; // DexieWrapper to access the database
+
+ /**
+ * Flags
+ */
+ isMeasuring; // flag to indicate when user is mid-measurement (one point marked)
+ useBuiltInUI; // when true, will setup the built-in UI after starting
+
+ /**
+ * Data
+ */
+ measurements; // holds measurements taken in an array
+ p1; // start point of the measurement
+ p2; // end point of the measurement
+ redoStack; // populated upon undo - this way the user can go back again
+
+ /**
+ * Customization options
+ */
+ conversionFactor; // factor to multiply for converting from pixels
+ measurementColor; // color to render measurement markings
+ menuOptions; // options object to be passed to the built-in UI if used
+ units; // string to indicate what units are used, for example "um"
+ imageIdentifier;
+ measurementIdCounter;
+ measurementList = null;
+ /**
+ * constructor
+ *
+ * Sets up the viewer by starting a fabric.js overlay and wiring callbacks
+ *
+ * @param {OpenSeadragon} viewer: the OpenSeadragon viewer
+ * @param {Object} options: object used to customize settings
+ */
+ constructor(viewer, options = {}) {
+ this.viewer = viewer;
+
+ this.processOptions(options);
+
+ // pull in the two libraries
+ this.overlay = viewer.fabricjsOverlay();
+ this.fabricCanvas = this.overlay.fabricCanvas();
+ this.viewer.gestureSettingsMouse.clickToZoom = false;
+ this.viewer.gestureSettingsTouch.clickToZoom = false;
+ this.isMeasuring = false; // toggles when user places first point of a measurement
+
+ // the two points used to measure - these are image coordinates
+ this.p1 = null;
+ this.p2 = null;
+
+ // store all the measurements (and extraneous points)
+ this.measurements = [];
+ // temporarily stores undone measurements
+ this.redoStack = [];
+
+ // initialize databasse
+ this.db = new DexieWrapper(this);
+
+ this.measurementList = new MeasurementList(this);
+
+ this.imageIdentifier = this.viewer.tileSources[this.viewer.currentPage()];
+
+ this.viewer.addHandler('page', (event) => {
+ this.updateImageIdentifier(event);
+ this.clearCanvas();
+ this.loadFromLocalStorage(this.imageIdentifier);
+ });
+
+ // add our custom handler for measurements
+ this.viewer.addHandler('canvas-double-click', (event) => {
+ this.addMeasurement(event);
+ if (!event.quick) {
+ event.preventDefaultAction = true;
+ }
+ });
+
+ // re-render on page event (change in zoom)
+ this.viewer.addHandler('zoom', this.adjustToZoom.bind(this));
+
+ // re-render on rotation
+ this.viewer.addHandler('rotate', () => {
+ this.viewer.viewport.rotateTo(0);
+ })
+
+ // dispatch correct method on key press
+ document.addEventListener('keydown', (event) => {
+ this.handleKeyPress(event);
+ });
+
+ this.loadFromLocalStorage();
+ this.measurementIdCounter = 0;
+ // Add a click event listener to select a measurement when clicked
+ this.viewer.addHandler("canvas-click", (event) => {
+ const webPoint = event.position;
+ const viewportPoint = this.viewer.viewport.pointFromPixel(webPoint);
+ const imagePoint = this.viewer.viewport.viewportToImageCoordinates(viewportPoint);
+
+ for (let i = 0; i < this.measurements.length; i++) {
+ if (this.measurements[i].isPointInside(imagePoint.x, imagePoint.y)) {
+ // Check if the clicked measurement is the same as the selectedMeasurement
+ if (this.selectedMeasurement === this.measurements[i]) {
+ // If it is, deselect it
+ this.deselectMeasurement();
+ } else {
+ // If it's not, select it
+ this.selectMeasurement(this.measurements[i]);
+ }
+ break;
+ }
+ }
+ });
+ }
+
+ updateImageIdentifier(event){
+ this.imageIdentifier = this.viewer.tileSources[this.viewer.currentPage()];
+ }
+
+ clearCanvas(){
+ this.fabricCanvas.clear();
+ for (let i = 0; i < this.measurements.length; i++) {
+ this.measurements[i].remove();
+ }
+ this.measurements = [];
+ this.redoStack = [];
+ if (this.isMeasuring) {
+ this.p1.remove();
+ }
+ this.p1 = null;
+ this.p2 = null;
+ this.isMeasuring = false;
+ document.dispatchEvent(new Event("measurements-reset"));
+ }
+ /*
+ * addMeasurement:
+ * Only called in measuring mode - places a new point onto the canvas,
+ * and performs measuring once two points have been placed.
+ */
+ addMeasurement(event) {
+ let webPoint = event.position;
+ let viewportPoint = this.viewer.viewport.pointFromPixel(webPoint);
+ let imagePoint = this.viewer.viewport.viewportToImageCoordinates(viewportPoint);
+ let zoom = this.viewer.viewport.getZoom();
+
+ if (this.isMeasuring) { // already have a point, so complete the measurement
+ this.p2 = new Point(imagePoint.x, imagePoint.y, this.measurementColor, this.fabricCanvas , this.imageIdentifier);
+ this.p2.render(zoom);
+ let measurement = new Measurement(
+ this.p1, this.p2,
+ `M${this.measurements.length + 1}`,
+ this.measurementColor, this.conversionFactor, this.units, this.fabricCanvas ,this.imageIdentifier
+ );
+ measurement.render(zoom);
+ this.measurements.push(measurement);
+ measurement.id = this.measurementIdCounter++;
+ measurement.imageIdentifier = this.imageIdentifier;
+ this.saveInLocalStorage(this.imageIdentifier);
+ // dispatch an event to let it be known there is a new measurement
+ document.dispatchEvent(new Event("measurement-added"));
+ } else { // place the first point
+ this.p1 = new Point(imagePoint.x, imagePoint.y, this.measurementColor, this.fabricCanvas ,this.imageIdentifier);
+ this.p1.render(zoom);
+ }
+ // have to blow out the redo stack since we made a new measurement
+ this.redoStack = [];
+ this.isMeasuring = !this.isMeasuring;
+ }
+ deleteSelectedMeasurement() {
+ if (this.selectedMeasurement) {
+ const measurementIndex = this.measurements.indexOf(this.selectedMeasurement);
+ console.log(measurementIndex);
+ if (measurementIndex !== -1) {
+ // Remove it from the canvas
+ this.selectedMeasurement.remove();
+
+ // Remove it from the list
+ this.measurements.splice(measurementIndex, 1);
+
+ // Remove it from the database
+ this.redoStack.push(this.selectedMeasurement);
+ this.db.removeMeasurement(this.selectedMeasurement, this.imageIdentifier);
+ this.saveInLocalStorage(this.imageIdentifier);
+ // Dispatch a custom event with a parameter
+ const event = new CustomEvent("delete-selected-measurement", {
+ detail: {
+ measurement: this.selectedMeasurement
+ }
+ });
+ document.dispatchEvent(event);
+
+ this.deselectMeasurement(); // Deselect the deleted measurement
+ }
+ }
+ }
+ // Add a method to handle measurement selection
+ selectMeasurement(measurement) {
+ // Deselect the previously selected measurement (if any)
+ if (this.selectedMeasurement) {
+ this.deselectMeasurement();
+ }
+
+ this.selectedMeasurement = measurement;
+ this.selectedMeasurement.select();
+ }
+ // Add a method to deselect the currently selected measurement
+ deselectMeasurement() {
+ if (this.selectedMeasurement) {
+ this.selectedMeasurement.deselect();
+ this.selectedMeasurement = null;
+ }
+ }
+
+ /**
+ * adjustToZoom:
+ *
+ * Adjusts the sizes of all fabric.js objects based on zoom
+ */
+ adjustToZoom() {
+ let zoom = this.viewer.viewport.getZoom();
+ for (let i = 0; i < this.measurements.length; i++) {
+ this.measurements[i].adjustToZoom(zoom);
+ }
+ if (this.p1 != null) {
+ this.p1.adjustToZoom(zoom);
+ }
+ if (this.p2 != null) {
+ this.p2.adjustToZoom(zoom);
+ }
+ }
+
+ /**
+ * clear:
+ * Erases all saved data relevant to this specific image from
+ * localStorage and clears fabric objects and measurement data.
+ */
+ clear() {
+ this.db.clear();
+ for (let i = 0; i < this.measurements.length; i++) {
+ this.measurements[i].remove();
+ }
+ this.measurements = [];
+ this.redoStack = [];
+ if (this.isMeasuring) {
+ this.p1.remove();
+ }
+ this.p1 = null;
+ this.p2 = null;
+ this.isMeasuring = false;
+ document.dispatchEvent(new Event("measurements-reset"));
+ }
+
+ /**
+ * exportCSV:
+ * creates a CSV containing the measurement data
+ */
+ exportCSV() {
+ let header = ["Name", "Point 1 X", "Point 1 Y", "Point 2 X", "Point 2 Y", "Distance(mm)"];
+ let createRow = (measurement) => {
+ return [
+ measurement.name,
+ measurement.p1.x,
+ measurement.p1.y,
+ measurement.p2.x,
+ measurement.p2.y,
+ measurement.distance
+ ];
+ };
+ // generate the rows
+ let rows = [header];
+ for (let i = 0; i < this.measurements.length; i++) {
+ rows.push(createRow(this.measurements[i]));
+ }
+ // join the rows together
+ let csv = "data:text/csv;charset=utf-8," + rows.map((row) => row.join(",")).join("\n");
+ // encode to URI
+ let uri = encodeURI(csv);
+ // download using invisible link trick
+ let link = document.createElement("a");
+ link.setAttribute("href", uri);
+ link.setAttribute("download", "measurements.csv");
+ document.body.appendChild(link);
+ link.click();
+ // clean up
+ document.body.removeChild(link);
+ }
+
+ /**
+ * handleKeyPress:
+ *
+ * Handles keyboard shortcuts
+ */
+ handleKeyPress(event) {
+ // reset
+ if (event.ctrlKey && event.key == 'r') {
+ if (window.confirm("Are you sure you want to reset all measurements and annotations?")) {
+ this.clear();
+ }
+ }
+ // undo
+ else if (event.ctrlKey && event.key == 'z') {
+ this.undo();
+ }
+ // redo
+ else if (event.ctrlKey && event.key == 'y') {
+ this.redo();
+ }
+ // export csv
+ else if (event.ctrlKey && event.key == 's') {
+ this.exportCSV();
+ }
+ else if(event.ctrlKey && event.key == "d") this.deleteSelectedMeasurement();
+ // override ctrl presses
+ if (event.ctrlKey) {
+ event.preventDefault();
+ }
+ }
+
+ /**
+ * loadFromLocalStorage:
+ * Loads any existing measurements from localStorage
+ */
+ async loadFromLocalStorage(imageIdentifier) {
+ this.measurements = await this.db.getAllMeasurements(this.imageIdentifier);
+ this.setMeasurementColor(localStorage.getItem("color"));
+ document.dispatchEvent(new Event("data-loaded"));
+ // render the measurements
+ this.renderAllMeasurements();
+ }
+
+ /**
+ * processOptions:
+ *
+ * Stores customization options in the object proper
+ * Loads the built-in UI if chosen for use
+ *
+ * @param {Object} options
+ */
+ processOptions(options) {
+ if (options.conversionFactor) {
+ this.conversionFactor = options.conversionFactor;
+ }
+ else {
+ this.conversionFactor = 1;
+ }
+
+ if (options.units) {
+ this.units = options.units;
+ }
+ else {
+ this.units = "px";
+ }
+
+ if (options.measurementColor) {
+ this.measurementColor = options.measurementColor;
+ }
+ else {
+ this.measurementColor = "#000000"
+ }
+
+ if (options.useBuiltInUI) {
+ let ui = new UI(this);
+ ui.addToDocument();
+ }
+ }
+
+ /**
+ * redo:
+ * replaces the last undone measurement or point if there are any in the stack
+ */
+ redo() {
+ if (this.redoStack.length > 0) {
+ let lastObject = this.redoStack.pop();
+ // get zoom level for rendering
+ let zoom = this.viewer.viewport.getZoom();
+ // if it's a point, handle it as such
+ if (lastObject instanceof Point) {
+ this.p1 = lastObject;
+ this.p1.render(zoom);
+ // set isMeasuring so the next double-click finishes the measurement
+ this.isMeasuring = true;
+ }
+ else { // it's a measurement
+ this.measurements.push(lastObject);
+ lastObject.id = this.measurements.length - 1;
+ lastObject.p1.render(zoom);
+ lastObject.p2.render(zoom);
+ lastObject.render(zoom);
+ // can't forget to save!
+ this.saveInLocalStorage(this.imageIdentifier);
+ // dispatch event to replace it in the measurement list
+ document.dispatchEvent(new Event("measurement-added"));
+ }
+ }
+ }
+
+ /**
+ * renderAllMeasurements:
+ * Renders all measurements
+ */
+ renderAllMeasurements() {
+ let zoom = this.viewer.viewport.getZoom();
+ for (let i = 0; i < this.measurements.length; i++) {
+ this.measurements[i].p1.render(zoom);
+ this.measurements[i].p2.render(zoom);
+ this.measurements[i].render(zoom);
+ }
+ if (this.isMeasuring && this.p1 != null) {
+ this.p1.render(zoom);
+ }
+ }
+
+ /**
+ * saveInLocalStorage:
+ * Saves the measurements in localStorage in JSON format
+ */
+ saveInLocalStorage(imageIdentifier) {
+ this.db.saveAll(this.measurements,imageIdentifier);
+ localStorage.setItem("color", this.measurementColor);
+ }
+
+ /**
+ * setMeasurementColor:
+ * changes color of measurement markings, also when
+ * mid-measurement, changes the color of the first marking
+ */
+ setMeasurementColor(color) {
+ this.measurementColor = color;
+ if (this.isMeasuring) {
+ // have to re-color the marking already placed
+ this.p1.color = this.measurementColor;
+ this.p1.fabricObject.fill = this.measurementColor;
+ this.fabricCanvas.renderAll();
+ }
+ this.saveInLocalStorage(this.imageIdentifier);
+ }
+
+ /**
+ * undo:
+ * Undose the last action - if mid-measurement, the first
+ * point is erased and the user will have to start over.
+ * Otherwise, the last created measurement is erased.
+ */
+ async undo() {
+ if (this.isMeasuring) { // we have a point
+ // store the point for redo
+ this.redoStack.push(this.p1);
+ this.p1.remove();
+ this.p1 = null;
+ this.isMeasuring = !this.isMeasuring;
+ }
+ else if (this.measurements.length > 0) { // we have a whole measurement
+ // pop out of measurements and into redoStack
+ let measurement = this.measurements.pop()
+ measurement.remove();
+ this.redoStack.push(measurement);
+ await this.db.removeMeasurement(measurement);
+ this.saveInLocalStorage(this.imageIdentifier);
+ document.dispatchEvent(new Event("measurement-removed"));
+ }
+
+ }
+}
+/**
+ * Measurement.js
+ * Object model to represent a measurement between two points
+ * By Nicholas Verrochi and Vidhya Sree N
+ * For CS410 - The Axolotl Project
+ */
+
+class Measurement {
+
+ id; // used to track in the database - set externally
+ p1; // starting point of the measurement in **image** coordinates
+ p2; // ending point of the measurement in image coordinates
+ name; // name given to the measurement
+ color; // color used to render the measurement
+ distance; // distance in pixels
+ conversionFactor; // factor to convert from px -> chosen units (distance in px * conversion factor = converted distance)
+ units; // string to represent the units, for example "px" for pixels
+ imageIdentifier;
+
+ /**
+ * fabric.js objects
+ */
+ fabricCanvas; // the canvas on which to render the fabric.js objects
+ line; // line between the two points
+ textObject; // text displaying the distance
+ isSelected;
+
+ /**
+ * constructor
+ *
+ * Stores points, name, color, and conversion factor and units
+ * Instantiates a fabric Group to store the relevant objects to render
+ *
+ * @param {Point} p1
+ * @param {Point} p2
+ * @param {string} name
+ * @param {string} color
+ * @param {float} conversionFactor
+ * @param {string} units
+ * @param {fabric.Canvas} fabricCanvas
+ */
+ constructor(p1, p2, name, color, conversionFactor, units, fabricCanvas,imageIdentifier) {
+ this.p1 = p1;
+ this.p2 = p2;
+ this.name = name;
+ this.color = color;
+ this.distance = Math.sqrt(Math.pow(this.p2.x - this.p1.x, 2) + Math.pow(this.p2.y - this.p1.y, 2));
+ this.conversionFactor = conversionFactor; // pixels * conversionFactor = actual measurement
+ this.units = units;
+ // convert to proper units
+ this.distance *= conversionFactor;
+ this.fabricCanvas = fabricCanvas;
+ this.imageIdentifier = imageIdentifier;
+ this.isSelected = false;
+ }
+
+ /**
+ * adjustToZoom:
+ *
+ * Adjusts the fabric objects to the zoom level of the viewer
+ *
+ * @param {float} zoom: zoom ratio to adjust to
+ */
+ adjustToZoom(zoom) {
+ this.p1.adjustToZoom(zoom);
+ this.p2.adjustToZoom(zoom);
+ this.line.strokeWidth = 50 / zoom;
+ this.textObject.fontSize = 300 / zoom;
+ // adjust distance between right-most point and text
+ this.textObject.left = Math.max(this.p1.x, this.p2.x) + 100 / zoom;
+ }
+
+ /**
+ * remove:
+ *
+ * Removes the fabric group from the canvas
+ */
+ remove() {
+ this.p1.remove();
+ this.p2.remove();
+ this.fabricCanvas.remove(this.line);
+ this.fabricCanvas.remove(this.textObject);
+ }
+
+ /**
+ * render:
+ *
+ * Adds the points, line, and length text to the canvas
+ *
+ * @param {float} zoom: zoom ratio of the viewer
+ */
+ render(zoom) {
+ // draw line between p1 and p2
+ this.line = new fabric.Line([this.p1.x, this.p1.y, this.p2.x, this.p2.y], {
+ originX: 'center',
+ originY: 'center',
+ stroke: this.color,
+ strokeWidth: 50 / zoom
+ });
+ this.fabricCanvas.add(this.line);
+
+ // create text object to display measurement
+ let text = (this.distance).toFixed(3) + " " + this.units;
+ this.textObject = new fabric.Text(text, {
+ left: Math.max(this.p1.x, this.p2.x) + 100 / zoom,
+ top: this.p1.x > this.p2.x ? this.p1.y : this.p2.y,
+ fontSize: 300 / zoom,
+ fill: this.color
+ });
+ this.fabricCanvas.add(this.textObject);
+ }
+ // Add methods to select and deselect the measurement
+ select() {
+ this.line.set({
+ stroke: "grey"
+ });
+ // Highlight both points with a grey fill
+ this.p1.fabricObject.set({ fill: "grey" });
+ this.p2.fabricObject.set({ fill: "grey" });
+
+ this.textObject.set({ fill: "grey" });
+ this.fabricCanvas.renderAll();
+ }
+
+ deselect() {
+ this.line.set({
+ stroke: this.color
+ });
+ this.p1.fabricObject.set({ fill: this.color });
+ this.p2.fabricObject.set({ fill: this.color });
+
+ this.textObject.set({ fill: this.color });
+ this.fabricCanvas.renderAll();
+ }
+ // Add a method to check if a point is inside the measurement
+ isPointInside(x, y) {
+ const minX = Math.min(this.p1.x, this.p2.x);
+ const maxX = Math.max(this.p1.x, this.p2.x);
+ const minY = Math.min(this.p1.y, this.p2.y);
+ const maxY = Math.max(this.p1.y, this.p2.y);
+
+ return x >= minX && x <= maxX && y >= minY && y <= maxY;
+ }
+}
+
+/**
+ * Point.js
+ * Object model to represent a point (part of a measurement)
+ * By Nicholas Verrochi and Vidhya Sree N
+ * For CS410 - The Axolotl Project
+ */
+
+class Point {
+
+ x; // x-coordinate in **image** coordinates
+ y; // y-coordinate in **image** coordinates
+ color; // color to render in
+
+ /**
+ * fabric.js objects
+ */
+ fabricCanvas; // canvas which holds the point
+ fabricObject; // the circle marking the point
+ imageIdentifier;
+
+ /**
+ * constructor
+ *
+ * Creates a point that can be rendered on the canvas
+ *
+ * @param {int} x
+ * @param {int} y
+ * @param {string} color
+ * @param {fabricCanvas} fabricCanvas
+ */
+ constructor(x, y, color, fabricCanvas,imageIdentifier) {
+ this.x = x;
+ this.y = y;
+ this.color = color;
+ this.fabricCanvas = fabricCanvas;
+ this.imageIdentifier = imageIdentifier;
+
+ // create the fabric.js object for rendering
+ this.fabricObject = new fabric.Circle({
+ originX: 'center',
+ originY: 'center',
+ left: this.x,
+ top: this.y,
+ fill: this.color,
+ radius: 150
+ });
+ }
+
+ /**
+ * adjustToZoom:
+ *
+ * Adjusts size of the circle based on zoom level
+ *
+ * @param {float} zoom: zoom ratio to adjust to
+ */
+ adjustToZoom(zoom) {
+ this.fabricObject.setRadius(150 / (zoom * 1.5));
+ }
+
+ /**
+ * remove:
+ *
+ * Removes the circle from the canvas
+ */
+ remove() {
+ this.fabricCanvas.remove(this.fabricObject);
+ }
+
+ /**
+ * render:
+ *
+ * Adds the circle to the canvas
+ *
+ * @param {float} zoom: zoom ratio
+ */
+ render(zoom) {
+ this.adjustToZoom(zoom); // needs to be called first for some silly reason
+ this.fabricCanvas.add(this.fabricObject);
+ }
+}
+
+/**
+ * ButtonBar.js
+ *
+ * Creates a row of buttons for the menu
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class ButtonBar {
+
+ plugin; // access to the OSDMeasure plugin
+
+ /**
+ * HTML elements
+ */
+ element; // container for all buttons
+ undoButton; // undo button
+ redoButton; // redo button
+ resetButton; // reset button
+ exportButton; // allows for exporting measurement data to csv
+ deleteButton;
+
+ /**
+ * constructor
+ *
+ * Creates the button bar and sets up handlers
+ *
+ * @param {OSDMeasure} plugin: reference to the plugin
+ */
+
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.element = document.createElement("div");
+
+ this.undoButton = document.createElement("input");
+ this.undoButton.setAttribute("type", "button");
+ this.undoButton.setAttribute("value", "undo (ctrl + z)");
+ this.setButtonStyle(this.undoButton);
+ this.undoButton.addEventListener("click", () => {
+ this.plugin.undo();
+ });
+ this.element.appendChild(this.undoButton);
+
+ this.redoButton = document.createElement("input");
+ this.redoButton.setAttribute("type", "button");
+ this.redoButton.setAttribute("value", "redo (ctrl + y)");
+ this.setButtonStyle(this.redoButton);
+ this.redoButton.addEventListener("click", () => {
+ this.plugin.redo();
+ });
+ this.element.appendChild(this.redoButton);
+
+ this.resetButton = document.createElement("input");
+ this.resetButton.setAttribute("type", "button");
+ this.resetButton.setAttribute("value", "reset (ctrl + r)")
+ this.setButtonStyle(this.resetButton);
+ this.resetButton.addEventListener("click", () => {
+ if (window.confirm("Are you sure you want to reset all measurements and annotations?")) {
+ this.plugin.clear();
+ }
+ });
+ this.element.appendChild(this.resetButton);
+
+ this.exportButton = document.createElement("input");
+ this.exportButton.setAttribute("type", "button");
+ this.exportButton.setAttribute("value", "export csv (ctrl + s)")
+ this.setButtonStyle(this.exportButton);
+ this.exportButton.addEventListener("click", () => {
+ this.plugin.exportCSV();
+ });
+ this.element.appendChild(this.exportButton);
+ // Create a "Delete" button
+ this.deleteButton = document.createElement("input");
+ this.deleteButton.setAttribute("type", "button");
+ this.deleteButton.setAttribute("value", "delete (ctrl + d)");
+ this.setButtonStyle(this.deleteButton);
+ this.deleteButton.addEventListener("click", () => {
+ this.plugin.deleteSelectedMeasurement();
+ });
+ this.element.appendChild(this.deleteButton);
+ }
+
+ /**
+ * setButtonStyle:
+ *
+ * Styles an individual button
+ *
+ * @param {HMTLInputElement} button: button to style
+ */
+ setButtonStyle(button) {
+ let style = button.style;
+ style.setProperty("color", "white");
+ style.setProperty("background-color", "black");
+ style.setProperty("width", "100%");
+ style.setProperty("height", "25px");
+ }
+}
+
+/**
+ * MeasurementList.js
+ *
+ * Encapsulates the dynamic measurement list in the menu
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class MeasurementList {
+
+ plugin; // reference to the OSDMeasure plugin
+
+ /**
+ * HTML elements
+ */
+ element; // UL element to hold the entire list
+ listItems = []; // list of li items, one per measurement of type MeasurementListItem
+
+ /**
+ * constructor:
+ *
+ * Creates an empty measurement list
+ *
+ * @param {OSDMeasure} plugin: reference to access the plugin
+ */
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.element = document.createElement("ul");
+ this.element.style.setProperty("list-style", "none");
+
+ // add new list item when measurement added
+ document.addEventListener("measurement-added", this.addLatestMeasurement.bind(this));
+ document.addEventListener("measurement-removed", this.removeLatestMeasurement.bind(this));
+ document.addEventListener("measurements-reset", this.resetMeasurements.bind(this));
+ document.addEventListener("data-loaded", this.addAllMeasurements.bind(this));
+ document.addEventListener("delete-selected-measurement", (event) => {
+ this.removeSelectedMeasurement(event.detail.measurement);
+ });
+ }
+
+ /**
+ * addAllMeasurements:
+ *
+ * Adds all measurements to the list
+ */
+ addAllMeasurements() {
+ for (let i = 0; i < this.plugin.measurements.length; i++) {
+ let measurement = this.plugin.measurements[i];
+ let listItem = new MeasurementListItem(this.plugin, measurement);
+ this.listItems.push(listItem);
+ this.element.appendChild(listItem.element);
+ }
+ }
+
+ /**
+ * addLatestMeasurement:
+ *
+ * Creates a new list item for the most recently added measurement and adds it to the list
+ */
+ addLatestMeasurement() {
+ let measurement = this.plugin.measurements[this.plugin.measurements.length - 1];
+ let listItem = new MeasurementListItem(this.plugin, measurement);
+ this.listItems.push(listItem);
+ this.element.appendChild(listItem.element);
+ }
+
+ /**
+ * addToDocument:
+ *
+ * Adds the entire list to the DOM tree
+ * Also sets it up to work in fullscreen mode
+ */
+ addToDocument() {
+ document.appendChild(this.element);
+ this.plugin.viewer.element.appendChild(this.element);
+ }
+
+ /**
+ * removeLatestMeasurement:
+ *
+ * Removes the latest measurement from the list upon undo
+ */
+ removeLatestMeasurement() {
+ this.element.removeChild(this.listItems.pop().element);
+ }
+
+ /**
+ * resetMeasurements:
+ *
+ * Clears the list when the user resets all measurements
+ */
+ resetMeasurements() {
+ for(let i = 0; i < this.listItems.length; i++) {
+ this.element.removeChild(this.listItems[i].element);
+ }
+ this.listItems = [];
+ }
+
+ removeSelectedMeasurement(measurement) {
+ const measurementId = measurement.id;
+ const indexToRemove = this.listItems.findIndex(item => item.measurement.id === measurementId);
+ console.log(indexToRemove);
+ if (indexToRemove !== -1) {
+ this.element.removeChild(this.listItems[indexToRemove].element);
+ this.listItems.splice(indexToRemove, 1);
+ }
+ }
+}
+
+/**
+ * MeasurementListItem.js
+ *
+ * Encapsulates a single item on the measurement list
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class MeasurementListItem {
+
+ plugin; // reference to the OSDMeasure plugin
+ measurement; // reference to the measurement's object representation
+
+ /**
+ * HTML elements
+ */
+ element; // li element that holds the list item
+ nameField; // text input that holds the measurement name
+ lengthDisplay; // displays the length of the measurement
+
+ /**
+ * constructor:
+ *
+ * @param {OSDMeasure} plugin: reference for access to the plugin
+ * @param {Measurement} measurement: measurement object to represent
+ */
+ constructor(plugin, measurement) {
+ this.plugin = plugin;
+ this.measurement = measurement;
+
+ this.element = document.createElement("li");
+
+ this.nameField = document.createElement("input");
+ this.nameField.setAttribute("type", "text");
+ this.nameField.value = this.measurement.name;
+ this.nameField.addEventListener("input", this.updateName.bind(this));
+ this.setNameFieldStyle();
+ this.element.appendChild(this.nameField);
+
+ this.lengthDisplay = document.createElement("span");
+ this.lengthDisplay.innerText = `: ${(this.measurement.distance).toFixed(3)} ${this.measurement.units}`;
+ this.element.appendChild(this.lengthDisplay);
+ }
+
+ /**
+ * setNameFieldStyle:
+ *
+ * Sets up the style of the name input field
+ */
+ setNameFieldStyle() {
+ let style = this.nameField.style;
+ style.setProperty("background", "transparent");
+ style.setProperty("border", "none");
+ style.setProperty("color", "white");
+ style.setProperty("text-align", "right");
+ style.setProperty("width", "50%");
+ }
+
+ /**
+ * updateName:
+ *
+ * Changes name to reflect the user's choice
+ */
+ updateName() {
+ this.measurement.name = this.nameField.value;
+ this.plugin.saveInLocalStorage();
+ }
+}
+
+/**
+ * Menu.js
+ *
+ * Class to represent the measurement menu
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class Menu {
+
+ plugin; // reference to the OSDMeasure plugin
+
+ /**
+ * HTML elements
+ */
+ element; // holds the entire menu
+ colorSelector; // color input to select measurement color
+ measurementList; // dynamically displays measurements added
+ buttonBar; // div which holds buttons for operations like undo, etc
+
+ /**
+ * constructor
+ *
+ * Programatically constructs a menu
+ *
+ * @param {OSDMeasure} plugin: reference to interact with the plugin
+ */
+ constructor(plugin) {
+ this.plugin = plugin;
+
+ // create menu container
+ this.element = document.createElement("div");
+ this.element.setAttribute("hidden", "hidden"); // start hidden until user opens
+ this.setMenuStyle();
+
+ // create color selector
+ this.colorSelector = document.createElement("input");
+ this.colorSelector.setAttribute("type", "color");
+ // handler for changing color
+ this.colorSelector.addEventListener("change", this.handleColorChange.bind(this), false);
+ this.setColorSelectorStyle();
+ this.element.appendChild(this.colorSelector);
+
+ // create measurement list
+ this.measurementList = new MeasurementList(this.plugin);
+ this.element.appendChild(this.measurementList.element);
+
+ // create button bar
+ this.buttonBar = new ButtonBar(this.plugin);
+ this.element.appendChild(this.buttonBar.element);
+
+ // set starting color after data loaded (color maintained upon restarting)
+ document.addEventListener("data-loaded", this.updateColor.bind(this));
+ }
+
+ /**
+ * addToDocument:
+ *
+ * Adds the menu to the DOM tree
+ */
+ addToDocument() {
+ document.body.appendChild(this.element);
+ // append to the viewer's element so menu will stay visible in fullscreen
+ this.plugin.viewer.element.appendChild(this.element);
+ }
+
+ /**
+ * handleColorChange:
+ *
+ * Handles change in color from the color selector
+ */
+ handleColorChange() {
+ let color = this.colorSelector.value;
+ this.plugin.setMeasurementColor(color);
+ }
+
+ /**
+ * setColorSelectorStyle:
+ *
+ * Sets the style of the color selector
+ */
+ setColorSelectorStyle() {
+ let style = this.colorSelector.style;
+ style.setProperty("width", "100%");
+ style.setProperty("height", "30px");
+ style.setProperty("border", "none");
+ style.setProperty("padding", "0px");
+ }
+
+ /**
+ * setMenuStyle:
+ *
+ * sets the style of the menu container
+ */
+ setMenuStyle() {
+ let style = this.element.style;
+ // positioning
+ style.setProperty("position", "absolute");
+ style.setProperty("text-align", "left");
+ style.setProperty("top", "10%");
+ style.setProperty("right", "0%");
+ style.setProperty("z-index", "2");
+ // sizing
+ style.setProperty("width", "20%");
+ style.setProperty("padding", "1%");
+ // coloring and opacity
+ style.setProperty("background", "rgba(0, 0, 0, 0.75)");
+ style.setProperty("color", "white"); // text color
+ }
+
+ /**
+ * updateColor:
+ *
+ * Callback that sets the color swatch properly after loading
+ * Needed because color selection saved between sessions
+ */
+ updateColor() {
+ let color = this.plugin.measurementColor;
+ this.colorSelector.value = color;
+ this.plugin.setMeasurementColor(color);
+ }
+}
+/**
+ * MenuBar.js
+ *
+ * Encapsulates the menu icon
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class MenuButton {
+
+ plugin; // reference to the OSDMeasure plugin
+
+ /**
+ * HTML elements
+ */
+ element; // img element that holds the menu icon
+
+ /**
+ * constructor:
+ *
+ * Encapsulates the menu icon using HTMLElement objects
+ * Sets up callbacks to open the menu on click
+ * Adds the menu icon to the DOM tree
+ *
+ * @param {OSDMeasure} plugin: reference used to interact with the plugin
+ */
+ constructor(plugin) {
+ this.plugin = plugin;
+ this.element = document.createElement("img");
+ this.element.setAttribute("tabindex", "0"); // allow tabbing
+ this.element.setAttribute("src", "img/hamburger-50.png")
+ this.setupStyle();
+ }
+
+ /**
+ * addToDocument:
+ *
+ * Adds the menu icon to the document
+ * Appends elements as children to the viewer - this allows the menu to appear while in fullscreen mode
+ */
+ addToDocument() {
+ document.body.appendChild(this.element);
+ // appending to viewer so icon displays in fullscreen mode
+ this.plugin.viewer.element.appendChild(this.element);
+ }
+
+ /**
+ * setupIconStyle:
+ *
+ * Sets up the CSS styling for the menu icon (not the dots within)
+ */
+ setupStyle() {
+ let style = this.element.style;
+ // need to set background color for visibility
+ style.setProperty("background-color", "white");
+ // positioning - set in top right
+ style.setProperty("position", "absolute");
+ style.setProperty("top", "0%");
+ style.setProperty("right", "0%");
+ style.setProperty("z-index", "1");
+ // pointer cursor so the user knows they can click
+ style.setProperty("cursor", "pointer");
+ }
+}
+/**
+ * UI.js
+ *
+ * Generates a basic UI for the OSDMeasure plugin
+ * This gets generated on the document and alters
+ * some of the css for the document
+ *
+ * By Nicholas Verrochi and Vidhya Sree N
+ */
+
+class UI {
+
+ plugin; // access to the OSDMeasure plugin
+
+ /**
+ * HTML elements
+ */
+ menuButton; // traditional (three dots) menu icon
+ menu; // the measurement menu itself
+
+ /**
+ * constructor:
+ *
+ * Sets up inner HTML elements and their style
+ * Sets up event callbacks for UI elements
+ * Doesn't add anything to the document! This has a separate function
+ *
+ * @param {OSDMeasure} plugin: reference to interact with the plugin
+ */
+ constructor(plugin, options = {}) {
+ this.plugin = plugin
+ this.setBodyStyle();
+
+ // setup menu and icon
+ this.menuButton = new MenuButton(plugin, options);
+ this.menu = new Menu(plugin, options);
+
+ // wire menu to open when icon clicked
+ this.menuButton.element.addEventListener("click", this.toggleMenu.bind(this));
+ }
+
+ /**
+ * addToDocument:
+ *
+ * Adds the entire UI to the document - call this after instantiating to display
+ */
+ addToDocument() {
+ this.menuButton.addToDocument();
+ this.menu.addToDocument();
+ }
+
+ /**
+ * toggleMenu:
+ *
+ * Toggles the menu visibility
+ */
+ toggleMenu() {
+ if (this.menu.element.getAttribute("hidden") == "hidden") {
+ this.menu.element.removeAttribute("hidden");
+ }
+ else {
+ this.menu.element.setAttribute("hidden", "hidden");
+ }
+ }
+
+ /**
+ * setBodyStyle:
+ *
+ * Sets style for the document to setup background color and stop overflow
+ */
+ setBodyStyle() {
+ let style = document.body.style;
+ style.setProperty("overflow", "hidden", "important");
+ style.setProperty("background-color", "black");
+ style.setProperty("font-size", "0.9em");
+ }
+}
\ No newline at end of file
diff --git a/src/assets/stories.json b/src/assets/stories.json
index 75f3de8..24662a5 100644
--- a/src/assets/stories.json
+++ b/src/assets/stories.json
@@ -1,4 +1,31 @@
[
+ {
+ "id": 3,
+ "name": "La Tour de Babel",
+ "author": "Pieter Brueghel l'Ancien",
+ "url": "https://via.hoff.in/demo/images/babel/babel.dzi",
+ "displayMode": "ARTICLE",
+ "markers": [
+ {
+ "id": 0,
+ "name": "first",
+ "order": 0,
+ "bounds": {
+ "degrees": 0,
+ "height": 0.028126953260643097,
+ "width": 0.09524470945402953,
+ "x": 0.32376980664954275,
+ "y": 0.283813392419349
+ },
+ "zoom": 9,
+ "point": {
+ "x": 0.3580975873485992,
+ "y": 0.2968103267302634
+ },
+ "annotation": "lorem ipsum dolor sit amet"
+ }
+ ]
+ },
{
"id": 2,
"name": "Cells",
diff --git a/src/components/StoryContainer.vue b/src/components/StoryContainer.vue
index 9770b3c..38b3e31 100644
--- a/src/components/StoryContainer.vue
+++ b/src/components/StoryContainer.vue
@@ -16,6 +16,7 @@
import OpenSeadragon from 'openseadragon'
import '@/assets/plugins/openseadragon-scalebar.js'
import '@/assets/plugins/openseadragon-bookmark-url.js'
+import '@/assets/plugins/openseadragon-measure.js'
import { onMounted, ref, provide, onUnmounted } from 'vue'
import type { Story, ViewerConfiguration } from '@/types/virages'
import ArticleTop from './StoryArticleTop.vue'
@@ -38,7 +39,7 @@ const defaultViewerConfiguration: ViewerConfiguration = {
drawer: 'canvas',
preventDefaultAction: true,
visibilityRatio: 0.5,
- defaultZoomLevel: 1.2,
+ defaultZoomLevel: 4,
gestureSettingsMouse: {
scrollToZoom: true,
clickToZoom: false,
@@ -52,6 +53,7 @@ const initViewer = () => {
element: openSeadragonElt.value,
tileSources: props.story.url,
...defaultViewerConfiguration,
+ debugMode: true,
})
}
diff --git a/src/components/player/StoryEditor.vue b/src/components/player/StoryEditor.vue
index 485103c..8963e70 100644
--- a/src/components/player/StoryEditor.vue
+++ b/src/components/player/StoryEditor.vue
@@ -195,10 +195,11 @@ useEscapeKey(() => {
onMounted(() => {
nextTick(() => {
Viewer?.value.clearOverlays()
- // Viewer.value.viewport.defaultZoomLevel = 1
+ Viewer.value.viewport.defaultZoomLevel = 1.2
loadStory(props.story.markers)
editMarkerOnDragOrZoom()
initScalebar()
+ const plugin = new OSDMeasure(viewer, {})
Viewer.value.bookmarkUrl()
zoomTo(6) // COMPOSABLE WORKS
})