/**
* OpenSeadragon paperjs overlay plugin based on paper.js
* @version 0.4.13
*
* Includes additional open source libraries which are subject to copyright notices
* as indicated accompanying those segments of code.
*
* Original code:
* Copyright (c) 2022-2024, Thomas Pearce
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of osd-paperjs-annotation nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
import { EditableContent } from "./utils/editablecontent.mjs";
import { dialog } from "./utils/dialog.mjs";
import { datastore} from "./utils/datastore.mjs";
import { paper } from './paperjs.mjs';
/**
* The FileDialog class provides options for saving and loading feature collections as GeoJSON, exporting them as SVG or PNG files,
* and storing them in local storage. It is designed to work with the AnnotationToolKit (atk) object to manage annotations.
*
* @class
* @memberof OSDPaperjsAnnotation
*/
class FileDialog{
/**
* Creates an instance of the FileDialog class, which allows users to save and load feature collections in various formats.
*
* @constructor
* @memberof OSDPaperjsAnnotation.FileDialog
* @param {any} atk - The AnnotationToolKit object.
* @param {object} opts - Additional options for the file dialog.
*/
constructor(atk, opts){
let _this=this;
this.dialog = dialog({innerHTML: fileDialogHtml(), title:''});
this.element = this.dialog.container;
this.element.querySelector('button[data-action="geojson-load"]').addEventListener('click',loadGeoJSON)
this.element.querySelector('button[data-action="geojson-save"]').addEventListener('click',saveGeoJSON)
this.element.querySelector('button[data-action="svg-export"]').addEventListener('click',exportSVG)
this.element.querySelector('button[data-action="png-export"]').addEventListener('click',exportPNG)
this.element.querySelector('button[data-action="ls-store"]').addEventListener('click',localstorageStore)
this.element.querySelector('button[data-action="ls-load"]').addEventListener('click',localstorageLoad)
function getFileName(appendIfNotBlank){
// TODO: handle case of multiple images
let output = atk.viewer.world.getItemAt(0)?.source.name || '';
if(output.length > 0){
output += appendIfNotBlank;
}
return output;
}
function initDlg(){
_this.element.querySelector('.featurecollection-list')?.replaceChildren();
_this.element.querySelector('.finalize')?.replaceChildren();
}
/**
* Sets up the feature collection list in the dialog. This function populates the file dialog with a list of available feature collections.
*
* @private
* @param {Array} fcarray - An array of feature collections.
* @returns {jQuery} The feature collection list element.
*/
function setupFeatureCollectionList(fcarray){
let list = _this.element.querySelector('.featurecollection-list');
list.replaceChildren();
fcarray.forEach(fc => {
let label = fc.label || fc.displayName; //handle geoJSON objects or paper.Layers
const d = document.createElement('div');
list.appendChild(d);
const input = document.createElement('input');
d.appendChild(input);
input.setAttribute('type', 'checkbox');
input.setAttribute('checked', 'true');
datastore.set(input, 'fc', fc);
const l = document.createElement('label');
l.innerText = label;
d.appendChild(l);
});
// list.append(els);
return list;
}
/**
* Loads a GeoJSON file and displays its content in the file dialog. This function triggers the file input and loads the GeoJSON file selected by the user.
* It then parses the GeoJSON data, sets up the feature collection list, and provides options to add or replace existing layers.
*
* @private
*/
function loadGeoJSON(){
initDlg();
const finput = document.createElement('input');
finput.type = 'file';
finput.accept = 'text/geojson,.geojson,text/json,.json';
finput.addEventListener('change',function(){
let file = this.files[0];
let fr = new FileReader();
let geoJSON=[];
fr.onload=function(){
try{
geoJSON = JSON.parse(this.result);
}catch(e){
alert('Bad file - JSON could not be parsed');
return;
}
if(!Array.isArray(geoJSON)) geoJSON = [geoJSON];
let type = Array.from(new Set(geoJSON.map(e=>e.type)))
if(type.length==0){
_this.element.find('.featurecollection-list').text('Bad file - no Features or FeatureCollections were found')
}
if(type.length > 1){
alert('Bad file - valid geoJSON consists of an array of objects with single type (FeatureCollection or Feature)');
return;
}
//convert list of features into a featurecolletion
if(type[0]=='Feature'){
let fc = [{
type:'FeatureCollection',
features:geoJSON,
properties:{
label:file.name
}
}];
geoJSON = fc;
}
setupFeatureCollectionList(geoJSON);
const replaceButton = document.createElement('button');
_this.element.querySelector('.finalize').appendChild(replaceButton);
replaceButton.innerText = 'Replace existing layers';
replaceButton.addEventListener('click',()=>atk.addFeatureCollections(geoJSON,true));
const addButton = document.createElement('button');
_this.element.querySelector('.finalize').appendChild(addButton);
addButton.innerText = 'Add new layers';
addButton.addEventListener('click',()=>atk.addFeatureCollections(geoJSON,false));
}
fr.readAsText(file);
})
// Normal dispatchEvent doesn't work; use the alternative below to open the file dialog
finput['click']();
}
/**
* Loads the feature collections from local storage and displays them in the file dialog. This function retrieves the feature collections stored in local storage
* and sets up the feature collection list in the file dialog, providing options to add or replace existing layers.
*
* @private
*/
function localstorageLoad(){
initDlg();
let geoJSON=[];
let filename=getFileName();
let lskeys=Object.keys(window.localStorage);
let listContainer = _this.element.querySelector('.featurecollection-list');
listContainer.innerText='';
const list = document.createElement('div');
list.classList.add('localstorage-key-list');
listContainer.appendChild(list);
lskeys.sort((a,b)=>a.localeCompare(b)).forEach(key=>{
const div = document.createElement('div');
div.classList.add('localstorage-key');
div.style.order = key==filename ? 0 : 1;
div.innerText = key;
list.appendChild(div);
});
list.querySelectorAll('.localstorage-key').forEach(e=>e.addEventListener('click', function(){
let lsdata = window.localStorage.getItem(this.textContent);
if(!lsdata){
alert(`No data found in local storage for key=${this.textContent}`);
return;
}
try{
geoJSON = JSON.parse(lsdata);
}catch(e){
alert('Bad data - JSON could not be parsed');
return;
}
setupFeatureCollectionList(geoJSON);
const replace = document.createElement('button');
replace.innerText = 'Replace existing layers';
replace.addEventListener('click',()=>atk.addFeatureCollections(geoJSON, true));
_this.element.querySelector('.finalize').appendChild(replace);
const add = document.createElement('button');
add.innerText = 'Add new layers';
add.addEventListener('click',()=>atk.addFeatureCollections(geoJSON, false));
_this.element.querySelector('.finalize').appendChild(add);
}));
}
/**
* Saves the feature collections as a GeoJSON file and provides a download link. This function prepares the selected feature collections in GeoJSON format,
* creates a Blob, and generates a download link for the user to save the file.
*
* @private
*/
function saveGeoJSON(){
initDlg();
let fcs = atk.toGeoJSON();
let list = setupFeatureCollectionList(fcs);
let finishbutton = setupFinalize('Create file','Choose file name:',getFileName('-') + 'FeatureCollections.json');
finishbutton.addEventListener('click',function(){
this.parentElement.querySelector('.download-link')?.remove();
let toSave=Array.from(list.querySelectorAll('input:checked')).map(cb => datastore.get(cb,'fc') );
let txt = JSON.stringify(toSave);
let blob = new Blob([txt],{type:'text/json'});
let filename=this.getAttribute('data-label');
const dl = document.createElement('div');
dl.classList.add('download-link');
this.parentElement.insertBefore(dl, this.nextSibling);
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.target = '_blank';
a.innerText = 'Download file';
dl.appendChild(a);
})
}
/**
* Exports the feature collections as an SVG file and provides a download link. This function prepares the selected feature collections and exports them as an SVG file.
* It generates a download link for the user to save the file in SVG format.
*
* @private
*/
function exportSVG(){
initDlg();
// let fcs = atk.toGeoJSON();
let fcs = atk.getFeatureCollectionGroups();
let list = setupFeatureCollectionList(fcs);
let finishbutton = setupFinalize('Create file','Choose file name:',getFileName('-')+'FeatureCollections.svg');
finishbutton.addEventListener('click',function(){
this.parentElement.querySelector('.download-link')?.remove();
let toSave=Array.from(list.querySelectorAll('input:checked')).map(function(cb){return datastore.get(cb, 'fc')});
if(toSave.length>0){
let p = new paper.PaperScope();
p.setup();
toSave.forEach(function(s){
p.project.activeLayer.addChildren(s.layer.clone({insert:false,deep:true}).children);
})
let blob = new Blob([p.project.exportSVG({asString:true,bounds:'content'})],{type:'text/svg'});
let filename=this.getAttribute('data-label');
const dl = document.createElement('div');
dl.classList.add('download-link');
this.parentElement.insertBefore(dl, this.nextSibling);
const a = document.createElement('a');
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.target = '_blank';
a.innerText = 'Download file';
dl.appendChild(a);
}
})
}
/**
* Exports the feature collections as a PNG file and provides a download link. This function prepares the selected feature collections and exports them as a rasterized PNG file.
* It generates a download link for the user to save the file in PNG format.
*
* @private
*/
function exportPNG(){
initDlg();
let fcs = atk.getFeatureCollectionGroups();
let list = setupFeatureCollectionList(fcs);
let finishbutton = setupFinalize('Create file','Choose file name:',getFileName('-')+'raster.png');
finishbutton.addEventListener('click',function(){
this.parentElement.querySelector('.download-link')?.remove();
let toSave=Array.from(list.querySelectorAll('input:checked')).map(function(cb){return datastore.get(cb, 'fc')});
if(toSave.length>0){
let p = new paper.PaperScope();
p.setup();
toSave.forEach(function(s){
p.project.activeLayer.addChildren(s.layer.clone({insert:false,deep:true}).children);
})
// let blob = new Blob([p.project.activeLayer.rasterize({insert:false}).toDataURL()],{type:'image/png'});
let filename=this.getAttribute('data-label');
const dl = document.createElement('div');
dl.classList.add('download-link');
this.parentElement.insertBefore(dl, this.nextSibling);
const a = document.createElement('a');
a.href = p.project.activeLayer.rasterize({insert:false}).toDataURL();
a.download = filename;
a.target = '_blank';
a.innerText = 'Download file';
dl.appendChild(a);
}
})
}
/**
* Stores the feature collections in the local storage.
* @private
*/
function localstorageStore(){
initDlg();
let fcs = atk.toGeoJSON();
let list = setupFeatureCollectionList(fcs);
let finishbutton=setupFinalize('Save data','Local storage key:',getFileName(),true)
finishbutton.addEventListener('click',function(){
let toSave=Array.from(list.querySelectorAll('input:checked')).map(function(cb){
return datastore.get(cb, 'fc');
});
let txt = JSON.stringify(toSave);
let filename=this.getAttribute('data-label');
window.localStorage.setItem(filename,txt);
})
}
/**
* Sets up the finalize button for performing actions and handling local storage. This function configures the finalize button,
* allowing users to specify a label or key and checks for local storage availability.
* @private
* @param {string} buttonText - The text to display on the button.
* @param {string} editableLabel - The label for the editable content.
* @param {string} editableContent - The initial content for the editable content.
* @param {boolean} localstorage - Whether to test for local storage.
* @returns {jQuery} The finish button element.
*/
function setupFinalize(buttonText,editableLabel,editableContent,localstorage){
function testLocalstorage(localstorage, text, div){
if(localstorage) Object.keys(localStorage).includes(text) ? div.classList.add('key-exists') : div.classList.remove('key-exists');
}
const finalize=_this.element.querySelector('.finalize');
const finishButton = document.createElement('button');
finishButton.innerText = buttonText;
finalize.appendChild(finishButton);
let ec;
if(editableLabel){
const div = document.createElement('div');
const div2 = document.createElement('div');
div.appendChild(div2);
finalize.appendChild(div);
div2.innerText = editableLabel;
ec = new EditableContent();
ec.setText(editableContent);
div.appendChild(ec.element);
if(localstorage) div.classList.add('localstorage-key-test');
ec.onChanged = (text)=>{
finishButton.setAttribute('data-label',text);
testLocalstorage(localstorage, text, div);
}
testLocalstorage(localstorage, editableContent, div);
}
finalize.appendChild(finishButton);
finishButton.setAttribute('data-label',editableContent);
return finishButton;
}
/**
* Returns the HTML for the file dialog. This function generates the HTML markup for the file dialog, including the buttons and feature collection list.
* @private
* @returns {string} The HTML for the file dialog.
*/
function fileDialogHtml(){
return `
<div class="annotation-ui-filedialog" title="Save and Load Feature Collections">
<div class="file-actions">
<div class='header'>1. Available actions</div>
<button class='btn' data-action='geojson-load'>Load GeoJSON</button>
<button class='btn' data-action='ls-load'>Load from browser</button>
<hr>
<button class='btn' data-action='geojson-save'>Save GeoJSON</button>
<button class='btn' data-action='svg-export'>Export as SVG</button>
<button class='btn' data-action='png-export'>Rasterize to PNG</button>
<button class='btn' data-action='ls-store'>Store in browser</button>
</div>
<div class='featurecollection-selection'>
<div class='header'>2. Select Feature Collections</div>
<div class='featurecollection-list'></div>
</div>
<div class="finalize-panel">
<div class='header'>3. Finalize</div>
<div class='finalize'>
</div>
</div>
</div>`;
}
}
/**
* Shows the file dialog.
*/
show(){
// this.element.dialog('open');
this.dialog.show();
}
/**
* Hides the file dialog.
*/
hide(){
// this.element.dialog('close');
this.dialog.hide();
}
/**
* Toggles the visibility of the file dialog.
*/
toggle(){
// this.element.dialog('isOpen') ? this.element.dialog('close') : this.element.dialog('open');
this.dialog.toggle();
}
/**
* Calls a method on the dialog element.
* @param {...any} args - The arguments to pass to the method.
*/
// dialog(...args){
// this.element.dialog(...args)
// }
}
export {FileDialog};