An open-source, web-based viewer for zoomable images, implemented in pure JavaScript.
// DZI - Image size W: 10709 H: 3850 - TileSize: 256
// Example Image Coords x: 5355.04 y: 1923.38
const imageCoords = tileToImageCoords(selectedToken.x, selectedToken.y); // Return the image coordinates
const tiledImage = viewer.current.world.getItemAt(0);
// $.Point {x: 0.5001400691007564, y: 0.18003548417219162}
const viewportCoord = tiledImage.imageToViewportCoordinates(
imageCoords.x,
imageCoords.y
);
const rect = new OpenSeaDragon.Rect(
viewportCoord.x,
viewportCoord.y
// width?
// height?
);
viewer.current.viewport.fitBounds(rect);
imageToViewportCoordinates()
to get the OSD Rect. You can pass this to fitBounds directly instead of instantiating your own Rect. imageToViewportRectangle()
uses image coordinates, so might be easier to handle.
import OpenSeaDragon from "openseadragon";
import React, { useEffect, useState } from "react";
const OpenSeaDragonViewer = ({ image_url }) => {
const [viewer, setViewer] = useState( null);
const InitOpenseadragon = () => {
viewer && viewer.destroy();
setViewer(
OpenSeaDragon({
tileSources: {
type: "image",
url: image_url ,
},
id: "openSeaDragon",
prefixUrl: "https://cdn.jsdelivr.net/npm/openseadragon@2.4/build/openseadragon/images/",
animationTime: 0.5,
blendTime: 0.1,
constrainDuringPan: true,
maxZoomPixelRatio: 2,
minZoomLevel: 1,
visibilityRatio: 1,
zoomPerScroll: 2
})
);
};
useEffect(() => {
InitOpenseadragon();
return () => {
viewer && viewer.destroy();
};
}, []);
return (
<div
id="openSeaDragon"
style={{
height: "600px",
width: "800px"
}}
>
</div>
);
};
export { OpenSeaDragonViewer };
return
in your useEffect
is creating a closure that will keep hold of the original value of viewer
from before it was created. For that reason, I doubt it's getting destroyed. Instead of having viewer
be state, you could have it be a local variable to the useEffect
, and then it should be the correct thing for the cleanup function.
tiledimage.js:1949 TypeError: this.imageToViewportCoordinates is not a function
at tiledimage.js:1938:30
at Array.map (<anonymous>)
at tiledimage.js:1936:36
at Array.map (<anonymous>)
at y.TiledImage._drawTiles (tiledimage.js:1935:55)
at y.TiledImage._updateViewport (tiledimage.js:1185:14)
at y.TiledImage.draw (tiledimage.js:317:18)
at g.World.draw (world.js:260:28)
at viewer.js:3591:18
at viewer.js:3543:9
if (tiledImage._croppingPolygons) {
tiledImage._drawer.saveContext(useSketch);
try {
var polygons = tiledImage._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
var point = tiledImage
.imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-tiledImage.getRotation(true), tiledImage._getRotationPoint(true));
var clipPoint = tiledImage._drawer.viewportCoordToDrawerCoord(point);
if (sketchScale) {
clipPoint = clipPoint.times(sketchScale);
}
return clipPoint;
});
});
tiledImage._drawer.clipWithPolygons(polygons, useSketch);
} catch (e) {
$.console.error(e);
}
usedClip = true;
}
this
inside the map function is not referred to tiledImage anymore. It can be fixed with using self
if (this._croppingPolygons) {
var self = this;
this._drawer.saveContext(useSketch);
try {
var polygons = this._croppingPolygons.map(function (polygon) {
return polygon.map(function (coord) {
var point = self
.imageToViewportCoordinates(coord.x, coord.y, true)
.rotate(-self.getRotation(true), self._getRotationPoint(true));
var clipPoint = self._drawer.viewportCoordToDrawerCoord(point);
if (sketchScale) {
clipPoint = clipPoint.times(sketchScale);
}
return clipPoint;
});
});
this._drawer.clipWithPolygons(polygons, useSketch);
} catch (e) {
$.console.error(e);
}
usedClip = true;
}
regarding #2115: I was looking through the code and found this
* @class IIIFTileSource
* @classdesc A client implementation of the International Image Interoperability Framework
* Format: Image API 1.0 - 2.1
*
* @memberof OpenSeadragon
* @extends OpenSeadragon.TileSource
* @see http://iiif.io/api/image/
* @param {String} [options.tileFormat='jpg']
it seems that the problem is actually solved long ago. The only thing: how to add this option to IIIF tile source? I usually just pass url to iiif info.json as tileSource
viewer.addHandler("canvas-click", (e) => {
// Get the tiled image from the world. Replace the 0 with a different index if your image is elsewhere in the OSD World.
const tiledImage = viewer.world.getItemAt(0)
// Find tiles containing the clicked point from among the last drawn tiles.
const tilesClicked = tiledImage.lastDrawn.filter((tile) => tile.bounds.containsPoint(e.position))
// Of the tiles that contain the point, find the one at the highest level, i.e., the smallest tile
const smallestTileClicked = tilesClicked.reduce((minTile, tile) => tile.level > minTile.level ? tile : minTile, {
level: 0
})
})
viewer.getTileBounds
method seems like it could be much better suited for this, but I’m unsure how to get the current level
of the Viewer to pass to it. Maybe keep track using the update-level
event? Or perhaps there’s a direct method to get the level from the Viewport zoom
?
That said, people do ask for this. Maybe we should add a function based on @PrafulB's implementation?
Before you do that, there’s a pretty big problem with my solution. It can only be used after the tiles have all been fully rendered in at that level. Since a tile is only added to lastDrawn
after it is actually drawn, calling the function immediately after a pan/zoom can return the wrong tile (or at least a tile at the wrong level).
getTileBounds
method on TiledImage was close to the desired functionality, though I haven’t used it so not entirely sure. In this context, would it also make sense to expose a method to get all the rendered levels at the current magnification? You could then use its output to pass to the new getTile
method.
getTileBounds
to look through all the possibilities (for matching up with an x/y)... Not necessarily as efficient as calculating it directly, but with the advantage of using the same code we already have in place (so we know it's correct). I don't expect this is a function that would need to be called all the time, so it doesn't need to be super optimized.
getTile
is called for a specific level.