mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-25 09:34:04 +02:00
Merge branch 'gsoc_2022_local_photos' into develop
This commit is contained in:
@@ -45,6 +45,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
#### :camera: Street-Level
|
||||
* Add [_Mapilio_](https://mapilio.com/openstreetmap) as new street-level imagery provider ([#9664], thanks [@channel-s])
|
||||
* Add photos from the [Norwegian Public Road Administration](https://vegbilder.atlas.vegvesen.no/) as new street-level imagery provider in Norway ([#9509], thanks [@noenandre])
|
||||
* Add functionality to display georeferenced photos from local files ([#9291], thanks [@nontech])
|
||||
* Gray out street level layers in "Map Data" pane when map is zoomed out too far
|
||||
#### :white_check_mark: Validation
|
||||
#### :bug: Bugfixes
|
||||
@@ -61,12 +62,14 @@ _Breaking developer changes, which may affect downstream projects or sites that
|
||||
|
||||
[#8997]: https://github.com/openstreetmap/iD/issues/8997
|
||||
[#9233]: https://github.com/openstreetmap/iD/issues/9233
|
||||
[#9291]: https://github.com/openstreetmap/iD/pull/9291
|
||||
[#9509]: https://github.com/openstreetmap/iD/pull/9509
|
||||
[#9664]: https://github.com/openstreetmap/iD/pull/9664
|
||||
[#9786]: https://github.com/openstreetmap/iD/issues/9786
|
||||
[#9817]: https://github.com/openstreetmap/iD/pull/9817
|
||||
[@channel-s]: https://github.com/channel-s
|
||||
[@noenandre]: https://github.com/noenandre
|
||||
[@nontech]: https://github.com/nontech
|
||||
|
||||
|
||||
# 2.26.2
|
||||
|
||||
+100
-3
@@ -409,12 +409,12 @@ label.streetside-hires {
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper {
|
||||
.photo-wrapper {
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper .plane-frame {
|
||||
.photoviewer .plane-frame {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
@@ -424,7 +424,7 @@ label.streetside-hires {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.vegbilder-wrapper .plane-frame > img.plane-photo{
|
||||
.photoviewer .plane-frame > img.plane-photo{
|
||||
width: auto;
|
||||
height: 100%;
|
||||
transform-origin: 0 0;
|
||||
@@ -471,3 +471,100 @@ label.streetside-hires {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* local georeferenced photos */
|
||||
.layer-local-photos {
|
||||
pointer-events: none;
|
||||
}
|
||||
.layer-local-photos .viewfield-group * {
|
||||
fill: #ed00d9;
|
||||
}
|
||||
.local-photos {
|
||||
display: flex;
|
||||
}
|
||||
.local-photos > div {
|
||||
width: 50%;
|
||||
}
|
||||
.local-photos > div:first-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.list-local-photos {
|
||||
max-height: 40vh;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
/* workaround for something like "overflow-x: visible"
|
||||
see https://stackoverflow.com/a/39554003 */
|
||||
margin-left: -100px;
|
||||
padding-left: 100px;
|
||||
}
|
||||
.list-local-photos::-webkit-scrollbar {
|
||||
border-left: none;
|
||||
}
|
||||
.list-local-photos li {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 30px;
|
||||
}
|
||||
.list-local-photos span.filename {
|
||||
display: block;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 30px;
|
||||
padding-left: 8px;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
}
|
||||
.list-local-photos li:first-child span.filename {
|
||||
border-top: 1px solid #ccc;
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
.list-local-photos li:first-child button {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.list-local-photos li:first-child button.remove {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.list-local-photos li:last-child span.filename {
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.list-local-photos li:last-child button.remove {
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.list-local-photos li.invalid span.filename {
|
||||
color: #ccc;
|
||||
}
|
||||
.list-local-photos li.invalid button.zoom-to-data {
|
||||
display: none;
|
||||
}
|
||||
.list-local-photos li button.no-geolocation {
|
||||
display: none;
|
||||
}
|
||||
.list-local-photos li.invalid button.no-geolocation {
|
||||
display: block;
|
||||
}
|
||||
.list-local-photos .placeholder div {
|
||||
display: block;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(img/loader-black.gif);
|
||||
filter: invert(1);
|
||||
}
|
||||
.local-photos label.button {
|
||||
background: #7092ff;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 10px 25px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
+4
-1
@@ -5650,7 +5650,7 @@ li.hide + li.version .badge .tooltip .popover-arrow {
|
||||
/* Scrollbars
|
||||
----------------------------------------------------- */
|
||||
::-webkit-scrollbar {
|
||||
height: 20px;
|
||||
height: 10px;
|
||||
overflow: visible;
|
||||
width: 10px;
|
||||
border-left: 1px solid #DDD;
|
||||
@@ -5677,6 +5677,9 @@ li.hide + li.version .badge .tooltip .popover-arrow {
|
||||
background-color: rgba(0,0,0,.05);
|
||||
}
|
||||
}
|
||||
body {
|
||||
scrollbar-width: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* Intro walkthrough
|
||||
|
||||
@@ -1442,6 +1442,17 @@ en:
|
||||
tooltip: "Street-level photos from Mapilio"
|
||||
street_side:
|
||||
minzoom_tooltip: "Zoom in to see street-side photos"
|
||||
local_photos:
|
||||
tooltip: Add georeferenced photos from local files
|
||||
tooltip_edit: Edit georeferenced photos
|
||||
header: Georeferenced Photos
|
||||
zoom: Zoom to photos
|
||||
zoom_single: Zoom to photo
|
||||
file:
|
||||
instructions: "Choose georeferenced photos to be displayed. Supported types are .jpg and .png with exif location data"
|
||||
label: "Browse files"
|
||||
no_geolocation:
|
||||
tooltip: Image without geolocation cannot be located on the map
|
||||
note:
|
||||
note: Note
|
||||
title: Edit note
|
||||
|
||||
@@ -12,7 +12,7 @@ let _widthOverflow;
|
||||
function zoomPan (d3_event) {
|
||||
let t = d3_event.transform;
|
||||
_photo.call(utilSetTransform, t.x, t.y, t.k);
|
||||
}
|
||||
}
|
||||
|
||||
function zoomBeahvior () {
|
||||
const {width: wrapperWidth, height: wrapperHeight} = _wrapper.node().getBoundingClientRect();
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
await Promise.resolve();
|
||||
|
||||
return this;
|
||||
},
|
||||
},
|
||||
|
||||
showPhotoFrame: function (context) {
|
||||
const isHidden = context.selectAll('.photo-frame.plane-frame.hide').size();
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
hidePhotoFrame: function (context) {
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
.classed('hide', false);
|
||||
|
||||
return this;
|
||||
},
|
||||
},
|
||||
|
||||
selectPhoto: function (data, keepOrientation) {
|
||||
dispatch.call('viewerChanged');
|
||||
|
||||
@@ -500,19 +500,19 @@ export default {
|
||||
},
|
||||
|
||||
showViewer: function (context) {
|
||||
const viewer = context.container().select('.photoviewer')
|
||||
.classed('hide', false);
|
||||
|
||||
const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();
|
||||
|
||||
if (isHidden) {
|
||||
viewer
|
||||
.selectAll('.photo-wrapper:not(.vegbilder-wrapper)')
|
||||
.classed('hide', true);
|
||||
|
||||
viewer
|
||||
.selectAll('.photo-wrapper.vegbilder-wrapper')
|
||||
const viewer = context.container().select('.photoviewer')
|
||||
.classed('hide', false);
|
||||
|
||||
const isHidden = viewer.selectAll('.photo-wrapper.vegbilder-wrapper.hide').size();
|
||||
|
||||
if (isHidden) {
|
||||
viewer
|
||||
.selectAll('.photo-wrapper:not(.vegbilder-wrapper)')
|
||||
.classed('hide', true);
|
||||
|
||||
viewer
|
||||
.selectAll('.photo-wrapper.vegbilder-wrapper')
|
||||
.classed('hide', false);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
@@ -532,7 +532,7 @@ export default {
|
||||
.classed('currentView', false);
|
||||
|
||||
return this.setStyles(context, null, true);
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// Updates the currently highlighted sequence and selected bubble.
|
||||
|
||||
+12
-2
@@ -29,6 +29,13 @@ export function svgData(projection, context, dispatch) {
|
||||
var _template;
|
||||
var _src;
|
||||
|
||||
const supportedFormats = [
|
||||
'.gpx',
|
||||
'.kml',
|
||||
'.geojson',
|
||||
'.json'
|
||||
];
|
||||
|
||||
|
||||
function init() {
|
||||
if (_initialized) return; // run once
|
||||
@@ -48,6 +55,9 @@ export function svgData(projection, context, dispatch) {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
if (!detected.filedrop) return;
|
||||
var f = d3_event.dataTransfer.files[0];
|
||||
var extension = getExtension(f.name);
|
||||
if (!supportedFormats.includes(extension)) return;
|
||||
drawData.fileList(d3_event.dataTransfer.files);
|
||||
})
|
||||
.on('dragenter.svgData', over)
|
||||
@@ -304,7 +314,7 @@ export function svgData(projection, context, dispatch) {
|
||||
function getExtension(fileName) {
|
||||
if (!fileName) return;
|
||||
|
||||
var re = /\.(gpx|kml|(geo)?json)$/i;
|
||||
var re = /\.(gpx|kml|(geo)?json|png)$/i;
|
||||
var match = fileName.toLowerCase().match(re);
|
||||
return match && match.length && match[0];
|
||||
}
|
||||
@@ -457,9 +467,9 @@ export function svgData(projection, context, dispatch) {
|
||||
if (!arguments.length) return _fileList;
|
||||
|
||||
_template = null;
|
||||
_fileList = fileList;
|
||||
_geojson = null;
|
||||
_src = null;
|
||||
_fileList = fileList;
|
||||
|
||||
if (!fileList || !fileList.length) return this;
|
||||
var f = fileList[0];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { svgData } from './data';
|
||||
import { svgLocalPhotos} from './local_photos';
|
||||
import { svgDebug } from './debug';
|
||||
import { svgGeolocate } from './geolocate';
|
||||
import { svgKeepRight } from './keepRight';
|
||||
@@ -40,6 +41,7 @@ export function svgLayers(projection, context) {
|
||||
{ id: 'kartaview', layer: svgKartaviewImages(projection, context, dispatch) },
|
||||
{ id: 'mapilio', layer: svgMapilioImages(projection, context, dispatch) },
|
||||
{ id: 'vegbilder', layer: svgVegbilder(projection, context, dispatch) },
|
||||
{ id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) },
|
||||
{ id: 'debug', layer: svgDebug(projection, context, dispatch) },
|
||||
{ id: 'geolocate', layer: svgGeolocate(projection, context, dispatch) },
|
||||
{ id: 'touch', layer: svgTouch(projection, context, dispatch) },
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
import exifr from 'exifr';
|
||||
import { isArray, isNumber } from 'lodash-es';
|
||||
|
||||
import { utilDetect } from '../util/detect';
|
||||
import { geoExtent, geoPolygonIntersectsPolygon } from '../geo';
|
||||
import planePhotoFrame from '../services/plane_photo';
|
||||
|
||||
var _initialized = false;
|
||||
var _enabled = false;
|
||||
const minViewfieldZoom = 16;
|
||||
|
||||
export function svgLocalPhotos(projection, context, dispatch) {
|
||||
const detected = utilDetect();
|
||||
let layer = d3_select(null);
|
||||
let _fileList;
|
||||
let _photos = [];
|
||||
let _idAutoinc = 0;
|
||||
let _photoFrame;
|
||||
|
||||
function init() {
|
||||
if (_initialized) return; // run once
|
||||
|
||||
_enabled = true;
|
||||
|
||||
function over(d3_event) {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
d3_event.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
|
||||
context.container()
|
||||
.attr('dropzone', 'copy')
|
||||
.on('drop.svgLocalPhotos', function(d3_event) {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
if (!detected.filedrop) return;
|
||||
drawPhotos.fileList(d3_event.dataTransfer.files, loaded => {
|
||||
if (loaded.length > 0) {
|
||||
drawPhotos.fitZoom();
|
||||
}
|
||||
});
|
||||
})
|
||||
.on('dragenter.svgLocalPhotos', over)
|
||||
.on('dragexit.svgLocalPhotos', over)
|
||||
.on('dragover.svgLocalPhotos', over);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
function ensureViewerLoaded(context) {
|
||||
if (_photoFrame) {
|
||||
return Promise.resolve(_photoFrame);
|
||||
}
|
||||
|
||||
const viewer = context.container().select('.photoviewer')
|
||||
.selectAll('.local-photos-wrapper')
|
||||
.data([0]);
|
||||
|
||||
const viewerEnter = viewer.enter()
|
||||
.append('div')
|
||||
.attr('class', 'photo-wrapper local-photos-wrapper')
|
||||
.classed('hide', true);
|
||||
|
||||
viewerEnter
|
||||
.append('div')
|
||||
.attr('class', 'photo-attribution fillD');
|
||||
|
||||
return planePhotoFrame.init(context, viewerEnter)
|
||||
.then(planePhotoFrame => {
|
||||
_photoFrame = planePhotoFrame;
|
||||
});
|
||||
}
|
||||
|
||||
// opens the image at bottom left
|
||||
function click(d3_event, image, zoomTo) {
|
||||
ensureViewerLoaded(context).then(() => {
|
||||
const viewer = context.container().select('.photoviewer')
|
||||
.datum(image)
|
||||
.classed('hide', false);
|
||||
|
||||
const viewerWrap = viewer.select('.local-photos-wrapper')
|
||||
.classed('hide', false);
|
||||
|
||||
const attribution = viewerWrap.selectAll('.photo-attribution').text('');
|
||||
|
||||
if (image.name) {
|
||||
attribution
|
||||
.append('span')
|
||||
.classed('filename', true)
|
||||
.text(image.name);
|
||||
}
|
||||
|
||||
_photoFrame.selectPhoto({ image_path: '' });
|
||||
image.getSrc().then(src => {
|
||||
_photoFrame
|
||||
.selectPhoto({ image_path: src })
|
||||
.showPhotoFrame(viewerWrap);
|
||||
});
|
||||
});
|
||||
|
||||
// centers the map with image location
|
||||
if (zoomTo) {
|
||||
context.map().centerEase(image.loc);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function transform(d) {
|
||||
// projection expects [long, lat]
|
||||
var svgpoint = projection(d.loc);
|
||||
return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')';
|
||||
}
|
||||
|
||||
function setStyles(hovered) {
|
||||
const viewer = context.container().select('.photoviewer');
|
||||
const selected = viewer.empty() ? undefined : viewer.datum();
|
||||
|
||||
context.container().selectAll('.layer-local-photos .viewfield-group')
|
||||
.classed('hovered', d => d.id === hovered?.id)
|
||||
.classed('highlighted', d => d.id === hovered?.id || d.id === selected?.id)
|
||||
.classed('currentView', d => d.id === selected?.id);
|
||||
}
|
||||
|
||||
// puts the image markers on the map
|
||||
function display_markers(imageList) {
|
||||
imageList = imageList.filter(image => isArray(image.loc) && isNumber(image.loc[0]) && isNumber(image.loc[1]));
|
||||
const groups = layer.selectAll('.markers').selectAll('.viewfield-group')
|
||||
.data(imageList, function(d) { return d.id; });
|
||||
|
||||
// exit
|
||||
groups.exit()
|
||||
.remove();
|
||||
|
||||
// enter
|
||||
const groupsEnter = groups.enter()
|
||||
.append('g')
|
||||
.attr('class', 'viewfield-group')
|
||||
.on('mouseenter', (d3_event, d) => setStyles(d))
|
||||
.on('mouseleave', () => setStyles(null))
|
||||
.on('click', click);
|
||||
|
||||
groupsEnter
|
||||
.append('g')
|
||||
.attr('class', 'viewfield-scale');
|
||||
|
||||
// update
|
||||
const markers = groups
|
||||
.merge(groupsEnter)
|
||||
.attr('transform', transform)
|
||||
.select('.viewfield-scale');
|
||||
|
||||
|
||||
markers.selectAll('circle')
|
||||
.data([0])
|
||||
.enter()
|
||||
.append('circle')
|
||||
.attr('dx', '0')
|
||||
.attr('dy', '0')
|
||||
.attr('r', '6');
|
||||
|
||||
const showViewfields = context.map().zoom() >= minViewfieldZoom;
|
||||
|
||||
const viewfields = markers.selectAll('.viewfield')
|
||||
.data(showViewfields ? [0] : []);
|
||||
|
||||
viewfields.exit()
|
||||
.remove();
|
||||
|
||||
// viewfields may or may not be drawn...
|
||||
// but if they are, draw below the circles
|
||||
viewfields.enter()
|
||||
.insert('path', 'circle')
|
||||
.attr('class', 'viewfield')
|
||||
.attr('transform', function() {
|
||||
const d = this.parentNode.__data__;
|
||||
return `rotate(${Math.round(d.direction ?? 0)},0,0),scale(1.5,1.5),translate(-8,-13)`;
|
||||
})
|
||||
.attr('d', 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z')
|
||||
.style('visibility', function() {
|
||||
const d = this.parentNode.__data__;
|
||||
return isNumber(d.direction) ? 'visible' : 'hidden';
|
||||
});
|
||||
}
|
||||
|
||||
function drawPhotos(selection) {
|
||||
layer = selection.selectAll('.layer-local-photos')
|
||||
.data(_photos ? [0] : []);
|
||||
|
||||
layer.exit()
|
||||
.remove();
|
||||
|
||||
const layerEnter = layer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-local-photos');
|
||||
|
||||
layerEnter
|
||||
.append('g')
|
||||
.attr('class', 'markers');
|
||||
|
||||
layer = layerEnter
|
||||
.merge(layer);
|
||||
|
||||
if (_photos && _photos.length !== 0) {
|
||||
display_markers(_photos);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function readFileAsDataURL(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.onerror = error => reject(error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Reads and parses files
|
||||
* @param {Array<object>} files - Holds array of file - [file_1, file_2, ...]
|
||||
*/
|
||||
async function readmultifiles(files, callback) {
|
||||
const loaded = [];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop
|
||||
const photo = {
|
||||
id: _idAutoinc++,
|
||||
name: file.name,
|
||||
getSrc: () => readFileAsDataURL(file),
|
||||
file: file,
|
||||
loc: [exifData.longitude, exifData.latitude],
|
||||
direction: exifData.GPSImgDirection
|
||||
};
|
||||
loaded.push(photo);
|
||||
const sameName = _photos.filter(i => i.name === photo.name);
|
||||
if (sameName.length === 0) {
|
||||
_photos.push(photo);
|
||||
} else {
|
||||
const thisContent = await photo.getSrc(); // eslint-disable-line no-await-in-loop
|
||||
const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); // eslint-disable-line no-await-in-loop
|
||||
if (!sameNameContent.some(i => i.value === thisContent)) {
|
||||
_photos.push(photo);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// skip files which are not a supported image file
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof callback === 'function') callback(loaded);
|
||||
dispatch.call('change');
|
||||
}
|
||||
|
||||
drawPhotos.setFiles = function(fileList, callback) {
|
||||
// read and parse asynchronously
|
||||
readmultifiles(Array.from(fileList), callback);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Step 1: entry point
|
||||
/**
|
||||
* Sets the fileList
|
||||
* @param {Object} fileList - The uploaded files. fileList is an object, not an array object
|
||||
* @param {Object} fileList.0 - A File - {name: "Das.png", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: "", size: 859658, …}
|
||||
* @param {Function} callback - A callback to be called after the photos have been loaded and parsed
|
||||
*/
|
||||
drawPhotos.fileList = function(fileList, callback) {
|
||||
if (!arguments.length) return _fileList;
|
||||
|
||||
_fileList = fileList;
|
||||
|
||||
if (!fileList || !fileList.length) return this;
|
||||
|
||||
drawPhotos.setFiles(_fileList, callback);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
drawPhotos.getPhotos = function() {
|
||||
return _photos;
|
||||
};
|
||||
|
||||
drawPhotos.removePhoto = function(id) {
|
||||
_photos = _photos.filter(i => i.id !== id);
|
||||
dispatch.call('change');
|
||||
return _photos;
|
||||
};
|
||||
|
||||
drawPhotos.openPhoto = click;
|
||||
|
||||
drawPhotos.fitZoom = function() {
|
||||
const coords = _photos
|
||||
.map(image => image.loc);
|
||||
const extent = coords
|
||||
.filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1]))
|
||||
.map(l => geoExtent(l, l))
|
||||
.reduce((a, b) => a.extend(b));
|
||||
|
||||
const map = context.map();
|
||||
var viewport = map.trimmedExtent().polygon();
|
||||
|
||||
if (!geoPolygonIntersectsPolygon(viewport, coords, true)) {
|
||||
map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent)));
|
||||
}
|
||||
};
|
||||
|
||||
function showLayer() {
|
||||
layer.style('display', 'block');
|
||||
|
||||
layer
|
||||
.style('opacity', 0)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 1)
|
||||
.on('end', function () { dispatch.call('change'); });
|
||||
}
|
||||
|
||||
|
||||
function hideLayer() {
|
||||
layer
|
||||
.transition()
|
||||
.duration(250)
|
||||
.style('opacity', 0)
|
||||
.on('end', () => {
|
||||
layer.selectAll('.viewfield-group').remove();
|
||||
layer.style('display', 'none');
|
||||
});
|
||||
}
|
||||
|
||||
drawPhotos.enabled = function(val) {
|
||||
if (!arguments.length) return _enabled;
|
||||
|
||||
_enabled = val;
|
||||
if (_enabled) {
|
||||
showLayer();
|
||||
} else {
|
||||
hideLayer();
|
||||
}
|
||||
|
||||
dispatch.call('change');
|
||||
return this;
|
||||
};
|
||||
|
||||
drawPhotos.hasData = function() {
|
||||
return isArray(_photos) && _photos.length > 0;
|
||||
};
|
||||
|
||||
|
||||
init();
|
||||
return drawPhotos;
|
||||
}
|
||||
@@ -164,7 +164,6 @@ export function svgMapillaryImages(projection, context, dispatch) {
|
||||
}
|
||||
|
||||
function update() {
|
||||
|
||||
const z = ~~context.map().zoom();
|
||||
const showMarkers = (z >= minMarkerZoom);
|
||||
const showViewfields = (z >= minViewfieldZoom);
|
||||
@@ -172,6 +171,15 @@ export function svgMapillaryImages(projection, context, dispatch) {
|
||||
const service = getService();
|
||||
let sequences = (service ? service.sequences(projection) : []);
|
||||
let images = (service && showMarkers ? service.images(projection) : []);
|
||||
// images[0]
|
||||
// {
|
||||
// "loc":[13.235349655151367,52.50694232952122],
|
||||
// "captured_at":1619457514500,
|
||||
// "ca":0,
|
||||
// "id":505488307476058,
|
||||
// "is_pano":false,
|
||||
// "sequence_id":"zcyumxorbza3dq3twjybam"
|
||||
// }
|
||||
|
||||
images = filterImages(images);
|
||||
sequences = filterSequences(sequences, service);
|
||||
|
||||
@@ -18,6 +18,7 @@ export function uiSectionDataLayers(context) {
|
||||
var settingsCustomData = uiSettingsCustomData(context)
|
||||
.on('change', customChanged);
|
||||
|
||||
// refers to `modules/svg/layers.js` -> function drawLayers(selection) {...}
|
||||
var layers = context.layers();
|
||||
|
||||
var section = uiSection('data-layers', context)
|
||||
@@ -386,7 +387,6 @@ export function uiSectionDataLayers(context) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function drawPanelItems(selection) {
|
||||
|
||||
var panelsListEnter = selection.selectAll('.md-extras-list')
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import _debounce from 'lodash-es/debounce';
|
||||
import {
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
import { select as d3_select } from 'd3-selection';
|
||||
|
||||
import { t } from '../../core/localizer';
|
||||
import { localizer, t } from '../../core/localizer';
|
||||
import { uiTooltip } from '../tooltip';
|
||||
import { uiSection } from '../section';
|
||||
import { utilGetSetValue, utilNoAuto } from '../../util';
|
||||
import { uiSettingsLocalPhotos } from '../settings/local_photos';
|
||||
import { svgIcon } from '../../svg';
|
||||
|
||||
export function uiSectionPhotoOverlays(context) {
|
||||
|
||||
var settingsLocalPhotos = uiSettingsLocalPhotos(context)
|
||||
.on('change', localPhotosChanged);
|
||||
|
||||
var layers = context.layers();
|
||||
|
||||
var section = uiSection('photo-overlays', context)
|
||||
@@ -28,7 +31,8 @@ export function uiSectionPhotoOverlays(context) {
|
||||
.call(drawPhotoItems)
|
||||
.call(drawPhotoTypeItems)
|
||||
.call(drawDateFilter)
|
||||
.call(drawUsernameFilter);
|
||||
.call(drawUsernameFilter)
|
||||
.call(drawLocalPhotos);
|
||||
}
|
||||
|
||||
function drawPhotoItems(selection) {
|
||||
@@ -335,6 +339,96 @@ export function uiSectionPhotoOverlays(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function drawLocalPhotos(selection) {
|
||||
var photoLayer = layers.layer('local-photos');
|
||||
var hasData = photoLayer && photoLayer.hasData();
|
||||
var showsData = hasData && photoLayer.enabled();
|
||||
|
||||
var ul = selection
|
||||
.selectAll('.layer-list-local-photos')
|
||||
.data(photoLayer ? [0] : []);
|
||||
|
||||
// Exit
|
||||
ul.exit()
|
||||
.remove();
|
||||
|
||||
// Enter
|
||||
var ulEnter = ul.enter()
|
||||
.append('ul')
|
||||
.attr('class', 'layer-list layer-list-local-photos');
|
||||
|
||||
var localPhotosEnter = ulEnter
|
||||
.append('li')
|
||||
.attr('class', 'list-item-local-photos');
|
||||
|
||||
var localPhotosLabelEnter = localPhotosEnter
|
||||
.append('label')
|
||||
.call(uiTooltip().title(() => t.append('local_photos.tooltip')));
|
||||
|
||||
localPhotosLabelEnter
|
||||
.append('input')
|
||||
.attr('type', 'checkbox')
|
||||
.on('change', function() { toggleLayer('local-photos'); });
|
||||
|
||||
localPhotosLabelEnter
|
||||
.call(t.append('local_photos.header'));
|
||||
|
||||
localPhotosEnter
|
||||
.append('button')
|
||||
.attr('class', 'open-data-options')
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.tooltip_edit'))
|
||||
.placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')
|
||||
)
|
||||
.on('click', function(d3_event) {
|
||||
d3_event.preventDefault();
|
||||
editLocalPhotos();
|
||||
})
|
||||
.call(svgIcon('#iD-icon-more'));
|
||||
|
||||
localPhotosEnter
|
||||
.append('button')
|
||||
.attr('class', 'zoom-to-data')
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.zoom'))
|
||||
.placement((localizer.textDirection() === 'rtl') ? 'right' : 'left')
|
||||
)
|
||||
.on('click', function(d3_event) {
|
||||
if (d3_select(this).classed('disabled')) return;
|
||||
|
||||
d3_event.preventDefault();
|
||||
d3_event.stopPropagation();
|
||||
photoLayer.fitZoom();
|
||||
})
|
||||
.call(svgIcon('#iD-icon-framed-dot', 'monochrome'));
|
||||
|
||||
// Update
|
||||
ul = ul
|
||||
.merge(ulEnter);
|
||||
|
||||
ul.selectAll('.list-item-local-photos')
|
||||
.classed('active', showsData)
|
||||
.selectAll('label')
|
||||
.classed('deemphasize', !hasData)
|
||||
.selectAll('input')
|
||||
.property('disabled', !hasData)
|
||||
.property('checked', showsData);
|
||||
|
||||
ul.selectAll('button.zoom-to-data')
|
||||
.classed('disabled', !hasData);
|
||||
}
|
||||
|
||||
function editLocalPhotos() {
|
||||
context.container()
|
||||
.call(settingsLocalPhotos);
|
||||
}
|
||||
|
||||
function localPhotosChanged(d) {
|
||||
var localPhotosLayer = layers.layer('local-photos');
|
||||
|
||||
localPhotosLayer.fileList(d);
|
||||
}
|
||||
|
||||
context.layers().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
context.photos().on('change.uiSectionPhotoOverlays', section.reRender);
|
||||
|
||||
|
||||
@@ -292,7 +292,11 @@ export function uiSectionRawTagEditor(id, context) {
|
||||
});
|
||||
|
||||
items.selectAll('button.remove')
|
||||
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', removeTag); // 'click' fires too late - #5878
|
||||
.on(('PointerEvent' in window ? 'pointer' : 'mouse') + 'down', // 'click' fires too late - #5878
|
||||
(d3_event, d) => {
|
||||
if (d3_event.button !== 0) return;
|
||||
removeTag(d3_event, d);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function uiSettingsCustomData(context) {
|
||||
};
|
||||
var _currSettings = {
|
||||
fileList: (dataLayer && dataLayer.fileList()) || null,
|
||||
url: prefs('settings-custom-data-url')
|
||||
// url: prefs('settings-custom-data-url')
|
||||
};
|
||||
|
||||
// var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
import { isArray, isNumber } from 'lodash-es';
|
||||
|
||||
import { t } from '../../core/localizer';
|
||||
import { uiConfirm } from '../confirm';
|
||||
import { utilRebind } from '../../util';
|
||||
import { uiTooltip } from '../tooltip';
|
||||
import { svgIcon } from '../../svg';
|
||||
|
||||
|
||||
export function uiSettingsLocalPhotos(context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var photoLayer = context.layers().layer('local-photos');
|
||||
var modal;
|
||||
|
||||
function render(selection) {
|
||||
|
||||
modal = uiConfirm(selection).okButton();
|
||||
|
||||
modal
|
||||
.classed('settings-modal settings-local-photos', true);
|
||||
|
||||
modal.select('.modal-section.header')
|
||||
.append('h3')
|
||||
.call(t.append('local_photos.header'));
|
||||
|
||||
modal.select('.modal-section.message-text')
|
||||
.append('div')
|
||||
.classed('local-photos', true);
|
||||
|
||||
var instructionsSection = modal.select('.modal-section.message-text .local-photos')
|
||||
.append('div')
|
||||
.classed('instructions', true);
|
||||
|
||||
instructionsSection
|
||||
.append('p')
|
||||
.classed('instructions-local-photos', true)
|
||||
.call(t.append('local_photos.file.instructions'));
|
||||
|
||||
instructionsSection
|
||||
.append('input')
|
||||
.classed('field-file', true)
|
||||
.attr('type', 'file')
|
||||
.attr('multiple', 'multiple')
|
||||
.attr('accept', '.jpg,.jpeg,.png,image/png,image/jpeg')
|
||||
.style('visibility', 'hidden')
|
||||
.attr('id', 'local-photo-files')
|
||||
.on('change', function(d3_event) {
|
||||
var files = d3_event.target.files;
|
||||
if (files && files.length) {
|
||||
photoList
|
||||
.select('ul')
|
||||
.append('li')
|
||||
.classed('placeholder', true)
|
||||
.append('div');
|
||||
dispatch.call('change', this, files);
|
||||
}
|
||||
d3_event.target.value = null;
|
||||
});
|
||||
instructionsSection
|
||||
.append('label')
|
||||
.attr('for', 'local-photo-files')
|
||||
.classed('button', true)
|
||||
.call(t.append('local_photos.file.label'));
|
||||
|
||||
const photoList = modal.select('.modal-section.message-text .local-photos')
|
||||
.append('div')
|
||||
.append('div')
|
||||
.classed('list-local-photos', true);
|
||||
|
||||
photoList
|
||||
.append('ul');
|
||||
|
||||
updatePhotoList(photoList.select('ul'));
|
||||
|
||||
context.layers().on('change', () => updatePhotoList(photoList.select('ul')));
|
||||
}
|
||||
|
||||
function updatePhotoList(container) {
|
||||
function locationUnavailable(d) {
|
||||
return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1]));
|
||||
}
|
||||
|
||||
container.selectAll('li.placeholder').remove();
|
||||
|
||||
let selection = container.selectAll('li')
|
||||
.data(photoLayer.getPhotos() ?? [], d => d.id);
|
||||
selection.exit()
|
||||
.remove();
|
||||
|
||||
const selectionEnter = selection.enter()
|
||||
.append('li');
|
||||
|
||||
selectionEnter
|
||||
.append('span')
|
||||
.classed('filename', true);
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button zoom-to-data', true)
|
||||
.attr('title', t('local_photos.zoom_single'))
|
||||
.call(svgIcon('#iD-icon-framed-dot'));
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button no-geolocation', true)
|
||||
.call(svgIcon('#iD-icon-alert'))
|
||||
.call(uiTooltip()
|
||||
.title(() => t.append('local_photos.no_geolocation.tooltip'))
|
||||
.placement('left')
|
||||
);
|
||||
selectionEnter
|
||||
.append('button')
|
||||
.classed('form-field-button remove', true)
|
||||
.attr('title', t('icons.remove'))
|
||||
.call(svgIcon('#iD-operation-delete'));
|
||||
|
||||
selection = selection.merge(selectionEnter);
|
||||
|
||||
selection
|
||||
.classed('invalid', locationUnavailable);
|
||||
selection.select('span.filename')
|
||||
.text(d => d.name)
|
||||
.attr('title', d => d.name);
|
||||
selection.select('span.filename')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.openPhoto(d3_event, d, false);
|
||||
});
|
||||
selection.select('button.zoom-to-data')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.openPhoto(d3_event, d, true);
|
||||
});
|
||||
selection.select('button.remove')
|
||||
.on('click', (d3_event, d) => {
|
||||
photoLayer.removePhoto(d.id);
|
||||
updatePhotoList(container);
|
||||
});
|
||||
}
|
||||
|
||||
return utilRebind(render, dispatch, 'on');
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
export function utilTriggerEvent(target, type) {
|
||||
export function utilTriggerEvent(target, type, eventProperties) {
|
||||
target.each(function() {
|
||||
var evt = document.createEvent('HTMLEvents');
|
||||
evt.initEvent(type, true, true);
|
||||
for (var prop in eventProperties) {
|
||||
evt[prop] = eventProperties[prop];
|
||||
}
|
||||
this.dispatchEvent(evt);
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+11
@@ -22,6 +22,7 @@
|
||||
"alif-toolkit": "^1.2.9",
|
||||
"core-js-bundle": "^3.32.0",
|
||||
"diacritics": "1.3.0",
|
||||
"exifr": "^7.1.3",
|
||||
"fast-deep-equal": "~3.1.1",
|
||||
"fast-json-stable-stringify": "2.1.0",
|
||||
"lodash-es": "~4.17.15",
|
||||
@@ -4053,6 +4054,11 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/exifr": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
|
||||
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
@@ -12741,6 +12747,11 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"exifr": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
|
||||
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
|
||||
},
|
||||
"extend": {
|
||||
"version": "3.0.2",
|
||||
"dev": true
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"alif-toolkit": "^1.2.9",
|
||||
"core-js-bundle": "^3.32.0",
|
||||
"diacritics": "1.3.0",
|
||||
"exifr": "^7.1.3",
|
||||
"fast-deep-equal": "~3.1.1",
|
||||
"fast-json-stable-stringify": "2.1.0",
|
||||
"lodash-es": "~4.17.15",
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('iD.svgLayers', function () {
|
||||
it('creates default data layers', function () {
|
||||
container.call(iD.svgLayers(projection, context));
|
||||
var nodes = container.selectAll('svg .data-layer').nodes();
|
||||
expect(nodes.length).to.eql(17);
|
||||
expect(nodes.length).to.eql(18);
|
||||
expect(d3.select(nodes[0]).classed('osm')).to.be.true;
|
||||
expect(d3.select(nodes[1]).classed('notes')).to.be.true;
|
||||
expect(d3.select(nodes[2]).classed('data')).to.be.true;
|
||||
@@ -41,9 +41,10 @@ describe('iD.svgLayers', function () {
|
||||
expect(d3.select(nodes[11]).classed('kartaview')).to.be.true;
|
||||
expect(d3.select(nodes[12]).classed('mapilio')).to.be.true;
|
||||
expect(d3.select(nodes[13]).classed('vegbilder')).to.be.true;
|
||||
expect(d3.select(nodes[14]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[15]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[16]).classed('touch')).to.be.true;
|
||||
expect(d3.select(nodes[14]).classed('local-photos')).to.be.true;
|
||||
expect(d3.select(nodes[15]).classed('debug')).to.be.true;
|
||||
expect(d3.select(nodes[16]).classed('geolocate')).to.be.true;
|
||||
expect(d3.select(nodes[17]).classed('touch')).to.be.true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('iD.uiSectionRawTagEditor', function() {
|
||||
expect(tags).to.eql({highway: undefined});
|
||||
done();
|
||||
});
|
||||
iD.utilTriggerEvent(element.selectAll('button.remove'), 'mousedown');
|
||||
iD.utilTriggerEvent(element.selectAll('button.remove'), 'mousedown', { button: 0 });
|
||||
});
|
||||
|
||||
it('adds tags when pressing the TAB key on last input.value', function (done) {
|
||||
|
||||
Reference in New Issue
Block a user