Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/autonoma.ca.git

Adds missing files

Author djarvis <email>
Date 2026-02-19 12:11:07 GMT-0800
Commit c49f88404dc44dc3f986812aafd8bb1b67abf097
Parent 8dec838
calculators/index.html
-<!DOCTYPE html><!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title>
+<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title>
</head>
<body>
calculators/rocket/index.html
+<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title></title>
+</head>
+<body>
+<ul>
+<li><a href="aero">Aerodynamic Heat Calculator</a></li>
+<li><a href="payload">Orbital Payload Calculator</a></li>
+</ol>
+</body>
+</html>
docs/.placeholder
editor.php
+<?php
+$filename = isset($_GET['filename']) ? $_GET['filename'] : '';
+
+// Handle annotation POST requests
+if( $_SERVER['REQUEST_METHOD'] === 'POST' &&
+ isset($_POST['annotations']) &&
+ isset($_POST['filename']) ) {
+ // Get the directory of the current script
+ $script_dir = dirname(__FILE__);
+
+ // Ensure target directory exists (relative to this script)
+ $target_dir = $script_dir . "/nn";
+
+ if( !is_dir($target_dir) ) {
+ mkdir($target_dir, 0755, true);
+ }
+
+ // Sanitize the filename to prevent directory traversal
+ $sanitized_filename = basename($_POST['filename']);
+
+ // Remove .pdf extension if present
+ $sanitized_filename = preg_replace('/\.pdf$/', '', $sanitized_filename);
+
+ // Full path to the target JSON file
+ $target_file = "$target_dir/{$sanitized_filename}.json";
+
+ // Write the annotations data to the file
+ $annotations_data = $_POST['annotations'];
+ file_put_contents($target_file, $annotations_data);
+
+ // Return a success response
+ header('Content-Type: application/json');
+ echo json_encode(['success' => true]);
+ exit;
+}
+
+$script_dir = dirname(__FILE__);
+
+// Define paths to check
+$alpha_file = realpath("$script_dir/alpha/$filename");
+$beta_file = realpath("$script_dir/beta/$filename");
+
+// Set default path
+$pdf_path = "/alpha/$filename";
+
+// Check if file exists in alpha directory, otherwise try beta
+if (!$alpha_file && $beta_file) {
+ $pdf_path = "/beta/$filename";
+}
+
+// Dynamically generate the base URL
+$base_url = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') . '/';
+?>
+<!DOCTYPE html>
+<html dir="ltr" lang="en" mozdisallowselectionprint>
+<head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <base href="<?= $base_url ?>" />
+ <title>NotaNexus: <?= $filename ?></title>
+ <link rel='stylesheet' type='text/css' href='styles/main.css'>
+</head>
+<body tabindex="1">
+ <div id='pdf' data-filename='<?= $pdf_path ?>'></div>
+ <script type="module" src='js/pdf.min.mjs'></script>
+ <script type='module' src='js/viewer.js'></script>
+</body>
+</html>
+
favicon/android-chrome-192x192.png
Binary files differ
favicon/android-chrome-512x512.png
Binary files differ
favicon/apple-touch-icon.png
Binary files differ
favicon/browserconfig.xml
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+ <msapplication>
+ <tile>
+ <square150x150logo src="/mstile-150x150.png"/>
+ <TileColor>#da532c</TileColor>
+ </tile>
+ </msapplication>
+</browserconfig>
favicon/build.sh
+#!/bin/bash
+
+# Relative path to location of SVG file to make into ICO file.
+ICON_PATH=../../images/edible.svg
+
+ICON_BASE=$(basename "$ICON_PATH")
+ICON_DIR=$(dirname "$ICON_PATH")
+ICON_FILE="${ICON_BASE%*.}"
+ICON_EXT="${ICON_BASE##*.}"
+
+FAVICON_FILE=favicon
+FAVICON_EXT=.ico
+
+# This uses rsvg-convert to create crisp PNG icons.
+for size in 16 32 64 128 150 192 512; do
+ ICON_OUT=$ICON_FILE-${size}.png
+ DIMENSIONS=${size}x${size}
+
+ echo "Create ${ICON_OUT}"
+
+ #rsvg-convert -w $size -p 300 -d 300 $ICON_PATH > $ICON_OUT
+ inkscape \
+ --batch-process \
+ -d 300 \
+ -w $size \
+ --export-type=png \
+ -o $ICON_OUT \
+ $ICON_PATH
+
+ # Center the image and make it square.
+ convert $ICON_OUT -gravity center -background transparent \
+ -resize $DIMENSIONS -extent $DIMENSIONS temp-$ICON_OUT
+
+ # Use 8-bit colour to reduce the file size.
+ pngquant 256 < temp-$ICON_OUT > $FAVICON_FILE-$DIMENSIONS.png
+done
+
+# Create a multi-sized ICO file.
+convert \
+ -resize x16 \
+ -gravity center \
+ -crop 16x16+0+0 \
+ -flatten \
+ -colors 256 \
+ -background transparent \
+ -define icon:auto-resize=32,16 \
+ $FAVICON_FILE-512x512.png \
+ ../$FAVICON_FILE$FAVICON_EXT
+
+# Create Android icons.
+mv $FAVICON_FILE-192x192.png android-chrome-192x192.png
+mv $FAVICON_FILE-512x512.png android-chrome-512x512.png
+
+# Create MS tile icon.
+mv $FAVICON_FILE-150x150.png mstile-150x150.png
+
+# Clean up the temporary files.
+rm ${ICON_FILE}*png temp-${ICON_FILE}*png
+
favicon/favicon-128x128.png
Binary files differ
favicon/favicon-16x16.png
Binary files differ
favicon/favicon-32x32.png
Binary files differ
favicon/favicon-64x64.png
Binary files differ
favicon/favicon.ico
Binary files differ
favicon/manifest.json
-
+{
+ "name": "",
+ "icons": [
+ {
+ "src": "/android-chrome-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/android-chrome-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "theme_color": "#ffffff",
+ "background_color": "#ffffff",
+ "display": "standalone"
+}
favicon/mstile-150x150.png
Binary files differ
favicon/safari-pinned-tab.svg
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="1056.000000pt" height="1056.000000pt" viewBox="0 0 1056.000000 1056.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+</metadata>
+<g transform="translate(0.000000,1056.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M1284 10501 c3 -5 8 -28 11 -52 2 -24 7 -55 9 -69 3 -14 8 -43 11
+-65 4 -22 9 -51 11 -65 3 -14 7 -38 9 -55 5 -32 49 -237 64 -295 6 -19 17 -62
+26 -95 15 -57 24 -95 29 -122 1 -7 5 -16 9 -19 4 -4 7 -14 7 -23 0 -16 36
+-140 50 -176 5 -11 18 -51 31 -90 12 -38 24 -74 28 -80 3 -5 6 -14 7 -20 8
+-44 91 -244 180 -433 29 -62 52 -114 51 -115 -1 -1 8 -15 21 -31 12 -16 22
+-34 22 -41 0 -7 7 -18 15 -25 8 -7 15 -21 15 -31 0 -11 3 -19 8 -19 4 0 20
+-24 36 -53 16 -28 40 -66 53 -83 13 -17 23 -36 23 -42 0 -6 3 -12 8 -14 4 -1
+23 -25 42 -53 77 -112 171 -224 280 -334 64 -64 125 -123 137 -133 120 -93
+157 -120 211 -157 170 -114 470 -234 642 -257 25 -3 54 -8 65 -11 77 -18 506
+-25 625 -11 30 4 73 9 95 12 22 2 63 10 90 16 28 6 59 13 69 16 36 7 121 37
+174 60 28 13 55 23 58 23 4 0 10 3 13 8 16 23 115 85 119 74 2 -6 10 -18 18
+-25 8 -8 14 -17 14 -20 0 -8 64 -101 73 -104 4 -2 7 -8 7 -14 0 -5 12 -28 28
+-51 58 -90 152 -259 152 -276 0 -6 5 -11 12 -11 6 0 9 -3 5 -6 -3 -4 4 -27 17
+-53 34 -67 78 -208 80 -256 1 -24 -27 -26 -129 -7 -38 7 -86 15 -105 17 -19 2
+-46 6 -60 9 -14 2 -88 14 -165 26 -197 30 -198 30 -215 35 -8 2 -42 7 -75 10
+-32 4 -63 9 -67 11 -5 3 -30 7 -56 9 -26 2 -47 4 -47 5 0 2 -11 4 -80 14 -22
+3 -51 8 -65 11 -13 2 -42 7 -65 10 -22 3 -67 11 -100 16 -33 6 -78 13 -100 16
+-22 2 -58 6 -80 9 -62 8 -262 8 -340 1 -74 -8 -91 -10 -170 -27 -27 -6 -59
+-13 -70 -15 -42 -8 -61 -14 -88 -24 -15 -6 -31 -10 -35 -9 -4 1 -16 -2 -27 -6
+-11 -5 -63 -24 -115 -44 -52 -20 -97 -37 -100 -37 -15 -3 -166 -81 -168 -87
+-2 -5 -9 -8 -16 -8 -10 0 -175 -102 -209 -130 -6 -5 -48 -37 -91 -71 -202
+-158 -407 -400 -527 -624 -12 -22 -27 -43 -33 -47 -6 -4 -8 -8 -4 -8 4 0 -7
+-30 -26 -67 -50 -102 -119 -283 -132 -348 -2 -11 -13 -54 -24 -95 -18 -65 -36
+-160 -54 -280 -21 -134 -27 -434 -12 -575 5 -44 10 -98 12 -120 2 -22 5 -53 8
+-70 7 -47 17 -105 22 -128 2 -12 6 -37 9 -57 3 -20 8 -47 11 -60 3 -14 7 -38
+9 -55 2 -16 4 -32 5 -35 1 -3 2 -8 1 -12 0 -5 2 -8 6 -8 5 0 9 -10 9 -22 3
+-56 81 -386 126 -533 12 -38 21 -74 19 -79 -1 -6 2 -19 8 -30 9 -18 33 -102
+46 -161 3 -11 9 -31 14 -45 14 -37 73 -228 73 -237 0 -4 4 -13 8 -18 5 -6 11
+-22 14 -35 6 -33 41 -128 49 -137 4 -3 7 -15 7 -25 0 -10 4 -26 10 -36 5 -9
+26 -64 46 -122 20 -58 46 -125 57 -150 11 -24 20 -49 22 -55 1 -5 7 -23 13
+-38 7 -16 12 -30 12 -33 0 -2 10 -26 21 -52 26 -58 40 -90 73 -169 89 -214
+264 -571 364 -745 12 -21 30 -53 41 -70 44 -77 84 -144 121 -200 22 -32 40
+-63 40 -68 0 -5 3 -10 8 -12 4 -2 27 -32 50 -67 61 -89 86 -125 93 -131 3 -3
+25 -30 48 -60 119 -152 249 -286 382 -395 13 -11 58 -42 99 -70 41 -27 78 -54
+81 -58 3 -5 9 -8 13 -7 4 1 31 -10 60 -24 56 -27 112 -47 186 -67 57 -15 228
+-20 283 -7 23 5 53 11 67 13 14 2 37 8 53 14 15 6 38 13 50 15 27 6 147 50
+173 64 11 6 21 10 24 10 6 1 42 14 115 42 28 10 89 31 137 46 l88 26 37 -19
+c175 -90 227 -114 285 -133 37 -12 69 -20 70 -19 2 1 12 -3 24 -9 11 -6 43
+-14 70 -18 27 -3 52 -8 56 -10 9 -6 204 -5 238 1 14 2 39 7 55 10 17 4 35 8
+40 9 23 5 92 29 122 42 17 8 34 14 37 14 10 0 180 101 187 111 10 17 69 18
+124 3 28 -8 57 -16 65 -18 8 -3 16 -5 18 -6 1 -2 5 -3 10 -4 9 -2 65 -23 110
+-42 64 -28 103 -44 138 -60 56 -25 201 -73 242 -81 104 -21 304 -2 432 39 36
+12 90 33 120 46 30 14 58 26 62 26 4 1 13 7 19 13 7 7 17 13 21 13 17 0 146
+86 223 149 84 69 216 199 275 271 19 23 40 48 45 54 6 6 26 31 45 56 19 25 37
+47 40 50 3 3 10 13 15 22 6 9 40 62 78 117 37 55 76 115 86 133 10 18 42 72
+70 120 57 99 211 410 211 427 0 6 5 11 12 11 6 0 9 3 5 6 -3 3 5 25 18 47 12
+23 24 47 25 52 4 15 17 50 40 103 12 26 27 61 35 77 7 17 28 71 47 120 96 248
+113 290 114 295 5 28 18 67 24 75 4 6 15 35 24 65 9 30 21 63 27 72 5 10 9 27
+9 37 0 11 4 22 9 25 4 3 12 20 16 38 4 18 9 38 11 43 2 6 17 51 34 100 17 50
+33 97 35 105 2 8 11 37 20 64 9 26 18 56 21 65 4 19 43 139 51 161 4 8 7 17 8
+20 0 3 11 39 25 80 13 41 24 80 26 85 1 6 14 47 29 93 15 46 28 89 29 95 1 7
+4 17 8 22 3 6 9 26 13 45 4 19 10 40 13 45 4 6 7 15 8 20 1 6 10 37 19 70 26
+88 104 412 110 454 3 20 8 48 11 61 2 14 7 39 9 55 10 70 16 112 20 135 19
+120 26 480 12 640 -3 41 -7 75 -7 75 -2 0 -8 36 -25 150 -4 22 -13 61 -20 88
+-8 26 -11 47 -7 47 3 0 1 5 -6 12 -7 7 -12 22 -12 34 0 13 -4 26 -10 29 -5 3
+-10 16 -10 29 0 12 -3 26 -7 30 -4 3 -8 14 -9 23 -4 24 -68 178 -77 181 -4 2
+-7 10 -7 18 0 7 -10 30 -21 51 -12 21 -31 56 -42 78 -22 42 -134 216 -161 250
+-9 11 -30 38 -47 60 -17 22 -47 60 -67 85 -42 52 -272 286 -272 277 0 -3 -18
+11 -40 31 -53 50 -136 112 -148 112 -6 0 -12 4 -14 9 -3 8 -202 131 -212 131
+-2 0 -37 15 -78 34 -40 18 -95 41 -123 50 -27 10 -57 20 -65 23 -8 3 -19 7
+-25 8 -5 2 -38 10 -71 19 -34 9 -79 18 -100 21 -22 3 -50 7 -64 10 -52 12
+-149 18 -275 19 -147 0 -273 -12 -445 -40 -14 -2 -38 -6 -55 -8 -38 -5 -104
+-15 -131 -20 -12 -2 -45 -7 -75 -11 -30 -3 -61 -8 -69 -10 -15 -4 -82 -14
+-130 -20 -65 -8 -116 -17 -129 -21 -8 -3 -35 -7 -60 -10 -26 -3 -100 -13 -166
+-24 -66 -10 -138 -22 -160 -25 -23 -3 -52 -8 -65 -10 -51 -10 -109 -20 -175
+-30 -23 -3 -52 -8 -65 -10 -14 -3 -38 -7 -55 -10 -16 -2 -41 -7 -54 -9 -17 -4
+-24 0 -28 14 -3 11 -8 20 -12 20 -4 0 -15 15 -25 33 -51 87 -372 573 -398 601
+-10 11 -18 26 -18 33 0 7 -4 13 -10 13 -5 0 -10 4 -10 10 0 5 -6 18 -14 27 -8
+10 -49 65 -92 123 -42 58 -80 107 -84 110 -3 3 -17 21 -30 40 -14 19 -34 46
+-46 60 -69 80 -114 130 -149 164 -22 22 -31 32 -19 23 11 -10 26 -17 32 -17 7
+0 12 -4 12 -10 0 -5 6 -10 14 -10 8 0 41 -17 73 -38 88 -58 91 -58 98 -8 6 36
+1 271 -5 271 -2 0 -3 9 -15 75 -17 96 -122 341 -166 389 -11 11 -19 23 -19 27
+0 3 -10 19 -22 34 -13 15 -33 42 -46 58 -33 43 -127 138 -192 194 -92 78 -232
+178 -251 178 -5 0 -9 3 -9 8 0 4 -10 12 -23 18 -12 6 -74 38 -137 70 -64 33
+-131 65 -150 73 -19 8 -53 22 -75 31 -22 10 -56 23 -75 30 -19 7 -44 16 -55
+21 -23 9 -115 40 -130 43 -5 1 -32 10 -60 19 -27 9 -61 19 -75 21 -14 3 -47
+12 -75 21 -27 9 -66 18 -85 21 -19 3 -42 8 -50 11 -14 5 -68 18 -120 28 -14 3
+-41 8 -60 12 -19 4 -42 8 -50 9 -8 1 -26 4 -40 7 -14 3 -70 15 -125 27 -55 11
+-111 23 -125 26 -14 3 -29 7 -35 8 -5 1 -37 10 -70 20 -33 10 -64 19 -70 21
+-10 2 -21 6 -80 28 -16 6 -34 13 -40 14 -87 19 -432 196 -475 243 -3 3 -21 16
+-40 30 -71 50 -179 158 -233 231 -17 24 -34 46 -37 49 -18 18 -90 165 -90 186
+0 8 -5 14 -11 14 -5 0 -8 -4 -5 -9z"/>
+</g>
+</svg>
favicon.ico
Binary files differ
head.html
+<head>
+<link
+ href="//cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css"
+ rel="stylesheet" type="text/css" />
+<link
+ href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200"
+ rel="stylesheet" />
+<link
+ href="//fonts.googleapis.com/css?family=Libre+Baskerville"
+ rel="stylesheet" />
+<link
+ href="themes/simple.css"
+ rel="stylesheet" type="text/css" />
+</head>
header.html
+<link
+ href="//cdnjs.cloudflare.com/ajax/libs/normalize/7.0.0/normalize.min.css"
+ rel="stylesheet" type="text/css" />
+<link
+ href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200"
+ rel="stylesheet" />
+<link
+ href="//fonts.googleapis.com/css?family=Libre+Baskerville"
+ rel="stylesheet" />
+<link
+ href="themes/simple.css"
+ rel="stylesheet" type="text/css" />
+
images/farmer.jpg
Binary files differ
index.html
+<!DOCTYPE HTML><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1user-scalable=yes"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="description" content="A hard sci-fi novel about automated food production, sentient machines, and surveillance societies."><link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png"><title>autónoma</title><style media="screen" type="text/css">
+/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}/*# sourceMappingURL=normalize.min.css.map */
+ </style><link rel="stylesheet" type="text/css" media="screen" href="themes/simple.css"><link href="//fonts.googleapis.com/css?family=Source+Sans+Pro:200|Libre+Baskerville" rel="stylesheet"></head><body><div class="page"><header class="section header"><h1>autónoma</h1></header><nav class="section menu"></nav><main class="section content"><div class="illustration"><img width="408" height="359" src="images/farmer.jpg" alt="Android Farmer"></div><p class="quote"><q>Free the food, free the people.</q> ~ Chloé Angelos, 2058.</p></main><footer class="section footer"></footer></div></body></html>
js/Annotation.js
+const PAGE_NUMBER_OFFSET = 6;
+
+export default class Annotation {
+ constructor(data) {
+ if (typeof data === 'object' && data !== null) {
+ this.id = data.id || this.generateId();
+ this.text = data.text;
+ this.note = data.note || '';
+ this.pageNumber = data.pageNumber;
+ this.timestamp = data.timestamp ? new Date(data.timestamp) : new Date();
+ this.regions = Array.isArray(data.regions) ? [...data.regions] : [];
+ } else {
+ const selectedText = arguments[0];
+ const pageNumber = arguments[1];
+ const note = arguments[2] || '';
+ const existingId = arguments[3] || null;
+ const timestamp = arguments[4] || new Date();
+
+ this.id = existingId || this.generateId();
+ this.text = selectedText;
+ this.note = note;
+ this.pageNumber = pageNumber;
+ this.timestamp = timestamp;
+ this.regions = [];
+ }
+ }
+
+ generateId() {
+ return Math.floor(1000000 + Math.random() * 10000000);
+ }
+
+ forEachRegion(callback) {
+ this.regions.forEach((region, index) => {
+ callback(region, index);
+ });
+ }
+
+ addPdfRegion(region) {
+ this.regions.push(region);
+ }
+
+ addRegion(element) {
+ const randomId = this.generateId();
+ const elementId = element.id || `region-${randomId}`;
+
+ const existingRegion = this.regions.find(r => r.id === elementId);
+ if (existingRegion) {
+ return;
+ }
+
+ this.regions.push({
+ id: elementId,
+ left: parseFloat(element.style.left),
+ top: parseFloat(element.style.top),
+ width: parseFloat(element.style.width),
+ height: parseFloat(element.style.height)
+ });
+
+ element.setAttribute('data-annotation-id', this.id);
+ }
+
+ removeRegion(elementIdOrElement) {
+ const elementId = typeof elementIdOrElement === 'string' ?
+ elementIdOrElement :
+ elementIdOrElement.id;
+
+ const index = this.regions.findIndex(r => r.id === elementId);
+ if (index !== -1) {
+ this.regions.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+
+ hasRegions() {
+ return this.regions.length > 0;
+ }
+
+ updateNote(newNote) {
+ this.note = newNote;
+ this._log();
+ }
+
+ toJson() {
+ return {
+ id: this.id,
+ text: this.text,
+ note: this.note,
+ pageNumber: this.pageNumber,
+ timestamp: this.timestamp,
+ regions: this.regions
+ };
+ }
+
+ static fromJson(data) {
+ return new Annotation(data);
+ }
+
+ save() {
+ this._log();
+ return this;
+ }
+
+ toString() {
+ const displayPageNum = parseInt(this.pageNumber) - PAGE_NUMBER_OFFSET;
+ let result = '-'.repeat(5) + `| Page ${displayPageNum} |` + '-'.repeat(30) + '\n';
+ result += ` > "${this.text}"\n`;
+ result += ` * ${this.note}\n\n`;
+ return result;
+ }
+
+ _log() {
+ console.log('Annotation:', this.toJson());
+ }
+}
+
js/DialogBox.js
+export default class DialogBox {
+ constructor() {
+ this.dialog = null;
+ this.isDragging = false;
+ this.dragOffset = { x: 0, y: 0 };
+
+ this._isEdit = false;
+ this._existingNote = null;
+ this._highlightElement = null;
+ this._onSave = null;
+ this._onCancel = null;
+ this._onDelete = null;
+ this._timestamp = null;
+ }
+
+ show(
+ selectionRect,
+ isEdit = false,
+ existingNote = null,
+ onSave,
+ onCancel,
+ onDelete,
+ highlightElement = null,
+ timestamp
+ ) {
+ this._removeExistingDialog();
+
+ this._isEdit = isEdit;
+ this._existingNote = existingNote;
+ this._highlightElement = highlightElement;
+ this._onSave = onSave;
+ this._onCancel = onCancel;
+ this._onDelete = onDelete;
+ this._timestamp = timestamp;
+
+ const dialog = document.createElement("div");
+ dialog.id = "dialog";
+ dialog.className = "dialog";
+
+ this._positionDialog(dialog, selectionRect);
+
+ const titleBar = this._createTitleBar();
+ const textarea = this._createTextArea();
+
+ dialog.appendChild(titleBar);
+ dialog.appendChild(textarea);
+
+ document.body.appendChild(dialog);
+ this.dialog = dialog;
+
+ textarea.focus();
+ }
+
+ handleOutsideClick(event) {
+ if (this._isClickOutside(event.target)) {
+ this._processCloseAction();
+ }
+ }
+
+ _positionDialog(dialog, selectionRect) {
+ const scrollX = window.scrollX;
+ const scrollY = window.scrollY;
+ dialog.style.left = `${selectionRect.left + scrollX}px`;
+ dialog.style.top = `${selectionRect.bottom + scrollY + 10}px`;
+ }
+
+ _createTitleBar() {
+ const titleBar = document.createElement("div");
+ titleBar.className = "dialogTitleBar";
+
+ const displayTimestamp = this._formatTimestamp(
+ this._isEdit && this._existingNote
+ ? this._existingNote.timestamp
+ : this._timestamp
+ );
+
+ const timestampElement = document.createElement("span");
+ timestampElement.className = "dialogTimestamp";
+ timestampElement.textContent = displayTimestamp;
+
+ const closeButton = document.createElement("span");
+ closeButton.className = "dialogCloseButton";
+ closeButton.textContent = "×";
+ closeButton.addEventListener("click", () => {
+ this._processCloseAction();
+ });
+
+ titleBar.appendChild(timestampElement);
+ titleBar.appendChild(closeButton);
+ this._setupDragHandlers(titleBar);
+
+ return titleBar;
+ }
+
+ _createTextArea() {
+ const textarea = document.createElement("textarea");
+ textarea.placeholder = "Add your note...";
+ textarea.rows = 3;
+ textarea.className = "dialogTextarea";
+
+ if (this._isEdit && this._existingNote) {
+ textarea.value = this._existingNote.note;
+ }
+
+ textarea.addEventListener("keydown", (event) => {
+ if (event.key === "Escape") {
+ this._processCloseAction();
+ }
+ });
+
+ return textarea;
+ }
+
+ _processCloseAction() {
+ const noteText = this._getTextContent().trim();
+ if (noteText === "") {
+ this._onDelete();
+ } else {
+ this._onSave(noteText);
+ }
+ this._removeExistingDialog();
+ }
+
+ _setupDragHandlers(titleBar) {
+ titleBar.style.cursor = "move";
+
+ titleBar.addEventListener("mousedown", (e) => {
+ if (e.button === 0 && !e.target.classList.contains("dialogCloseButton")) {
+ this.isDragging = true;
+ const rect = this.dialog.getBoundingClientRect();
+ this.dragOffset = {
+ x: e.clientX - rect.left,
+ y: e.clientY - rect.top,
+ };
+ e.preventDefault();
+ }
+ });
+
+ document.addEventListener("mousemove", (e) => {
+ if (this.isDragging) {
+ const x = e.clientX - this.dragOffset.x;
+ const y = e.clientY - this.dragOffset.y;
+ this.dialog.style.left = `${x}px`;
+ this.dialog.style.top = `${y}px`;
+ }
+ });
+
+ document.addEventListener("mouseup", () => {
+ this.isDragging = false;
+ });
+ }
+
+ _removeExistingDialog() {
+ if (this.dialog) {
+ this.dialog.remove();
+ this.dialog = null;
+ }
+ }
+
+ _isClickOutside(target) {
+ return this.dialog && !this.dialog.contains(target);
+ }
+
+ _getTextContent() {
+ const textarea = this.dialog?.querySelector(".dialogTextarea");
+ return textarea ? textarea.value : "";
+ }
+
+ _formatTimestamp(date) {
+ const formatDate = new Intl.DateTimeFormat("en", {
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ });
+
+ const parts = formatDate.formatToParts(date);
+ return `${parts.find((part) => part.type === "year").value}-${
+ parts.find((part) => part.type === "month").value
+ }-${parts.find((part) => part.type === "day").value} ${
+ parts.find((part) => part.type === "hour").value
+ }:${parts.find((part) => part.type === "minute").value}:${
+ parts.find((part) => part.type === "second").value
+ }`;
+ }
+}
+
js/Highlighter.js
+import DialogBox from './DialogBox.js';
+import Annotation from './Annotation.js';
+import Navigator from './Navigator.js';
+import Summary from './Summary.js';
+
+export default class Highlighter {
+ constructor(pdf) {
+ this.pdf = pdf;
+ this.annotations = [];
+ this.currentSelection = {
+ range: null,
+ text: '',
+ pageNumber: null,
+ pageElement: null
+ };
+
+ this.dialogBox = new DialogBox();
+
+ this.handleTextSelection = this.handleTextSelection.bind(this);
+ this.handleAnnotationClick = this.handleAnnotationClick.bind(this);
+ this.handleDocumentClick = this.handleDocumentClick.bind(this);
+
+ const path = document.getElementById('pdf').getAttribute('data-filename');
+ this.filename = path.split('/').pop();
+
+ this.loadAnnotationsFromServer();
+ }
+
+ setupEventListeners() {
+ document.addEventListener('mouseup', this.handleTextSelection);
+ document.addEventListener('touchend', this.handleTextSelection);
+ document.addEventListener('click', this.handleAnnotationClick);
+ document.addEventListener('mousedown', this.handleDocumentClick);
+
+ const toolbar = document.createElement('div');
+ toolbar.className = 'navToolbar';
+ document.body.appendChild(toolbar);
+
+ this.navigator = new Navigator(this, toolbar);
+ this.summary = new Summary(this, toolbar);
+ }
+
+ async loadAnnotationsFromServer() {
+ try {
+ const response = await fetch(`nn/${this.filename.replace('.pdf', '')}.json`, {
+ method: 'GET',
+ headers: {
+ 'Cache-Control': 'no-cache'
+ }
+ });
+
+ if (!response.ok) {
+ console.log('No existing annotations found or unable to load annotations');
+ return;
+ }
+
+ const annotationsData = await response.json();
+
+ for (const item of annotationsData) {
+ this.annotations.push(new Annotation(item));
+ }
+
+ this.waitForPdfRenderAndCreateHighlights(annotationsData);
+
+ console.log(`Loaded ${annotationsData.length} annotations`);
+ } catch (error) {
+ console.error('Error loading annotations:', error);
+ }
+ }
+
+ waitForPdfRenderAndCreateHighlights(annotationsData) {
+ const checkInterval = 100;
+ const maxAttempts = 50;
+ let attempts = 0;
+
+ const checkPagesReady = () => {
+ attempts++;
+ const allPages = document.querySelectorAll('.page');
+ const targetPage = Math.max(...annotationsData.map(a => parseInt(a.pageNumber)));
+
+ if (allPages.length >= targetPage) {
+ this.createHighlightsFromAnnotations(annotationsData);
+ return;
+ }
+
+ if (attempts < maxAttempts) {
+ setTimeout(checkPagesReady, checkInterval);
+ } else {
+ console.error('Timed out waiting for PDF pages to render');
+ }
+ };
+
+ setTimeout(checkPagesReady, checkInterval);
+ }
+
+ createHighlightsFromAnnotations(annotationsData) {
+ for (const annotationData of annotationsData) {
+ const annotation = this.annotations.find(a => a.id === annotationData.id);
+ if (!annotation) continue;
+
+ const pageNumber = parseInt(annotation.pageNumber);
+ const pageElement = document.querySelector(`.page[data-page-number="${pageNumber}"]`);
+ if (!pageElement) continue;
+
+ let highlightLayer = pageElement.querySelector('.highlightLayer');
+ if (!highlightLayer) {
+ highlightLayer = document.createElement('div');
+ highlightLayer.className = 'highlightLayer';
+ pageElement.appendChild(highlightLayer);
+ }
+
+ const page = this.pdf.pages[pageNumber - 1];
+ if (!page) continue;
+
+ for (const regionData of annotationData.regions) {
+ const highlight = document.createElement('div');
+ highlight.className = 'customHighlight';
+ highlight.id = regionData.id || `highlight-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ highlight.setAttribute('data-annotation-id', annotation.id);
+
+ // Convert PDF coordinates to viewport coordinates
+ const viewport = page.viewport;
+ const rect = viewport.convertToViewportRectangle([
+ regionData.pdfX,
+ regionData.pdfY,
+ regionData.pdfX + regionData.pdfWidth,
+ regionData.pdfY - regionData.pdfHeight
+ ]);
+
+ highlight.style.left = Math.min(rect[0], rect[2]) + 'px';
+ highlight.style.top = Math.min(rect[1], rect[3]) + 'px';
+ highlight.style.width = Math.abs(rect[2] - rect[0]) + 'px';
+ highlight.style.height = Math.abs(rect[3] - rect[1]) + 'px';
+
+ highlight.addEventListener('mouseover', () => {
+ this.showAnnotationTooltip(annotation, highlight);
+ });
+
+ highlight.addEventListener('mouseout', () => {
+ const tooltip = document.getElementById('annotationTooltip');
+ if (tooltip) {
+ tooltip.remove();
+ }
+ });
+
+ highlightLayer.appendChild(highlight);
+ }
+ }
+ }
+
+ handleDocumentClick(event) {
+ this.dialogBox.handleOutsideClick(event);
+ }
+
+ handleTextSelection(event) {
+ const selection = window.getSelection();
+ if (!selection || selection.isCollapsed) return;
+
+ const selectedText = selection.toString().trim();
+ if (selectedText.length === 0) return;
+
+ const textLayers = this.pdf.getTextLayers();
+ let isWithinTextLayer = false;
+
+ for (const textLayer of textLayers) {
+ if (
+ textLayer.contains(selection.anchorNode) ||
+ textLayer.contains(selection.focusNode) ||
+ this.isTextLayerChild(selection.anchorNode, textLayer) ||
+ this.isTextLayerChild(selection.focusNode, textLayer)
+ ) {
+ isWithinTextLayer = true;
+ const pageElement = textLayer.closest('.page');
+
+ this.currentSelection = {
+ range: selection.getRangeAt(0).cloneRange(),
+ text: selectedText,
+ pageNumber: pageElement.getAttribute('data-page-number'),
+ pageElement: pageElement
+ };
+
+ break;
+ }
+ }
+
+ if (!isWithinTextLayer) return;
+
+ const selectionRect = this.currentSelection.range.getBoundingClientRect();
+ this.showAnnotationPopup(selectionRect);
+ }
+
+ isTextLayerChild(node, textLayer) {
+ if (!node || !node.parentNode) return false;
+ let parent = node.parentNode;
+ while (parent) {
+ if (parent === textLayer) return true;
+ parent = parent.parentNode;
+ }
+ return false;
+ }
+
+ handleAnnotationClick(event) {
+ if (
+ event.target.classList.contains('customHighlight') &&
+ event.target.hasAttribute('data-annotation-id')
+ ) {
+ event.stopPropagation();
+ const annotationId = parseInt(
+ event.target.getAttribute('data-annotation-id')
+ );
+ const annotation = this.findAnnotationById(annotationId);
+ if (annotation) {
+ const rect = event.target.getBoundingClientRect();
+ this.showAnnotationPopup(rect, true, annotation, event.target);
+ }
+ }
+ }
+
+ findAnnotationById(id) {
+ return this.annotations.find(a => a.id === id);
+ }
+
+ showAnnotationPopup(
+ selectionRect,
+ isEdit = false,
+ existingAnnotation = null,
+ highlightElement = null
+ ) {
+ const timestamp = new Date();
+
+ const onSave = (annotationText) => {
+ if (isEdit && existingAnnotation) {
+ existingAnnotation.updateNote(annotationText);
+ this.saveAnnotationsToServer('update');
+ } else {
+ this.createAndSaveAnnotation(annotationText, timestamp);
+ }
+ };
+
+ const onDelete = () => {
+ if (isEdit && existingAnnotation) {
+ this.deleteAnnotation(existingAnnotation.id, highlightElement);
+ this.saveAnnotationsToServer('delete');
+ } else {
+ this.removeHighlights();
+ }
+ };
+
+ const onCancel = () => {};
+
+ this.dialogBox.show(
+ selectionRect,
+ isEdit,
+ existingAnnotation,
+ onSave,
+ onCancel,
+ onDelete,
+ highlightElement,
+ timestamp
+ );
+
+ if (!isEdit) {
+ this.highlightSelectedText();
+ }
+ }
+
+ createAndSaveAnnotation(annotationText, timestamp) {
+ const annotation = new Annotation(
+ this.currentSelection.text,
+ this.currentSelection.pageNumber,
+ annotationText,
+ null,
+ timestamp
+ );
+
+ this.annotations.push(annotation);
+
+ const customHighlights = document.querySelectorAll(
+ '.customHighlight:not([data-annotation-id])'
+ );
+
+ const pageNumber = parseInt(this.currentSelection.pageNumber);
+ const page = this.pdf.pages[pageNumber - 1];
+ const viewport = page.viewport;
+
+ customHighlights.forEach((highlight) => {
+ // Convert viewport coordinates to PDF coordinates before storing
+ const left = parseFloat(highlight.style.left);
+ const top = parseFloat(highlight.style.top);
+ const width = parseFloat(highlight.style.width);
+ const height = parseFloat(highlight.style.height);
+
+ // Convert from viewport coordinates to PDF user space coordinates
+ const pdfCoords = viewport.convertToPdfPoint(left, top);
+ const pdfBottomRight = viewport.convertToPdfPoint(left + width, top + height);
+
+ const pdfRegion = {
+ id: highlight.id,
+ pdfX: pdfCoords[0],
+ pdfY: pdfCoords[1],
+ pdfWidth: pdfBottomRight[0] - pdfCoords[0],
+ pdfHeight: pdfCoords[1] - pdfBottomRight[1],
+ // Keep viewport coordinates for reference
+ left: left,
+ top: top,
+ width: width,
+ height: height
+ };
+
+ annotation.addPdfRegion(pdfRegion);
+
+ highlight.setAttribute('data-annotation-id', annotation.id);
+
+ highlight.addEventListener('mouseover', () => {
+ this.showAnnotationTooltip(annotation, highlight);
+ });
+
+ highlight.addEventListener('mouseout', () => {
+ const tooltip = document.getElementById('annotationTooltip');
+ if (tooltip) {
+ tooltip.remove();
+ }
+ });
+ });
+
+ annotation.save();
+ this.saveAnnotationsToServer('add');
+ }
+
+ deleteAnnotation(annotationId, highlightElement) {
+ const annotation = this.findAnnotationById(annotationId);
+ if (!annotation) return;
+
+ const index = this.annotations.findIndex(a => a.id === annotationId);
+
+ if (highlightElement) {
+ annotation.removeRegion(highlightElement.id);
+ highlightElement.remove();
+ } else {
+ const highlights = document.querySelectorAll(
+ `.customHighlight[data-annotation-id="${annotationId}"]`
+ );
+ highlights.forEach(highlight => {
+ highlight.remove();
+ });
+ }
+
+ if (!annotation.hasRegions()) {
+ this.annotations.splice(index, 1);
+ }
+ }
+
+ showAnnotationTooltip(annotation, element) {
+ const tooltip = document.createElement('div');
+ tooltip.id = 'annotationTooltip';
+ tooltip.className = 'annotationTooltip';
+ tooltip.textContent = annotation.note;
+
+ const rect = element.getBoundingClientRect();
+ const scrollX = window.scrollX;
+ const scrollY = window.scrollY;
+
+ tooltip.style.left = rect.left + scrollX + 'px';
+ tooltip.style.top = rect.bottom + scrollY + 5 + 'px';
+
+ document.body.appendChild(tooltip);
+ }
+
+ highlightSelectedText() {
+ if (!this.currentSelection.range) return;
+
+ const pageNumber = parseInt(this.currentSelection.pageNumber);
+ let highlightLayer = this.currentSelection.pageElement.querySelector('.highlightLayer');
+ if (!highlightLayer) {
+ highlightLayer = document.createElement('div');
+ highlightLayer.className = 'highlightLayer';
+ this.currentSelection.pageElement.appendChild(highlightLayer);
+ }
+
+ const pageRect = this.currentSelection.pageElement.getBoundingClientRect();
+ const rects = this.currentSelection.range.getClientRects();
+
+ for (let i = 0; i < rects.length; i++) {
+ const rect = rects[i];
+
+ const highlight = document.createElement('div');
+ highlight.className = 'customHighlight';
+ highlight.id = `highlight-${Date.now()}-${i}`;
+
+ const relativeLeft = rect.left - pageRect.left;
+ const relativeTop = rect.top - pageRect.top;
+
+ highlight.style.left = relativeLeft + 'px';
+ highlight.style.top = relativeTop + 'px';
+ highlight.style.width = rect.width + 'px';
+ highlight.style.height = rect.height + 'px';
+
+ highlightLayer.appendChild(highlight);
+ }
+ }
+
+ removeHighlights() {
+ const customHighlights = document.querySelectorAll(
+ '.customHighlight:not([data-annotation-id])'
+ );
+ customHighlights.forEach((highlight) => {
+ highlight.remove();
+ });
+ }
+
+ forEachAnnotation(callback) {
+ this.annotations.forEach(callback);
+ }
+
+ _getAnnotations() {
+ return this.annotations.map(annotation => annotation.toJson());
+ }
+
+ async saveAnnotationsToServer(action) {
+ try {
+ const annotationsData = this._getAnnotations();
+
+ const formData = new FormData();
+ formData.append('action', action);
+ formData.append('filename', this.filename);
+ formData.append('annotations', JSON.stringify(annotationsData));
+
+ const response = await fetch('editor.php', {
+ method: 'POST',
+ body: formData
+ });
+
+ if (!response.ok) {
+ throw new Error(`Server responded with status: ${response.status}`);
+ }
+
+ console.log(`Annotations ${action}d successfully`);
+ } catch (error) {
+ console.error(`Error ${action}ing annotations:`, error);
+ }
+ }
+}
js/Navigator.js
+export default class Navigator {
+ constructor(highlighter, toolbar) {
+ this.highlighter = highlighter;
+ this.currentIndex = -1;
+
+ this._createButtons(toolbar);
+ this._setupEventListeners();
+ }
+
+ _createButtons(toolbar) {
+ this.prevButton = document.createElement('button');
+ this.prevButton.textContent = 'â—€';
+ this.prevButton.className = 'navButton navPrev';
+
+ this.nextButton = document.createElement('button');
+ this.nextButton.textContent = 'â–¶';
+ this.nextButton.className = 'navButton navNext';
+
+ toolbar.appendChild(this.prevButton);
+ toolbar.appendChild(this.nextButton);
+ }
+
+ _setupEventListeners() {
+ this.prevButton.addEventListener('click', () => this.backward());
+ this.nextButton.addEventListener('click', () => this.forward());
+ }
+
+ forward() {
+ this._navigate(1);
+ }
+
+ backward() {
+ this._navigate(-1);
+ }
+
+ _navigate(direction) {
+ let count = 0;
+ let targetId = null;
+ let firstId = null;
+ let lastId = null;
+ const nextIndex = this.currentIndex + direction;
+
+ this.highlighter.forEachAnnotation((annotation) => {
+ if (count === 0) firstId = annotation.id;
+ if (count === nextIndex) targetId = annotation.id;
+ lastId = annotation.id;
+ count++;
+ });
+
+ if (count === 0) return;
+
+ if (targetId === null) {
+ targetId = direction > 0 ? firstId : lastId;
+ this.currentIndex = direction > 0 ? 0 : count - 1;
+ } else {
+ this.currentIndex = nextIndex;
+ }
+
+ this._scrollTo(targetId);
+ }
+
+ _scrollTo(annotationId) {
+ const highlight = document.querySelector(`[data-annotation-id="${annotationId}"]`);
+ if (highlight) {
+ highlight.scrollIntoView({ block: 'center' });
+ }
+ }
+}
+
js/Page.js
+export default class Page {
+ constructor(viewerElement, pdfPage, pageNum, scale = 1.5) {
+ this.viewerElement = viewerElement;
+ this.pdfPage = pdfPage;
+ this.pageNum = pageNum;
+ this.scale = scale;
+ this.pageElement = null;
+ this.textLayer = null;
+ this.highlightLayer = null;
+ this.viewport = null;
+ }
+
+ async render() {
+ this.viewport = this.pdfPage.getViewport({ scale: this.scale });
+
+ this.pageElement = document.createElement('div');
+ this.pageElement.className = 'page';
+ this.pageElement.setAttribute('data-page-number', this.pageNum);
+ this.pageElement.style.width = this.viewport.width + 'px';
+ this.pageElement.style.height = this.viewport.height + 'px';
+
+ this.pageElement.style.setProperty('--scale-factor', this.scale);
+ this.viewerElement.appendChild(this.pageElement);
+
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ canvas.width = this.viewport.width;
+ canvas.height = this.viewport.height;
+ this.pageElement.appendChild(canvas);
+
+ const renderContext = {
+ canvasContext: context,
+ viewport: this.viewport
+ };
+
+ await this.pdfPage.render(renderContext).promise;
+
+ this.textLayer = document.createElement('div');
+ this.textLayer.className = 'textLayer';
+
+ this.textLayer.style.left = '0';
+ this.textLayer.style.top = '0';
+ this.textLayer.style.right = '0';
+ this.textLayer.style.bottom = '0';
+ this.textLayer.style.position = 'absolute';
+ this.textLayer.style.transformOrigin = '0% 0%';
+
+ this.textLayer.style.setProperty('--scale-factor', this.viewport.scale.toString());
+
+ this.pageElement.appendChild(this.textLayer);
+
+ this.highlightLayer = document.createElement('div');
+ this.highlightLayer.className = 'highlightLayer';
+ this.highlightLayer.style.left = '0';
+ this.highlightLayer.style.top = '0';
+ this.highlightLayer.style.right = '0';
+ this.highlightLayer.style.bottom = '0';
+ this.highlightLayer.style.position = 'absolute';
+ this.highlightLayer.style.transformOrigin = '0% 0%';
+
+ this.pageElement.appendChild(this.highlightLayer);
+
+ const textLayer = new pdfjsLib.TextLayer({
+ textContentSource: this.pdfPage.streamTextContent(),
+ container: this.textLayer,
+ viewport: this.viewport
+ });
+
+ await textLayer.render();
+ }
+
+ getPageElement() {
+ return this.pageElement;
+ }
+
+ getTextLayer() {
+ return this.textLayer;
+ }
+
+ getHighlightLayer() {
+ return this.highlightLayer;
+ }
+
+ getPageNumber() {
+ return this.pageNum;
+ }
+
+ getViewport() {
+ return this.viewport;
+ }
+}
js/Pdf.js
+import Page from './Page.js';
+import Highlighter from './Highlighter.js';
+
+const element = ( id ) => document.getElementById( id );
+
+export default class Pdf {
+ constructor( containerId ) {
+ const workerSrc = new URL('./pdf.worker.min.mjs', import.meta.url).href;
+ pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
+
+ this.container = element( containerId );
+ this.url = this.container.getAttribute( 'data-filename' );
+ this.pages = [];
+ this.scale = this.calculateScale();
+
+ window.addEventListener('resize', this.handleResize.bind(this));
+
+ this.load();
+ }
+
+ calculateScale() {
+ const maxWidth = window.innerWidth - 40;
+ return maxWidth;
+ }
+
+ handleResize() {
+ const newScale = this.calculateScale();
+ if (Math.abs(this.scale - newScale) > 20) {
+ this.scale = newScale;
+ this.container.innerHTML = '';
+ this.pages = [];
+ this.load();
+ }
+ }
+
+ async load() {
+ try {
+ const loadingTask = pdfjsLib.getDocument( this.url );
+ const pdfDoc = await loadingTask.promise;
+ this.pdfDoc = pdfDoc;
+ this.numPages = pdfDoc.numPages;
+ await this.render();
+ } catch (error) {
+ console.error('Error loading PDF:', error);
+ }
+ }
+
+ async render() {
+ const firstPage = await this.pdfDoc.getPage(1);
+ const viewport = firstPage.getViewport({ scale: 1.0 });
+
+ const scaleFactor = this.scale / viewport.width;
+
+ for( let pageNum = 1; pageNum <= this.pdfDoc.numPages; pageNum++ ) {
+ try {
+ const pdfPage = await this.pdfDoc.getPage( pageNum );
+ const page = new Page( this.container, pdfPage, pageNum, scaleFactor );
+ await page.render();
+ this.pages.push( page );
+ } catch (error) {
+ console.error(`Error rendering page ${pageNum}:`, error);
+ }
+ }
+
+ this.initializeHighlighter();
+ }
+
+ initializeHighlighter() {
+ this.highlighter = new Highlighter( this );
+ this.highlighter.setupEventListeners();
+ return this.highlighter;
+ }
+
+ getHighlighter() {
+ return this.highlighter;
+ }
+
+ getPageCount() {
+ return this.pdfDoc ? this.pdfDoc.numPages : 0;
+ }
+
+ getPageElements() {
+ return this.pages.map( page => page.getPageElement() );
+ }
+
+ getTextLayers() {
+ return this.pages.map( page => page.getTextLayer() );
+ }
+}
+
js/Summary.js
+export default class Summary {
+ constructor(highlighter, toolbar) {
+ this.highlighter = highlighter;
+ this.summaryDialog = null;
+ this._createButton(toolbar);
+ }
+
+ _createButton(toolbar) {
+ this.summaryButton = document.createElement('button');
+ this.summaryButton.textContent = '📋';
+ this.summaryButton.className = 'navButton summaryButton';
+ this.summaryButton.addEventListener('click', () => this.showSummary());
+
+ if (toolbar) {
+ toolbar.appendChild(this.summaryButton);
+ }
+ }
+
+ showSummary() {
+ if (this.summaryDialog) {
+ this.summaryDialog.remove();
+ }
+
+ const dialog = document.createElement('div');
+ dialog.className = 'summaryDialog';
+
+ const closeButton = document.createElement('button');
+ closeButton.textContent = '×';
+ closeButton.className = 'summaryCloseButton';
+ closeButton.addEventListener('click', () => this.closeSummary());
+
+ const content = document.createElement('textarea');
+ content.className = 'summaryContent';
+ content.readOnly = true;
+ content.value = this._generateSummary();
+
+ dialog.appendChild(closeButton);
+ dialog.appendChild(content);
+
+ document.body.appendChild(dialog);
+ this.summaryDialog = dialog;
+ }
+
+ closeSummary() {
+ if (this.summaryDialog) {
+ this.summaryDialog.remove();
+ this.summaryDialog = null;
+ }
+ }
+
+ _generateSummary() {
+ let summary = "";
+
+ this.highlighter.forEachAnnotation(annotation => {
+ summary += annotation.toString();
+ });
+
+ return summary.trim();
+ }
+}
+
js/index.html
js/pdf.min.mjs
Binary files differ
js/pdf.mjs.map
Binary files differ
js/pdf.worker.min.mjs
Binary files differ
js/viewer.js
+import Pdf from './Pdf.js';
+
+document.addEventListener('DOMContentLoaded', function() {
+ const pdf = new Pdf('pdf');
+});
+
model.png
Binary files differ
nn/.gitignore
+*.json
+
nn/.placeholder
nn/index.html
robots.txt
+User-agent: googlebot
+User-agent: google
+User-agent: bingbot
+User-agent: bing
+User-agent: DuckDuckBot/1.0
+User-agent: DuckDuckBot/1.1
+Disallow: /crawler
+
+User-agent: *
+Disallow: /
+
security.txt
sitemap.xml
+<?xml version="1.0" encoding="UTF-8"?>
+<urlset
+ xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
+ http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
+<url>
+ <loc>https://autonoma.ca/</loc>
+ <lastmod>2025-05-21T00:14:53+00:00</lastmod>
+</url>
+</urlset>
styles/main.css
+body {
+ margin: 0;
+ padding: 20px;
+ background-color: #1a1a1a;
+}
+
+#pdf {
+ position: relative;
+ margin: 0 auto;
+ background-color: transparent;
+ box-shadow: none;
+}
+
+.page {
+ position: relative;
+ margin-bottom: 20px;
+ overflow: hidden;
+ background-color: #fff;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
+ border-radius: 4px;
+ margin-bottom: 30px;
+}
+
+.textLayer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ overflow: hidden;
+ opacity: 0.2;
+ user-select: text;
+ transform-origin: 0 0;
+ line-height: 1.0;
+}
+
+.textLayer > span,
+.textLayer > .endOfContent {
+ color: transparent;
+ position: absolute;
+ white-space: pre;
+ cursor: text;
+ transform-origin: 0% 0%;
+}
+
+.textLayer .endOfContent {
+ display: inline-block;
+ pointer-events: none;
+}
+
+.textLayer .highlight {
+ margin: -1px;
+ padding: 1px;
+ background-color: rgba(255, 255, 0, 0.4);
+ border-radius: 4px;
+}
+
+.highlightLayer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+ transform-origin: 0 0;
+}
+
+.customHighlight {
+ position: absolute;
+ background-color: rgba(255, 255, 0, 0.3);
+ pointer-events: auto;
+ cursor: pointer;
+}
+
+/* Enhanced Dialog Box Styles (without animations) */
+.dialog {
+ position: absolute;
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ z-index: 1000;
+ overflow: hidden;
+ width: 300px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+.dialogTextarea {
+ width: 100%;
+ padding: 12px;
+ resize: vertical;
+ border: 1px solid #eaeaea;
+ border-radius: 4px;
+ box-sizing: border-box;
+ font-family: inherit;
+ font-size: 14px;
+ min-height: 80px;
+ margin: 0 0 10px 0;
+}
+
+.dialogTextarea:focus {
+ outline: none;
+ border-color: #4285f4;
+ box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
+}
+
+.dialogButtonContainer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 0 12px 12px 12px;
+ gap: 8px;
+}
+
+.dialogButton {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+}
+
+.saveButton {
+ background-color: #4285f4;
+ color: white;
+}
+
+.saveButton:hover {
+ background-color: #3367d6;
+}
+
+.cancelButton {
+ background-color: #f1f3f4;
+ color: #3c4043;
+}
+
+.cancelButton:hover {
+ background-color: #e8eaed;
+}
+
+.deleteButton {
+ background-color: #ea4335;
+ color: white;
+}
+
+.deleteButton:hover {
+ background-color: #d33426;
+}
+
+.dialogTooltip {
+ position: absolute;
+ background-color: #333;
+ color: white;
+ padding: 8px 12px;
+ border-radius: 4px;
+ max-width: 250px;
+ z-index: 1000;
+ font-size: 14px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+}
+
+.dialogTitleBar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #eaeaea;
+ border-radius: 8px 8px 0 0;
+ font-weight: 500;
+ user-select: none;
+}
+
+.dialogCloseButton {
+ cursor: pointer;
+ font-size: 20px;
+ line-height: 1;
+ color: #5f6368;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+}
+
+.dialogCloseButton:hover {
+ background-color: #e8eaed;
+ color: #202124;
+}
+
+.dialogTimestamp {
+ font-size: 13px;
+ color: #5f6368;
+}
+
+.navToolbar {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ gap: 5px;
+ padding: 5px;
+ background: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ z-index: 1000;
+}
+
+.navButton {
+ width: 40px;
+ height: 40px;
+ border: 1px solid #ccc;
+ background: white;
+ font-size: 16px;
+ cursor: pointer;
+}
+
+.summaryDialog {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 600px;
+ max-width: 90vw;
+ height: 500px;
+ max-height: 80vh;
+ background-color: white;
+ border: 1px solid #ccc;
+ padding: 10px;
+ z-index: 2000;
+}
+
+.summaryCloseButton {
+ float: right;
+ border: none;
+ background: none;
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0 5px;
+}
+
+.summaryContent {
+ width: 100%;
+ height: calc(100% - 30px);
+ border: 1px solid #ccc;
+ padding: 10px;
+ font-family: monospace;
+ resize: none;
+}
+
Delta 1629 lines added, 2 lines removed, 1627-line increase