Overview

The editor developed in this project is a web-based tool for recording and managing arbitrary coordinates on IIIF-compatible high-resolution images. It is designed as a general-purpose coordinate recording tool that can specify images via URL parameters and be used across various research projects.

https://youtu.be/UqPo5Xrkin8

Technology Stack

  • OpenSeadragon: IIIF image viewer library (v4.1)
  • SVG Overlay: For marker display
  • localStorage: Data persistence
  • Vanilla JavaScript: Framework-free implementation

Technical Features

1. Image Specification via URL Parameters

The tool’s most distinctive feature is the ability to specify any IIIF image via URL parameters:

function getImageUrlFromQuery() {
    const urlParams = new URLSearchParams(window.location.search);
    const urlParam = urlParams.get('u');
    if (urlParam) {
        try {
            return decodeURIComponent(urlParam);
        } catch (e) {
            console.error('Error decoding URL parameter:', e);
            alert('Failed to decode URL parameter. Using default image.');
        }
    }
    // Default image URL
    return 'https://img.toyobunko-lab.jp/iiif/premodern_chinese/suikeichuzu/Suikeichuuzu_grid_l.tif';
}

const imageUrl = getImageUrlFromQuery();

Usage example:

intersection_editor.html?u=https%3A%2F%2Fexample.com%2Fiiif%2Fimage.tif

Simply pass a URL-encoded image URL via the ?u= parameter to open any image.

2. Per-Image Data Separation

By including the image URL in the localStorage key, data is managed independently for each image:

const imageUrl = getImageUrlFromQuery();
const storageKey = `intersection_points_${btoa(imageUrl).substring(0, 50)}`;

By Base64-encoding the image URL and using it as part of the key, multiple image projects can be managed simultaneously.

3. Automatic IIIF Image Loading

The IIIF info.json URL is automatically generated from the image URL:

function getIIIFInfoUrl(imageUrl) {
    if (imageUrl.endsWith('info.json')) {
        return imageUrl;
    }
    let baseUrl = imageUrl.replace(/\.(jpg|jpeg|png|tif|tiff)$/i, '');
    return `${baseUrl}/info.json`;
}

const viewer = OpenSeadragon({
    id: "viewer",
    tileSources: imageUrl.startsWith('http') ? getIIIFInfoUrl(imageUrl) : imageUrl,
    showNavigationControl: true,
    showNavigator: true
});

File extensions such as .tif and .jpg are automatically handled, and an info.json compliant with IIIF Image API 2.0 is requested.

4. Coordinate Transformation System

OpenSeadragon uses three coordinate systems:

  • Pixel coordinates: Display position in the browser
  • Viewport coordinates: Normalized coordinates (0 to 1)
  • Image coordinates: Actual image pixel coordinates

This tool performs conversions as follows:

// Click position -> Image coordinates
const viewportPoint = viewer.viewport.pointFromPixel(event.position);
const imagePoint = viewer.viewport.viewportToImageCoordinates(viewportPoint);
const x = Math.round(imagePoint.x);
const y = Math.round(imagePoint.y);

5. Dynamic Marker Display with SVG Overlay

Markers that follow zoom and pan operations are implemented:

function updateMarkerPositions() {
    const markers = svgOverlay.querySelectorAll('g');
    markers.forEach(marker => {
        const imageX = parseFloat(marker.getAttribute('data-image-x'));
        const imageY = parseFloat(marker.getAttribute('data-image-y'));

        const imagePoint = new OpenSeadragon.Point(imageX, imageY);
        const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
        const pixelPoint = viewer.viewport.pixelFromPoint(viewportPoint, true);

        marker.setAttribute('transform', `translate(${pixelPoint.x}, ${pixelPoint.y})`);
    });
}

Auto-updating on viewport change events:

viewer.addHandler('update-viewport', updateMarkerPositions);
viewer.addHandler('resize', updateMarkerPositions);

6. Auto-Navigation Feature

To maximize user efficiency, the tool predicts the next intersection position and automatically moves the view.

Basic Algorithm

function predictAndNavigateToNext() {
    const lastPoint = points[len - 1];
    const secondLastPoint = points[len - 2];

    // Vector calculation
    const dx = lastPoint.x - secondLastPoint.x;
    const dy = lastPoint.y - secondLastPoint.y;

    // Predicted next position
    const predictedX = lastPoint.x + dx;
    const predictedY = lastPoint.y + dy;

    // Move the view
    const imagePoint = new OpenSeadragon.Point(predictedX, predictedY);
    const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
    viewer.viewport.panTo(viewportPoint, true);
}

Distance Change Detection and Warning

When an abnormal movement distance is detected, the user is prompted for confirmation:

const currentDistance = Math.sqrt(dx * dx + dy * dy);
const prevDistance = Math.sqrt(prevDx * prevDx + prevDy * prevDy);
const distanceRatio = currentDistance / prevDistance;

if (distanceRatio > 1.5 || distanceRatio  0.67) {
    const shouldMove = confirm(
        `The movement distance differs significantly from the previous one.\n` +
        `Previous: ${Math.round(prevDistance)}px\n` +
        `Current: ${Math.round(currentDistance)}px\n\n` +
        `Move to the predicted position?`
    );
    if (!shouldMove) return;
}

This allows detection of accidental clicks or pattern changes.

7. Data Persistence and Backup

Auto-Save with localStorage

function savePointsToStorage() {
    const data = {
        imageUrl: imageUrl,
        timestamp: new Date().toISOString(),
        points: points
    };
    localStorage.setItem(storageKey, JSON.stringify(data));
}

Auto-saving is performed in an independent data space associated with each image URL.

Export Features

CSV format:

number,x,y,memo
1,12345,23456,"Important point"
2,12389,23456,""

JSON format:

{
  "imageUrl": "https://example.com/iiif/image.tif",
  "count": 2,
  "coordinates": [
    {
      "id": 1,
      "x": 12345,
      "y": 23456,
      "memo": "Important point"
    },
    {
      "id": 2,
      "x": 12389,
      "y": 23456
    }
  ]
}

JSON exports include the image URL, allowing later identification of which image the data belongs to.

8. Switching Between Edit Mode and View Mode

function toggleEditMode(enabled) {
    editMode = enabled;

    // Disable new additions
    if (!editMode) {
        // Early return on canvas-click event
        if (!editMode) return;
    }

    // UI update
    const clearBtn = document.getElementById('clear-btn');
    const importBtn = document.getElementById('import-btn');
    clearBtn.disabled = !enabled;
    importBtn.disabled = !enabled;
}

In view mode, adding and editing memos is still possible, but adding new points and deleting existing ones are disabled.

9. Point Reordering Feature

For cases where reordering is needed:

function movePointTo(fromIndex, toIndex) {
    // Extract the point
    const [movedPoint] = points.splice(fromIndex, 1);

    // Calculate new position
    let newIndex = toIndex;
    if (fromIndex  toIndex) {
        newIndex = toIndex;
    } else {
        newIndex = toIndex + 1;
    }

    // Insert
    points.splice(newIndex, 0, movedPoint);

    // Redraw
    redrawMarkers();
}

10. Keyboard Shortcuts

  • Delete: Delete the selected point
  • Up/Down arrows: Navigate between points
document.addEventListener('keydown', function(e) {
    if (e.key === 'Delete' && selectedPointIndex !== null) {
        deletePoint(selectedPointIndex);
    }

    if (e.key === 'ArrowDown' && selectedPointIndex !== null) {
        selectedPointIndex++;
        // Move the view
        const point = points[selectedPointIndex];
        const imagePoint = new OpenSeadragon.Point(point.x, point.y);
        const viewportPoint = viewer.viewport.imageToViewportCoordinates(imagePoint);
        viewer.viewport.panTo(viewportPoint);
    }
});

User Interface Design

Responsive Layout

body {
    display: flex;
    height: 100vh;
}

#viewer {
    flex: 1;
    height: 100vh;
}

#sidebar {
    width: 400px;
    display: flex;
    flex-direction: column;
}

#sidebar-content {
    flex: 1;
    overflow-y: auto;
}

The viewer and sidebar are positioned at fixed height, with a scrollable list area.

Visual Feedback

  • Normal points: Red circle markers + number labels
  • Predicted points: Blue dashed circles + crosshair
  • Selected: Green background
  • Move mode: Orange background

Performance Optimization

1. Marker Update Optimization

When the viewport changes, all markers are recalculated, but the DOM is not regenerated. Only the transform attribute is updated:

marker.setAttribute('transform', `translate(${pixelPoint.x}, ${pixelPoint.y})`);

2. Event Delegation

Instead of setting event listeners on individual markers, clicks are handled at the parent element:

headerDiv.onclick = () => {
    selectedPointIndex = index;
    updatePointsList();
};

Data Format

Internal Data Structure

points = [
    {
        x: 12345,      // X coordinate on the image
        y: 23456,      // Y coordinate on the image
        memo: "Note"   // Optional memo
    },
    // ...
]

localStorage Format

{
    "imageUrl": "https://img.toyobunko-lab.jp/iiif/...",
    "timestamp": "2025-10-29T12:34:56.789Z",
    "points": [...]
}

Usage Examples

Basic Workflow

  1. Specify an image via URL parameter (or use the default image)
intersection_editor.html?u=https%3A%2F%2Fexample.com%2Fiiif%2Fimage.tif
  1. The page automatically loads previously saved data upon opening
  2. Click on coordinates in the image to record them
  3. The view automatically moves to the next predicted position
  4. Add memos as needed
  5. Export in CSV/JSON format

Managing Multiple Image Projects

When working on multiple projects with different images simultaneously:

  1. Create dedicated URL bookmarks for each image
  2. Data for each image is saved independently in localStorage
  3. No risk of data mixing between images
  4. JSON exports include the image URL

Processing Large Datasets

When recording hundreds to thousands of coordinates:

  1. Enable auto-navigation to improve efficiency
  2. Periodically back up in JSON format
  3. Use the reordering feature to correct mistakes
  4. After completion, export in CSV format for analysis

Security Considerations

XSS Prevention

Memo input values are escaped during CSV export:

const memo = (point.memo || '').replace(/"/g, '""');
csv += `${index + 1},${point.x},${point.y},"${memo}"\n`;

Data Validation

JSON structure is validated during import (maintaining backward compatibility with older formats):

const coordsArray = data.coordinates || data.intersections;
if (!coordsArray || !Array.isArray(coordsArray)) {
    alert('Invalid JSON format.\nA "coordinates" or "intersections" array is required.');
    return;
}

Future Improvements

  1. Undo/Redo: Implementing operation history
  2. Export format expansion: Supporting GeoJSON and Allmaps formats
  3. Collaborative editing: Real-time sharing via WebSocket
  4. Precision improvement: Sub-pixel precision coordinate recording
  5. AI assistance: Automatic intersection detection

Summary

This tool combines flexible image specification via URL parameters, per-image data separation, IIIF image display capabilities, and auto-navigation to achieve a general-purpose system for efficiently recording large volumes of coordinate data.

References


Created: October 2025 Version: 1.0