Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drag and Drop JSON File to Tile Layer to Reconstruct Saved Map #1345

Merged
merged 17 commits into from
Feb 11, 2023
7,973 changes: 7,973 additions & 0 deletions dist/leaflet.distortableimage.js

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions examples/archive.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</head>
<body style="margin:0;">
<!--Welcome Modal-->
<div class="modal fade" id="welcomeModal" aria-labelledby="exampleModalLabel" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="welcomeModal" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content bg-light">
<div class="modal-header d-block">
Expand All @@ -49,11 +49,12 @@ <h2 class="modal-title">Welcome to MapKnitter Lite</h2>
<p>Paste it here to begin:</p>
<form id="form">
<input id="input" type="text" class="form-control" placeholder="https://archive.org/details/..." required>
<button type="submit" class="btn btn-primary mt-4 mb-5">Begin</button>
<button id="beginBtn" type="submit" class="btn btn-primary mt-4 mb-5">Begin</button>
</form>
<p>
<p>Alternatively, close this dialogue box then drag image to the tile from your computer. You can also drag a JSON file to the tile to restore your map to previous saved state.</p>
<!-- <p>
<a href="local.html">Click here</a> to try this offline with local files only.
</p>
</p> -->
</div>
</div>
</div>
Expand Down Expand Up @@ -96,8 +97,10 @@ <h4>Share via email</h4>
</div>

<i title="Open Sidebar" id="mapToggle" class="fa fa-bars fa-3x " style="position: absolute; right: 0; top:30px; margin: 1rem; z-index: 900; color: white; cursor: pointer;" aria-hidden="true"></i>

<div id="map" style="width:100%; height:100%; position:absolute; top:0;"></div>

<div id="dropZone" class="flex-item flex-item1">
<div id="map" style="width:100%; height:100%; position:absolute; top:0;"></div>
</div>

<div class="offcanvas offcanvas-end" data-bs-backdrop="false" data-bs-keyboard="false" tabindex="1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
Expand Down
129 changes: 114 additions & 15 deletions examples/js/archive.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Paginator} from './modules/paginator.js';
let map;
const welcomeModal = document.getElementById('welcomeModal');
const tileMap = document.getElementById('map');
const beginBtn = document.getElementById('beginBtn');
const restoreWelcomeModal = document.getElementById('restoreWelcomeModalBtn');
const sidebar = document.getElementById('offcanvasRight');
const form = document.getElementById('form');
Expand All @@ -15,10 +16,11 @@ let fetchedFrom;
let fetchedImages;
let currPagination; // currPagination is used to initiate the Paginator Class
let sidebarOpen = false;
let mapReconstructionMode = false;
let mapReconstructionMode = false; // map is reconstructed from json URL in this mode

const setupMap = () => {
map = L.map('map').setView([51.505, -0.09], 13);
window.map = map; // make map global for debugging

map.attributionControl.setPosition('bottomleft');

Expand Down Expand Up @@ -160,9 +162,10 @@ function showImages(getUrl) {
});
}

welcomeModal.addEventListener('hidden.bs.modal', (event) => {
new bootstrap.Offcanvas(sidebar).show();
sidebarOpen = true;
beginBtn.addEventListener('click', (event) => {
bootstrap.Modal.getInstance(welcomeModal).hide();
new bootstrap.Offcanvas(sidebar).show();
sidebarOpen = true;
});

restoreWelcomeModal.addEventListener('click', (event) => {
Expand Down Expand Up @@ -213,29 +216,28 @@ function isJsonDetected(url) {
function placeImage (imageURL, options, newImage = false) {
let image;

if (newImage) {
if (newImage) { // Construct new map
image = L.distortableImageOverlay(
imageURL,
{tooltipText: options.tooltipText}
);
} else {
} else { // Reconstruct map to previous saved state
image = L.distortableImageOverlay(
imageURL,
{
height: options.height,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed height here because it'd be auto-detected if we do the fitBounds thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting... okay.

tooltipText: options.tooltipText,
// corners: options.corners, <== uncomment this to see the effect of the corners
corners: options.corners, // <== uncomment this to see the effect of the corners
}
);
}

map.imgGroup.addLayer(image);
};

// Reconstruct Map from JSON
document.addEventListener('DOMContentLoaded', async (event) => {
if (mapReconstructionMode) {
const url = location.href;
// expected url format http://localhost:8081/examples/archive.html?json=https://archive.org/download/mkl-1/mkl-1.json
const url = location.href;

if (isJsonDetected(url)) {
const jsonDownloadURL = extractJsonFromUrlParams(url);
Expand All @@ -249,6 +251,9 @@ document.addEventListener('DOMContentLoaded', async (event) => {
let imageURL;
let options;

// let cornerBounds = .... // collect all corners into a big array of [lat, lon]
// map.fitBounds(cornerBounds);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we can collect up all the corner lat/lon pairs, and make an array. Then on the second line, we fit the window to contain them all before we start placing images!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay... many thanks!


imageCollectionObj.imgCollectionProps.forEach((imageObj) => {
imageURL = imageObj.src;
options = {
Expand Down Expand Up @@ -290,21 +295,115 @@ document.addEventListener('click', (event) => {

// download JSON
saveMap.addEventListener('click', () => {
const jsonImages = map.imgGroup.generateExportJson(true).images;
const jsonImages = map.imgGroup.generateExportJson(true);
// a check to prevent download of empty file
if (jsonImages.length) {
const encodedFile = 'text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(jsonImages));
if (jsonImages.images.length) {
const modifiedJsonImages = {};
const tempCollection = [];

// restructure jsonImages
modifiedJsonImages.avg_cm_per_pixel = jsonImages.avg_cm_per_pixel;
jsonImages.images.map((image) => {
tempCollection.push({
id: image.id,
src: image.src,
width: image.width,
height: image.height,
tooltipText: image.tooltipText,
image_file_name: image.image_file_name,
nodes: image.nodes,
cm_per_pixel: image.cm_per_pixel,
});
});
modifiedJsonImages.collection = tempCollection;

const encodedFile = 'text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(modifiedJsonImages));
const a = document.createElement('a');
a.href = 'data:' + encodedFile;
const fileName = prompt('Use this file to recover your map’s saved state. Enter filename:');
a.download = fileName ? fileName + '.json' : 'MapknitterLite.json';
a.click();
}
})
});

// share map modal
const shareModal = document.getElementById('shareModal')
const modality = new bootstrap.Modal(shareModal)
shareMapBtn.addEventListener('click', () => {
bootstrap.Modal.getInstance(shareModal).show()
})
});

function extractFileName(name) {
const startIndex = name.lastIndexOf('.');
const fileName = name.substring(0, startIndex);

return fileName;
}

function handleDrop (e) {
const files = e.dataTransfer.files;
const reader = new FileReader();

// confirm file being dragged has json format
if (files.length === 1 && files[0].type === 'application/json') {
reader.addEventListener('load', () => {
let imgUrl;
let options;
const imgObj = JSON.parse(reader.result);
// for json file with multiple image property sets
if (imgObj.collection.length > 1) {
imgObj.collection.forEach((imgObj) => {
imgUrl = imgObj.src;
options = {
height: imgObj.height,
tooltipText: imgObj.tooltipText,
corners: imgObj.nodes, // uncomment to view the effect of corners
};
placeImage(imgUrl, options);
});
return;
}
// for json file with only one image property set
imgUrl = imgObj.collection[0].src;
options = {
height: imgObj.collection[0].height,
tooltipText: imgObj.collection[0].tooltipText,
corners: imgObj.nodes, // uncomment to view the effect of corners
};
placeImage(imgUrl, options);
});
reader.readAsText(files[0]);
} else {
// non-json (i.e., images) files make it to this point
for (let i = 0; i < files.length; i++) {
reader.addEventListener('load', () => {
const options = {tooltipText: extractFileName(files[i].name)};
placeImage(reader.result, options, true);
});
reader.readAsDataURL(files[i]);
}
}
};

function uploadFiles() {
const dropZone = document.getElementById('dropZone');
const active = () => dropZone.classList.add('overlay');
const inactive = () => dropZone.classList.remove('overlay');
const prevents = e => e.preventDefault();

['dragenter', 'dragover', 'dragleave', 'drop'].forEach((e) => {
dropZone.addEventListener(e, prevents);
});

['dragenter', 'dragover'].forEach((e) => {
dropZone.addEventListener(e, active);
});

['dragleave', 'drop'].forEach((e) => {
dropZone.addEventListener(e, inactive);
});

dropZone.addEventListener('drop', handleDrop);
};

document.addEventListener('DOMContentLoaded', uploadFiles);
73 changes: 57 additions & 16 deletions examples/js/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,25 @@ const uploadFiles = () => {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, i just still see some changes to this file so I asked, maybe this was unintentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intentional, do you want me to remove the changes since focus has since shifted to archive.js?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the changes, does it do the same thing for the local.html demo? That makes sense if so!

Copy link
Collaborator Author

@segun-codes segun-codes Feb 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes expand existing drag and drop functionality in local.html/local.js to include support for JSON file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, great, let's keep it!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, many thanks!

document.addEventListener('DOMContentLoaded', uploadFiles);

// display uploaded image
const loadMap = (image) => {
// <-Ignore --- delete this comment
// this function is a candidate from modularization. It's used in many other .js files
const placeImage = (imageUrl, options = {}) => {
map.whenReady(function() {
img = L.distortableImageOverlay(image, {
selected: true,
fullResolutionSrc: 'large.jpg',
}).addTo(map);
// constructs new map
const noOptions = Object.keys(options).length === 0 && options.constructor === Object
if (noOptions) {
img = L.distortableImageOverlay(imageUrl, {
selected: true,
fullResolutionSrc: 'large.jpg',
}).addTo(map);
} else {
// reconstructs map into previous state
image = L.distortableImageOverlay(imageUrl, {
height: options.height,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to make the same changes to local.js

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure... okay.

tooltipText: options.tooltipText,
// corners: options.corners, <== uncomment this to see the effect of the corners
}).addTo(map);
}
});
};

Expand All @@ -50,30 +62,59 @@ const handleDrop = (e) => {
form.append('scale', prompt('Choose a scale to download image or use the default (cm per pixel):', 100) || mergedOpts.scale);

const files = e.dataTransfer.files;
// eslint-disable-next-line no-unused-vars
for (let i = 0, f; (f = files[i]); i++) {
const reader = new FileReader();
// save file to local storage
const reader = new FileReader();

// confirm file being dragged has json format
if (files.length === 1 && files[0].type === 'application/json') {
reader.addEventListener('load', () => {
loadMap(reader.result);
});
reader.readAsDataURL(files[i]);
// Read the File objects in this FileList.
imgObj = JSON.parse(reader.result);
// for json file with multiple image property sets
if (imgObj.collection.length > 1) {
imgObj.collection.forEach((imgObj) => {
imgUrl = imgObj.src;
options = {
height: imgObj.height,
tooltipText: imgObj.tooltipText,
// corners: imgObj.nodes, // uncomment to view the effect of corners
};
placeImage(imgUrl, options);
});
return;
}
// for json file with only one image property set
imgUrl = imgObj.collection[0].src;
options = {
height: imgObj.collection[0].height,
tooltipText: imgObj.collection[0].tooltipText,
// corners: imgObj.nodes, // uncomment to view the effect of corners
};
placeImage(imgUrl, options);
});
reader.readAsText(files[0]);
} else {
// non-json (i.e., images) files make it to this point
// eslint-disable-next-line no-unused-vars
for (let i = 0, f; (f = files[i]); i++) {
reader.addEventListener('load', () => {
placeImage(reader.result);
});
reader.readAsDataURL(files[i]);
// Read the File objects in this FileList.
}
}
};

// <-Ignore --- delete this comment
// notify user that they will lose all changes
window.addEventListener('beforeunload', (e) => {
e.preventDefault();
e.returnValue = '';
});


let map;
(function() {
map = L.map('map').setView([51.505, -0.09], 13);
map.addGoogleMutant();
})();


L.Control.geocoder().addTo(map);
13 changes: 7 additions & 6 deletions src/DistortableCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,15 +199,15 @@ L.DistortableCollection = L.FeatureGroup.extend({
return reduce / imgs.length;
},

// Connects to JSON file and fetches JSON data therein from remote source
// connects to JSON file and fetches JSON data therein from remote source
async fetchRemoteJson(url) {
let index = 0;
const imgCollectionProps = [];

try {
const response = await axios.get(url);
if (response.data.images.length > 1) {
response.data.images.forEach((data) => {
if (response.data.collection.length > 1) {
response.data.collection.forEach((data) => {
imgCollectionProps[index] = data;
index++;
});
Expand All @@ -216,7 +216,7 @@ L.DistortableCollection = L.FeatureGroup.extend({
imgCollectionProps,
};
}
imgCollectionProps[index] = response.data.images;
imgCollectionProps[index] = response.data.collection;

return {
avg_cm_per_pixel: response.data.avg_cm_per_pixel,
Expand All @@ -227,7 +227,7 @@ L.DistortableCollection = L.FeatureGroup.extend({
}
},

// expects url in this format: https://archive.org/download/segeotest/segeotest.json
// expects url in this format: https://archive.org/download/mkl-1/mkl-1.json
async recreateImagesFromJsonUrl(url) {
let imageCollectionObj = {};

Expand All @@ -251,14 +251,15 @@ L.DistortableCollection = L.FeatureGroup.extend({
const corners = [
{lat: zc[0].lat, lon: zc[0].lng},
{lat: zc[1].lat, lon: zc[1].lng},
{lat: zc[3].lat, lon: zc[3].lng},
{lat: zc[2].lat, lon: zc[2].lng},
{lat: zc[3].lat, lon: zc[3].lng},
];
json.images.push({
id: layer._leaflet_id,
src: layer._image.src,
width: layer._image.width,
height: layer._image.height,
tooltipText: layer.getTooltipText(),
image_file_name: filename,
nodes: corners,
cm_per_pixel: L.ImageUtil.getCmPerPixel(layer),
Expand Down
Loading