From 8d800801ff32ce4de85c172f9616baead41857ec Mon Sep 17 00:00:00 2001 From: mukesh Date: Mon, 20 Jun 2022 09:25:43 +0200 Subject: [PATCH 01/23] Feature: local photos ui added --- modules/ui/sections/data_layers.js | 60 +++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index da0c1830c..ad8cb0e4f 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -306,27 +306,28 @@ export function uiSectionDataLayers(context) { .append('ul') .attr('class', 'layer-list layer-list-data'); - var liEnter = ulEnter + // Custom Map Data + var mapEnter = ulEnter .append('li') .attr('class', 'list-item-data'); - var labelEnter = liEnter + var mapLabelEnter = mapEnter .append('label') .call(uiTooltip() .title(t.html('map_data.layers.custom.tooltip')) .placement('top') ); - labelEnter + mapLabelEnter .append('input') .attr('type', 'checkbox') .on('change', function() { toggleLayer('data'); }); - labelEnter + mapLabelEnter .append('span') .call(t.append('map_data.layers.custom.title')); - liEnter + mapEnter .append('button') .attr('class', 'open-data-options') .call(uiTooltip() @@ -339,7 +340,54 @@ export function uiSectionDataLayers(context) { }) .call(svgIcon('#iD-icon-more')); - liEnter + mapEnter + .append('button') + .attr('class', 'zoom-to-data') + .call(uiTooltip() + .title(t.html('map_data.layers.custom.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(); + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + + // new item - local photos + var localPhotosEnter = ulEnter + .append('li') + .attr('class', 'list-item-local-photos'); + + var localPhotosLabelEnter = localPhotosEnter + .append('label') + // TODO: Add tooltip + + localPhotosLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('data'); }); + + localPhotosLabelEnter + .append('span') + .text('Local Photos'); + + localPhotosEnter + .append('button') + .attr('class', 'open-data-options') + .call(uiTooltip() + .title(t.html('settings.custom_data.tooltip')) + .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') + ) + .on('click', function(d3_event) { + d3_event.preventDefault(); + editCustom(); + }) + .call(svgIcon('#iD-icon-more')); + + localPhotosEnter .append('button') .attr('class', 'zoom-to-data') .call(uiTooltip() From 16280638b5a6bfaa941a1bee18d57e79dea8e65c Mon Sep 17 00:00:00 2001 From: mukesh Date: Wed, 29 Jun 2022 18:51:33 +0200 Subject: [PATCH 02/23] plop --- modules/svg/data.js | 7 +- modules/ui/sections/data_layers.js | 20 +++- modules/ui/settings/custom_data.js | 2 +- modules/ui/settings/local_photos_data.js | 126 +++++++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 modules/ui/settings/local_photos_data.js diff --git a/modules/svg/data.js b/modules/svg/data.js index a88e4b0ff..40a89f34e 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -304,7 +304,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]; } @@ -353,6 +353,11 @@ export function svgData(projection, context, dispatch) { stringifyGeojsonProperties(gj); } break; + case '.png': + // xx = JSON.parse(data); + console.log('Hello world!'); + console.log(data); + break; } gj = gj || {}; diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index ad8cb0e4f..211004731 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -12,12 +12,16 @@ import { modeBrowse } from '../../modes/browse'; import { uiCmd } from '../cmd'; import { uiSection } from '../section'; import { uiSettingsCustomData } from '../settings/custom_data'; +import { uiSettingsLocalPhotosData } from '../settings/local_photos_data'; export function uiSectionDataLayers(context) { var settingsCustomData = uiSettingsCustomData(context) .on('change', customChanged); + var settingsLocalPhotosData = uiSettingsLocalPhotosData(context) + .on('change', localPhotosChanged); + var layers = context.layers(); var section = uiSection('data-layers', context) @@ -362,7 +366,7 @@ export function uiSectionDataLayers(context) { .attr('class', 'list-item-local-photos'); var localPhotosLabelEnter = localPhotosEnter - .append('label') + .append('label'); // TODO: Add tooltip localPhotosLabelEnter @@ -383,7 +387,7 @@ export function uiSectionDataLayers(context) { ) .on('click', function(d3_event) { d3_event.preventDefault(); - editCustom(); + editLocalPhotos(); }) .call(svgIcon('#iD-icon-more')); @@ -424,6 +428,11 @@ export function uiSectionDataLayers(context) { .call(settingsCustomData); } + function editLocalPhotos() { + context.container() + .call(settingsLocalPhotosData); + } + function customChanged(d) { var dataLayer = layers.layer('data'); @@ -434,6 +443,13 @@ export function uiSectionDataLayers(context) { } } + function localPhotosChanged(d) { + var dataLayer = layers.layer('data'); + + if (d && d.fileList) { + dataLayer.fileList(d.fileList); + } + } function drawPanelItems(selection) { diff --git a/modules/ui/settings/custom_data.js b/modules/ui/settings/custom_data.js index 29a1d83d2..2ad991fc4 100644 --- a/modules/ui/settings/custom_data.js +++ b/modules/ui/settings/custom_data.js @@ -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'; diff --git a/modules/ui/settings/local_photos_data.js b/modules/ui/settings/local_photos_data.js new file mode 100644 index 000000000..11b87945a --- /dev/null +++ b/modules/ui/settings/local_photos_data.js @@ -0,0 +1,126 @@ +import { dispatch as d3_dispatch } from 'd3-dispatch'; + +import { prefs } from '../../core/preferences'; +import { t } from '../../core/localizer'; +import { uiConfirm } from '../confirm'; +import { utilNoAuto, utilRebind } from '../../util'; + + +export function uiSettingsLocalPhotosData (context) { + var dispatch = d3_dispatch('change'); + + function render(selection) { + var dataLayer = context.layers().layer('data'); + + // keep separate copies of original and current settings + var _origSettings = { + fileList: (dataLayer && dataLayer.fileList()) || null, + url: prefs('settings-custom-data-url') + }; + var _currSettings = { + fileList: (dataLayer && dataLayer.fileList()) || null, + url: prefs('settings-custom-data-url') + }; + + // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; + var modal = uiConfirm(selection).okButton(); + + modal + .classed('settings-modal settings-custom-data', true); + + modal.select('.modal-section.header') + .append('h3') + .call(t.append('settings.custom_data.header')); + + + var textSection = modal.select('.modal-section.message-text'); + + //TODO: Add translation + textSection + .append('pre') + .text('Choose local photos. Supported types are: .jpg, .jpeg, .png'); + // .attr('class', 'instructions-file') + // .call(t.append('settings.custom_data.file.instructions')); + + textSection + .append('input') + .attr('class', 'field-file') + .attr('type', 'file') + // .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json') + .property('files', _currSettings.fileList) + .on('change', function(d3_event) { + var files = d3_event.target.files; + console.log(files); + if (files && files.length) { + // _currSettings.url = ''; + // textSection.select('.field-url').property('value', ''); + _currSettings.fileList = files; + } else { + _currSettings.fileList = null; + } + }); + + // textSection + // .append('h4') + // .call(t.append('settings.custom_data.or')); + + // textSection + // .append('pre') + // .attr('class', 'instructions-url') + // .call(t.append('settings.custom_data.url.instructions')); + + // textSection + // .append('textarea') + // .attr('class', 'field-url') + // .attr('placeholder', t('settings.custom_data.url.placeholder')) + // .call(utilNoAuto) + // .property('value', _currSettings.url); + + + // insert a cancel button + var buttonSection = modal.select('.modal-section.buttons'); + + buttonSection + .insert('button', '.ok-button') + .attr('class', 'button cancel-button secondary-action') + .call(t.append('confirm.cancel')); + + + buttonSection.select('.cancel-button') + .on('click.cancel', clickCancel); + + buttonSection.select('.ok-button') + .attr('disabled', isSaveDisabled) + .on('click.save', clickSave); + + + function isSaveDisabled() { + return null; + } + + + // restore the original url + function clickCancel() { + textSection.select('.field-url').property('value', _origSettings.url); + prefs('settings-custom-data-url', _origSettings.url); + this.blur(); + modal.close(); + } + + // accept the current url + function clickSave() { + // _currSettings.url = textSection.select('.field-url').property('value').trim(); + + // one or the other but not both + // if (_currSettings.url) { _currSettings.fileList = null; } + if (_currSettings.fileList) { _currSettings.url = ''; } + + // prefs('settings-custom-data-url', _currSettings.url); + this.blur(); + modal.close(); + dispatch.call('change', this, _currSettings); + } + } + + return utilRebind(render, dispatch, 'on'); +} From 7a2ab8aeaa0a3b1a9d123d6c8aa5b4f8d0899aac Mon Sep 17 00:00:00 2001 From: mukesh Date: Wed, 3 Aug 2022 15:37:09 +0200 Subject: [PATCH 03/23] Draft: still in progress --- modules/svg/data.js | 36 +- modules/svg/layers.js | 2 + modules/svg/local_photos.js | 440 +++++++++++++++++++++++ modules/svg/mapillary_images.js | 17 +- modules/ui/sections/data_layers.js | 192 +++++++++- modules/ui/settings/local_photos_data.js | 22 +- package.json | 8 +- 7 files changed, 683 insertions(+), 34 deletions(-) create mode 100644 modules/svg/local_photos.js diff --git a/modules/svg/data.js b/modules/svg/data.js index 40a89f34e..11559cb13 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -31,12 +31,14 @@ export function svgData(projection, context, dispatch) { function init() { + console.log('init() called'); if (_initialized) return; // run once _geojson = {}; _enabled = true; function over(d3_event) { + console.log('over() called'); d3_event.stopPropagation(); d3_event.preventDefault(); d3_event.dataTransfer.dropEffect = 'copy'; @@ -59,6 +61,7 @@ export function svgData(projection, context, dispatch) { function getService() { + console.log('getService() called'); if (services.vectorTile && !_vtService) { _vtService = services.vectorTile; _vtService.event.on('loadedData', throttledRedraw); @@ -71,6 +74,7 @@ export function svgData(projection, context, dispatch) { function showLayer() { + console.log('showLayer() called'); layerOn(); layer @@ -83,6 +87,7 @@ export function svgData(projection, context, dispatch) { function hideLayer() { + console.log('hideLayer() called'); throttledRedraw.cancel(); layer @@ -94,11 +99,13 @@ export function svgData(projection, context, dispatch) { function layerOn() { + console.log('layerOn() called'); layer.style('display', 'block'); } function layerOff() { + console.log('layerOff() called'); layer.selectAll('.viewfield-group').remove(); layer.style('display', 'none'); } @@ -106,6 +113,7 @@ export function svgData(projection, context, dispatch) { // ensure that all geojson features in a collection have IDs function ensureIDs(gj) { + console.log('ensureIDs() called'); if (!gj) return null; if (gj.type === 'FeatureCollection') { @@ -121,6 +129,7 @@ export function svgData(projection, context, dispatch) { // ensure that each single Feature object has a unique ID function ensureFeatureID(feature) { + console.log('ensureFeatureID() called'); if (!feature) return; feature.__featurehash__ = utilHashcode(stringify(feature)); return feature; @@ -129,6 +138,7 @@ export function svgData(projection, context, dispatch) { // Prefer an array of Features instead of a FeatureCollection function getFeatures(gj) { + console.log('getFeatures() called'); if (!gj) return []; if (gj.type === 'FeatureCollection') { @@ -140,21 +150,25 @@ export function svgData(projection, context, dispatch) { function featureKey(d) { + console.log('featureKey() called'); return d.__featurehash__; } function isPolygon(d) { + console.log('isPolygon() called'); return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon'; } function clipPathID(d) { + console.log('clipPathID() called'); return 'ideditor-data-' + d.__featurehash__ + '-clippath'; } function featureClasses(d) { + console.log('featureClasses() called'); return [ 'data' + d.__featurehash__, d.geometry.type, @@ -165,6 +179,7 @@ export function svgData(projection, context, dispatch) { function drawData(selection) { + console.log('drawData() called'); var vtService = getService(); var getPath = svgPath(projection).geojson; var getAreaPath = svgPath(projection, null, true).geojson; @@ -269,6 +284,7 @@ export function svgData(projection, context, dispatch) { function drawLabels(selection, textClass, data) { + console.log('drawLabels() called'); var labelPath = d3_geoPath(projection); var labelData = data.filter(function(d) { return _showLabels && d.properties && (d.properties.desc || d.properties.name); @@ -302,6 +318,7 @@ export function svgData(projection, context, dispatch) { function getExtension(fileName) { + console.log('getExtension() called'); if (!fileName) return; var re = /\.(gpx|kml|(geo)?json|png)$/i; @@ -311,11 +328,13 @@ export function svgData(projection, context, dispatch) { function xmlToDom(textdata) { + console.log('xmlToDom() called'); return (new DOMParser()).parseFromString(textdata, 'text/xml'); } function stringifyGeojsonProperties(feature) { + console.log('stringifyGeojsonProperties() called'); const properties = feature.properties; for (const key in properties) { const property = properties[key]; @@ -331,6 +350,7 @@ export function svgData(projection, context, dispatch) { drawData.setFile = function(extension, data) { + console.log('drawData.setFile called'); _template = null; _fileList = null; _geojson = null; @@ -353,11 +373,11 @@ export function svgData(projection, context, dispatch) { stringifyGeojsonProperties(gj); } break; - case '.png': + // case '.png': // xx = JSON.parse(data); - console.log('Hello world!'); - console.log(data); - break; + // console.log('Hello world!'); + // console.log(data); + // break; } gj = gj || {}; @@ -373,6 +393,7 @@ export function svgData(projection, context, dispatch) { drawData.showLabels = function(val) { + console.log('drawData.showLabels called'); if (!arguments.length) return _showLabels; _showLabels = val; @@ -381,6 +402,7 @@ export function svgData(projection, context, dispatch) { drawData.enabled = function(val) { + console.log('drawData.enabled called'); if (!arguments.length) return _enabled; _enabled = val; @@ -396,12 +418,14 @@ export function svgData(projection, context, dispatch) { drawData.hasData = function() { + console.log('drawData.hasData called'); var gj = _geojson || {}; return !!(_template || Object.keys(gj).length); }; drawData.template = function(val, src) { + console.log('drawData.template called'); if (!arguments.length) return _template; // test source against OSM imagery blocklists.. @@ -440,6 +464,7 @@ export function svgData(projection, context, dispatch) { drawData.geojson = function(gj, src) { + console.log('drawData.geojson called'); if (!arguments.length) return _geojson; _template = null; @@ -459,6 +484,7 @@ export function svgData(projection, context, dispatch) { drawData.fileList = function(fileList) { + console.log('drawData.fileList called'); if (!arguments.length) return _fileList; _template = null; @@ -483,6 +509,7 @@ export function svgData(projection, context, dispatch) { drawData.url = function(url, defaultExtension) { + console.log('drawData.url called'); _template = null; _fileList = null; _geojson = null; @@ -515,6 +542,7 @@ export function svgData(projection, context, dispatch) { drawData.fitZoom = function() { + console.log('drawData.fitZoom called'); var features = getFeatures(_geojson); if (!features.length) return; diff --git a/modules/svg/layers.js b/modules/svg/layers.js index 169805c4c..2aada8e59 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -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'; @@ -27,6 +28,7 @@ export function svgLayers(projection, context) { { id: 'osm', layer: svgOsm(projection, context, dispatch) }, { id: 'notes', layer: svgNotes(projection, context, dispatch) }, { id: 'data', layer: svgData(projection, context, dispatch) }, + { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) }, { id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) }, { id: 'improveOSM', layer: svgImproveOSM(projection, context, dispatch) }, { id: 'osmose', layer: svgOsmose(projection, context, dispatch) }, diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js new file mode 100644 index 000000000..c2f8d0a86 --- /dev/null +++ b/modules/svg/local_photos.js @@ -0,0 +1,440 @@ +import _throttle from 'lodash-es/throttle'; + +import { utilDetect } from '../util/detect'; +import { select as d3_select } from 'd3-selection'; +import { svgPath, svgPointTransform } from './helpers'; +// import { services } from '../services'; +// Modern Node.js can import CommonJS +import exifr from 'exifr'; // => exifr/dist/full.umd.cjs + +// new +var _initialized = false; +var _enabled = false; + +export function svgLocalPhotos(projection, context, dispatch) { + // required + const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + const minZoom = 12; + const minMarkerZoom = 16; + const minViewfieldZoom = 18; + var detected = utilDetect(); + let layer = d3_select(null); + // maybe required + // let _mapillary; + var _fileList; + + // instead of svgLocalPhotos.something, using global variable at top + // function init() { + // if (svgMapillaryImages.initialized) return; // run once + // svgMapillaryImages.enabled = false; + // svgMapillaryImages.initialized = true; + // } + + // new + function init() { + console.log('inti() called'); + 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.svgData', function(d3_event) { + .on('drop.svgLocalPhotos', function(d3_event) { + d3_event.stopPropagation(); + d3_event.preventDefault(); + if (!detected.filedrop) return; + drawPhotos.fileList(d3_event.dataTransfer.files); + }) + // .on('dragenter.svgData', over) + // .on('dragexit.svgData', over) + // .on('dragover.svgData', over); + .on('dragenter.svgLocalPhotos', over) + .on('dragexit.svgLocalPhotos', over) + .on('dragover.svgLocalPhotos', over); + + _initialized = true; + } + + function showLayer() { + console.log('showLayer() called'); + // if images are not available return + // const service = getService(); + // if (!service) return; + + // same as layerOn() in data.js + editOn(); + + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end', function () { dispatch.call('change'); }); + } + + + function hideLayer() { + console.log('hideLayer() called'); + throttledRedraw.cancel(); + + layer + .transition() + .duration(250) + .style('opacity', 0) + .on('end', editOff); + } + + // same as layerOn() in data.js + function editOn() { + console.log('editOn() called'); + layer.style('display', 'block'); + } + + + // same as layerOff() in data.js + function editOff() { + console.log('editOff() called'); + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + // opens the image at bottom left + function click(d3_event, image) { + console.log('click() called'); + // const service = getService(); + // if (!service) return; + + // service + // .ensureViewerLoaded(context) + // .then(function() { + // service + // .selectImage(context, image.id) + // .showViewer(context); + // }); + + context.map().centerEase(image.loc); + } + + + function mouseover(d3_event, image) { + console.log('mouseover() called'); + // const service = getService(); + + // if (service) service.setStyles(context, image); + } + + + function mouseout() { + console.log('mouseout() called'); + // const service = getService(); + // if (service) service.setStyles(context, null); + } + + // if you want to put any image with geo coordinates + // this is coordinates transformation + // converting gps coordinates on screen + function transform(d) { + console.log('transform() called'); + let t = svgPointTransform(projection)(d); + if (d.ca) { + t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + } + return t; + } + + // a sequence is a list of images + // no need to filter sequence + // function filterSequences(sequences) {...} + + // puts the images on the map + function update() { + console.log('update() called'); + const z = ~~context.map().zoom(); + const showMarkers = (z >= minMarkerZoom); + const showViewfields = (z >= minViewfieldZoom); + + // const service = getService(); + // let sequences = (service ? service.sequences(projection) : []); + // let images = (service && showMarkers ? service.images(projection) : []); + + // images = filterImages(images); + // sequences = filterSequences(sequences, service); + + // service.filterViewer(context); + + let traces = layer.selectAll('.sequences').selectAll('.sequence'); + // .data(sequences, function(d) { return d.properties.id; }); + + // exit + traces.exit() + .remove(); + + // enter/update + traces = traces.enter() + .append('path') + .attr('class', 'sequence') + .merge(traces) + .attr('d', svgPath(projection).geojson); + + + const groups = layer.selectAll('.markers').selectAll('.viewfield-group'); + // .data(images, function(d) { return d.id; }); + + // exit + groups.exit() + .remove(); + + // enter + const groupsEnter = groups.enter() + .append('g') + .attr('class', 'viewfield-group') + .on('mouseenter', mouseover) + .on('mouseleave', mouseout) + .on('click', click); + + groupsEnter + .append('g') + .attr('class', 'viewfield-scale'); + + // update + const markers = groups + .merge(groupsEnter) + .sort(function(a, b) { + return b.loc[1] - a.loc[1]; // sort Y + }) + .attr('transform', transform) + .select('.viewfield-scale'); + + + markers.selectAll('circle') + .data([0]) + .enter() + .append('circle') + .attr('dx', '0') + .attr('dy', '0') + .attr('r', '6'); + + const viewfields = markers.selectAll('.viewfield') + .data(showViewfields ? [0] : []); + + viewfields.exit() + .remove(); + + viewfields.enter() // viewfields may or may not be drawn... + .insert('path', 'circle') // but if they are, draw below the circles + .attr('class', 'viewfield') + .classed('pano', function() { return this.parentNode.__data__.is_pano; }) + .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + .attr('d', viewfieldPath); + + function viewfieldPath() { + console.log('viewfieldPath() called'); + if (this.parentNode.__data__.is_pano) { + return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; + } else { + return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; + } + } + } + + + // draws the actual images + // create your onw css for this + function drawPhotos(selection) { + console.log('drawPhotos fn called'); + // const enabled = svgMapillaryImages.enabled; + const enabled = _enabled; + // const service = getService(); + + // layer = selection.selectAll('.local-photos') + // layer = selection.selectAll('.layer-local-photos'); + // layer = selection.selectAll('.data'); + // .data(service ? [0] : []); + // .data([0]); + + // layer.exit() + // .remove(); + + // const layerEnter = layer.enter() + // .append('g') + // // .attr('class', 'layer-local-photos') + // .attr('class', 'layer-mapillary') + // .style('display', enabled ? 'block' : 'none'); + + // layerEnter + // .append('g') + // .attr('class', 'sequences'); + + // layerEnter + // .append('g') + // .attr('class', 'markers'); + + // layer = layerEnter + // .merge(layer); + + // if (enabled) { + // if (~~context.map().zoom() >= minZoom) { + // editOn(); + // update(); + // // service.loadImages(projection); + // } else { + // editOff(); + // } + // } + } + + + // drawImages.enabled = function(_) { + // if (!arguments.length) return svgMapillaryImages.enabled; + // svgMapillaryImages.enabled = _; + // if (svgMapillaryImages.enabled) { + // showLayer(); + // context.photos().on('change.mapillary_images', update); + // } else { + // hideLayer(); + // context.photos().on('change.mapillary_images', null); + // } + // dispatch.call('change'); + // return this; + // }; + + // new + // use this since using global value + // slightly modified for photos + drawPhotos.enabled = function(val) { + console.log('drawPhotos.enabled called'); + if (!arguments.length) return _enabled; + + _enabled = val; + if (_enabled) { + showLayer(); + // context.photos().on('change.mapillary_images', update); + context.photos().on('change.local_photos', update); + } else { + hideLayer(); + // context.photos().on('change.mapillary_images', null); + context.photos().on('change.local_photos', null); + } + + dispatch.call('change'); + return this; + }; + + function extract_exif(image) { + var reader = new FileReader(); + + reader.onload = function () { + exifr.parse(image) + .then(output => console.log('Image parsed', output)); + }; + + reader.readAsText(image); + } + + // Step 2 + // this is where the exif parsing library comes into play + // get all info from the image + // drawPhotos.setFile = function(extension, data) { + // drawPhotos.setFile = function(fileList) { + drawPhotos.setFile = function(file) { + console.log('drawPhotos.setFile called'); + _fileList = null; + + // fileList.forEach(image => + // extract_exif(image) + // .then(console.log('All images parsed successfully')) + // .catch(err => console.log(err)) + // ); + + extract_exif(file); + + dispatch.call('change'); + return this; + }; + + // Step 1: entry point + drawPhotos.fileList = function(fileList) { + console.log('drawPhotos.fileList called'); + console.log('Step 2: fileList read in local_photos.js', fileList); + // if (!arguments.length) return _fileList; + + _fileList = fileList; + + if (!fileList || !fileList.length) return this; + // its just fetching one entry + // fetch all for local photos + // probablay a promise is required + var f = fileList[0]; + drawPhotos.setFile(f); + + // var reader = new FileReader(); + // reader.onload = (function() { + // return function(e) { + // // Step 2 + // drawPhotos.setFile(extension, e.target.result); + // }; + // })(f); + + return this; + }; + + // new + // when all photos are uploaded, zoom to see them all + drawPhotos.fitZoom = function() { + console.log('drawPhotos.fitZoom called'); + // var features = getFeatures(_geojson); + // if (!features.length) return; + + var map = context.map(); + var viewport = map.trimmedExtent().polygon(); + // features is not defined + // var coords = features.reduce(function(coords) { + // var geom = feature.geometry; + // if (!geom) return coords; + + // var c = geom.coordinates; + + // /* eslint-disable no-fallthrough */ + // switch (geom.type) { + // case 'Point': + // c = [c]; + // case 'MultiPoint': + // case 'LineString': + // break; + + // case 'MultiPolygon': + // // c = utilArrayFlatten(c); + // case 'Polygon': + // case 'MultiLineString': + // // c = utilArrayFlatten(c); + // break; + // } + // /* eslint-enable no-fallthrough */ + + // return utilArrayUnion(coords, c); + // }, []); + + // if (!geoPolygonIntersectsPolygon(viewport, coords, true)) { + // var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords })); + // map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); + // } + + return this; + }; + + drawPhotos.supported = function() { + console.log('drawPhotos.supported called'); + // return !!getService(); + }; + + + init(); + return drawPhotos; +} diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 7e9d9ea08..a7f9d61a7 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -15,6 +15,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function init() { + console.log('init() called'); if (svgMapillaryImages.initialized) return; // run once svgMapillaryImages.enabled = false; svgMapillaryImages.initialized = true; @@ -22,6 +23,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function getService() { + console.log('getService() called'); if (services.mapillary && !_mapillary) { _mapillary = services.mapillary; _mapillary.event.on('loadedImages', throttledRedraw); @@ -34,6 +36,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function showLayer() { + console.log('showLayer() called'); const service = getService(); if (!service) return; @@ -49,6 +52,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function hideLayer() { + console.log('hideLayer() called'); throttledRedraw.cancel(); layer @@ -60,17 +64,20 @@ export function svgMapillaryImages(projection, context, dispatch) { function editOn() { + console.log('editOn() called'); layer.style('display', 'block'); } function editOff() { + console.log('editOff() called'); layer.selectAll('.viewfield-group').remove(); layer.style('display', 'none'); } function click(d3_event, image) { + console.log('click() called'); const service = getService(); if (!service) return; @@ -87,6 +94,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function mouseover(d3_event, image) { + console.log('mouseover() called'); const service = getService(); if (service) service.setStyles(context, image); @@ -94,12 +102,14 @@ export function svgMapillaryImages(projection, context, dispatch) { function mouseout() { + console.log('mouseout() called'); const service = getService(); if (service) service.setStyles(context, null); } function transform(d) { + console.log('transform() called'); let t = svgPointTransform(projection)(d); if (d.ca) { t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; @@ -109,6 +119,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function filterImages(images) { + console.log('filterImages() called'); const showsPano = context.photos().showsPanoramic(); const showsFlat = context.photos().showsFlat(); const fromDate = context.photos().fromDate(); @@ -135,6 +146,7 @@ export function svgMapillaryImages(projection, context, dispatch) { } function filterSequences(sequences) { + console.log('filterSequences() called'); const showsPano = context.photos().showsPanoramic(); const showsFlat = context.photos().showsFlat(); const fromDate = context.photos().fromDate(); @@ -164,7 +176,7 @@ export function svgMapillaryImages(projection, context, dispatch) { } function update() { - + console.log('update() called'); const z = ~~context.map().zoom(); const showMarkers = (z >= minMarkerZoom); const showViewfields = (z >= minViewfieldZoom); @@ -254,6 +266,7 @@ export function svgMapillaryImages(projection, context, dispatch) { function drawImages(selection) { + console.log('drawImages() called'); const enabled = svgMapillaryImages.enabled; const service = getService(); @@ -292,6 +305,7 @@ export function svgMapillaryImages(projection, context, dispatch) { drawImages.enabled = function(_) { + console.log('drawImages.enabled called'); if (!arguments.length) return svgMapillaryImages.enabled; svgMapillaryImages.enabled = _; if (svgMapillaryImages.enabled) { @@ -307,6 +321,7 @@ export function svgMapillaryImages(projection, context, dispatch) { drawImages.supported = function() { + console.log('drawImages.supported called'); return !!getService(); }; diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index 211004731..16aab4a4f 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -22,6 +22,7 @@ export function uiSectionDataLayers(context) { var settingsLocalPhotosData = uiSettingsLocalPhotosData(context) .on('change', localPhotosChanged); + // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...} var layers = context.layers(); var section = uiSection('data-layers', context) @@ -39,6 +40,7 @@ export function uiSectionDataLayers(context) { .call(drawOsmItems) .call(drawQAItems) .call(drawCustomDataItems) + // .call(drawLocalPhotos) .call(drawVectorItems) // Beta - Detroit mapping challenge .call(drawPanelItems); } @@ -292,6 +294,178 @@ export function uiSectionDataLayers(context) { } } + // original function + // function drawCustomDataItems(selection) { + // var dataLayer = layers.layer('data'); + // var hasData = dataLayer && dataLayer.hasData(); + // var showsData = hasData && dataLayer.enabled(); + + // var ul = selection + // .selectAll('.layer-list-data') + // .data(dataLayer ? [0] : []); + + // // Exit + // ul.exit() + // .remove(); + + // // Enter + // var ulEnter = ul.enter() + // .append('ul') + // .attr('class', 'layer-list layer-list-data'); + + // var liEnter = ulEnter + // .append('li') + // .attr('class', 'list-item-data'); + + // var labelEnter = liEnter + // .append('label') + // .call(uiTooltip() + // .title(() => t.append('map_data.layers.custom.tooltip')) + // .placement('top') + // ); + + // labelEnter + // .append('input') + // .attr('type', 'checkbox') + // .on('change', function() { toggleLayer('data'); }); + + // labelEnter + // .append('span') + // .call(t.append('map_data.layers.custom.title')); + + // liEnter + // .append('button') + // .attr('class', 'open-data-options') + // .call(uiTooltip() + // .title(() => t.append('settings.custom_data.tooltip')) + // .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') + // ) + // .on('click', function(d3_event) { + // d3_event.preventDefault(); + // editCustom(); + // }) + // .call(svgIcon('#iD-icon-more')); + + // liEnter + // .append('button') + // .attr('class', 'zoom-to-data') + // .call(uiTooltip() + // .title(() => t.append('map_data.layers.custom.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(); + // dataLayer.fitZoom(); + // }) + // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + + // // Update + // ul = ul + // .merge(ulEnter); + + // ul.selectAll('.list-item-data') + // .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); + // } + + // added/testing: new function for local photos + function drawLocalPhotos(selection) { + var dataLayer = layers.layer('local-photos'); + console.log(dataLayer, 'dataLayer'); + // var hasData = dataLayer && dataLayer.hasData(); + // var showsData = hasData && dataLayer.enabled(); + + // var ul = selection + // .selectAll('.layer-list-local-photos') + // .data(dataLayer ? [0] : []); + + var ul = selection + .selectAll('.layer-list-local-photos') + .data(dataLayer ? [0] : []); + console.log(ul, 'ul inside drawLocalPhotos'); + + // Exit + ul.exit() + .remove(); + + // Enter + var ulEnter = ul.enter() + .append('ul') + .attr('class', 'layer-list layer-list-data'); + console.log(ulEnter, 'ulEnter inside drawLocalPhotos'); + var localPhotosEnter = ulEnter + .append('li') + .attr('class', 'list-item-local-photos'); + + var localPhotosLabelEnter = localPhotosEnter + .append('label'); + // TODO: Add tooltip + + localPhotosLabelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('local-photos'); }); + + localPhotosLabelEnter + .append('span') + .text('Local Photos'); + + localPhotosEnter + .append('button') + .attr('class', 'open-data-options') + .call(uiTooltip() + .title(t.html('settings.custom_data.tooltip')) + .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.html('map_data.layers.custom.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(); + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + + // Update + ul = ul + .merge(ulEnter); + + // ul.selectAll('.list-item-data') + // .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); + } + + // current fn function drawCustomDataItems(selection) { var dataLayer = layers.layer('data'); var hasData = dataLayer && dataLayer.hasData(); @@ -428,12 +602,9 @@ export function uiSectionDataLayers(context) { .call(settingsCustomData); } - function editLocalPhotos() { - context.container() - .call(settingsLocalPhotosData); - } - function customChanged(d) { + console.log('customChanged called'); + console.log(layers); var dataLayer = layers.layer('data'); if (d && d.url) { @@ -443,11 +614,18 @@ export function uiSectionDataLayers(context) { } } + function editLocalPhotos() { + context.container() + .call(settingsLocalPhotosData); + } + function localPhotosChanged(d) { - var dataLayer = layers.layer('data'); + console.log('localPhotosChanged called'); + var localPhotosLayer = layers.layer('local-photos'); if (d && d.fileList) { - dataLayer.fileList(d.fileList); + console.log('Step 1: fileList set', d.fileList); + localPhotosLayer.fileList(d.fileList); } } diff --git a/modules/ui/settings/local_photos_data.js b/modules/ui/settings/local_photos_data.js index 11b87945a..c6278dcdc 100644 --- a/modules/ui/settings/local_photos_data.js +++ b/modules/ui/settings/local_photos_data.js @@ -10,7 +10,7 @@ export function uiSettingsLocalPhotosData (context) { var dispatch = d3_dispatch('change'); function render(selection) { - var dataLayer = context.layers().layer('data'); + var dataLayer = context.layers().layer('local-photos'); // keep separate copies of original and current settings var _origSettings = { @@ -38,7 +38,7 @@ export function uiSettingsLocalPhotosData (context) { //TODO: Add translation textSection .append('pre') - .text('Choose local photos. Supported types are: .jpg, .jpeg, .png'); + .text('Choose local photos'); // .attr('class', 'instructions-file') // .call(t.append('settings.custom_data.file.instructions')); @@ -46,11 +46,11 @@ export function uiSettingsLocalPhotosData (context) { .append('input') .attr('class', 'field-file') .attr('type', 'file') + .attr('multiple', 'multiple') // .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json') .property('files', _currSettings.fileList) .on('change', function(d3_event) { var files = d3_event.target.files; - console.log(files); if (files && files.length) { // _currSettings.url = ''; // textSection.select('.field-url').property('value', ''); @@ -60,22 +60,6 @@ export function uiSettingsLocalPhotosData (context) { } }); - // textSection - // .append('h4') - // .call(t.append('settings.custom_data.or')); - - // textSection - // .append('pre') - // .attr('class', 'instructions-url') - // .call(t.append('settings.custom_data.url.instructions')); - - // textSection - // .append('textarea') - // .attr('class', 'field-url') - // .attr('placeholder', t('settings.custom_data.url.placeholder')) - // .call(utilNoAuto) - // .property('value', _currSettings.url); - // insert a cancel button var buttonSection = modal.select('.modal-section.buttons'); diff --git a/package.json b/package.json index 6dce58c1f..1e0719e3e 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,10 @@ "alif-toolkit": "^1.2.9", "core-js-bundle": "^3.19.0", "diacritics": "1.3.0", + "exifr": "^7.1.3", "fast-deep-equal": "~3.1.1", "fast-json-stable-stringify": "2.1.0", + "fs": "^0.0.1-security", "lodash-es": "~4.17.15", "marked": "~4.0.12", "node-diff3": "~3.1.0", @@ -77,9 +79,9 @@ "autoprefixer": "^10.0.1", "btoa": "^1.2.1", "chai": "^4.3.4", + "chalk": "^4.1.2", "cldr-core": "37.0.0", "cldr-localenames-full": "37.0.0", - "chalk": "^4.1.2", "concat-files": "^0.1.1", "d3": "~7.4.4", "editor-layer-index": "github:osmlab/editor-layer-index#gh-pages", @@ -97,8 +99,8 @@ "karma-coverage": "2.1.1", "karma-mocha": "^2.0.1", "karma-remap-istanbul": "^0.6.0", - "mapillary-js": "4.1.0", "mapillary_sprite_source": "^1.8.0", + "mapillary-js": "4.1.0", "minimist": "^1.2.3", "mocha": "^9.2.0", "name-suggestion-index": "~6.0", @@ -122,4 +124,4 @@ "browserslist": [ "> 0.2%, last 6 major versions, Firefox ESR, maintained node versions" ] -} \ No newline at end of file +} From 448c8b914cbd410d6d171f35971fa5fe3daea8b3 Mon Sep 17 00:00:00 2001 From: Mukesh Jaiswal Date: Tue, 23 Aug 2022 19:37:54 +0200 Subject: [PATCH 04/23] plop --- modules/svg/data.js | 6 +- modules/svg/local_photos.js | 90 ++++--- modules/ui/sections/data_layers.js | 410 ++++++++++++++--------------- 3 files changed, 261 insertions(+), 245 deletions(-) diff --git a/modules/svg/data.js b/modules/svg/data.js index 11559cb13..0e3b9f372 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -180,6 +180,7 @@ export function svgData(projection, context, dispatch) { function drawData(selection) { console.log('drawData() called'); + console.log('selection', selection); var vtService = getService(); var getPath = svgPath(projection).geojson; var getAreaPath = svgPath(projection, null, true).geojson; @@ -373,11 +374,6 @@ export function svgData(projection, context, dispatch) { stringifyGeojsonProperties(gj); } break; - // case '.png': - // xx = JSON.parse(data); - // console.log('Hello world!'); - // console.log(data); - // break; } gj = gj || {}; diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index c2f8d0a86..47742bff6 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -62,6 +62,7 @@ export function svgLocalPhotos(projection, context, dispatch) { _initialized = true; } + // TODO: after checkbox is implemented function showLayer() { console.log('showLayer() called'); // if images are not available return @@ -79,7 +80,7 @@ export function svgLocalPhotos(projection, context, dispatch) { .on('end', function () { dispatch.call('change'); }); } - + // TODO: after checkbox is implemented function hideLayer() { console.log('hideLayer() called'); throttledRedraw.cancel(); @@ -122,7 +123,7 @@ export function svgLocalPhotos(projection, context, dispatch) { context.map().centerEase(image.loc); } - + // TODO: later function mouseover(d3_event, image) { console.log('mouseover() called'); // const service = getService(); @@ -130,7 +131,7 @@ export function svgLocalPhotos(projection, context, dispatch) { // if (service) service.setStyles(context, image); } - + // TODO: later function mouseout() { console.log('mouseout() called'); // const service = getService(); @@ -153,6 +154,18 @@ export function svgLocalPhotos(projection, context, dispatch) { // no need to filter sequence // function filterSequences(sequences) {...} + // function getService() { + // console.log('getService() called'); + // if (services.mapillary && !_fileList) { + // _fileList = services.mapillary; + // _fileList.event.on('loadedImages', throttledRedraw); + // } else if (!services.mapillary && _fileList) { + // _fileList = null; + // } + + // return _mapillary; + // } + // puts the images on the map function update() { console.log('update() called'); @@ -161,16 +174,19 @@ export function svgLocalPhotos(projection, context, dispatch) { const showViewfields = (z >= minViewfieldZoom); // const service = getService(); + // const service = _fileList; // let sequences = (service ? service.sequences(projection) : []); // let images = (service && showMarkers ? service.images(projection) : []); + // supply dummy data and see the rest of the code // images = filterImages(images); // sequences = filterSequences(sequences, service); // service.filterViewer(context); - let traces = layer.selectAll('.sequences').selectAll('.sequence'); + let traces = layer.selectAll('.sequences').selectAll('.sequence') // .data(sequences, function(d) { return d.properties.id; }); + .data(_fileList, function(d) { return 33;}); // exit traces.exit() @@ -184,8 +200,9 @@ export function svgLocalPhotos(projection, context, dispatch) { .attr('d', svgPath(projection).geojson); - const groups = layer.selectAll('.markers').selectAll('.viewfield-group'); + const groups = layer.selectAll('.markers').selectAll('.viewfield-group') // .data(images, function(d) { return d.id; }); + .data(_fileList, function(d) { return 33;}); // exit groups.exit() @@ -195,8 +212,8 @@ export function svgLocalPhotos(projection, context, dispatch) { const groupsEnter = groups.enter() .append('g') .attr('class', 'viewfield-group') - .on('mouseenter', mouseover) - .on('mouseleave', mouseout) + // .on('mouseenter', mouseover) + // .on('mouseleave', mouseout) .on('click', click); groupsEnter @@ -249,35 +266,33 @@ export function svgLocalPhotos(projection, context, dispatch) { // create your onw css for this function drawPhotos(selection) { console.log('drawPhotos fn called'); + // const enabled = svgMapillaryImages.enabled; const enabled = _enabled; - // const service = getService(); + const fileList = _fileList; - // layer = selection.selectAll('.local-photos') - // layer = selection.selectAll('.layer-local-photos'); - // layer = selection.selectAll('.data'); - // .data(service ? [0] : []); - // .data([0]); + // creates a layer if doesn't exist + layer = selection.selectAll('.layer-local-photos') + .data(fileList ? [0] : []); - // layer.exit() - // .remove(); + layer.exit() + .remove(); - // const layerEnter = layer.enter() - // .append('g') - // // .attr('class', 'layer-local-photos') - // .attr('class', 'layer-mapillary') - // .style('display', enabled ? 'block' : 'none'); + const layerEnter = layer.enter() + .append('g') + .attr('class', 'layer-local-photos') + .style('display', enabled ? 'block' : 'none'); - // layerEnter - // .append('g') - // .attr('class', 'sequences'); + layerEnter + .append('g') + .attr('class', 'sequences'); - // layerEnter - // .append('g') - // .attr('class', 'markers'); + layerEnter + .append('g') + .attr('class', 'markers'); - // layer = layerEnter - // .merge(layer); + layer = layerEnter + .merge(layer); // if (enabled) { // if (~~context.map().zoom() >= minZoom) { @@ -288,6 +303,13 @@ export function svgLocalPhotos(projection, context, dispatch) { // editOff(); // } // } + + if (_fileList) { + editOn(); + update(); + } else { + editOff(); + } } @@ -316,11 +338,11 @@ export function svgLocalPhotos(projection, context, dispatch) { if (_enabled) { showLayer(); // context.photos().on('change.mapillary_images', update); - context.photos().on('change.local_photos', update); + context.photos().on('change.', update); } else { hideLayer(); // context.photos().on('change.mapillary_images', null); - context.photos().on('change.local_photos', null); + context.photos().on('change.', null); } dispatch.call('change'); @@ -345,7 +367,7 @@ export function svgLocalPhotos(projection, context, dispatch) { // drawPhotos.setFile = function(fileList) { drawPhotos.setFile = function(file) { console.log('drawPhotos.setFile called'); - _fileList = null; + // _fileList = null; // fileList.forEach(image => // extract_exif(image) @@ -362,8 +384,8 @@ export function svgLocalPhotos(projection, context, dispatch) { // Step 1: entry point drawPhotos.fileList = function(fileList) { console.log('drawPhotos.fileList called'); - console.log('Step 2: fileList read in local_photos.js', fileList); - // if (!arguments.length) return _fileList; + console.log('Step 2: fileList read', fileList); + if (!arguments.length) return _fileList; _fileList = fileList; @@ -385,6 +407,7 @@ export function svgLocalPhotos(projection, context, dispatch) { return this; }; + // TODO: later // new // when all photos are uploaded, zoom to see them all drawPhotos.fitZoom = function() { @@ -429,6 +452,7 @@ export function svgLocalPhotos(projection, context, dispatch) { return this; }; + // TODO: later drawPhotos.supported = function() { console.log('drawPhotos.supported called'); // return !!getService(); diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index 16aab4a4f..3e88226da 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -40,7 +40,7 @@ export function uiSectionDataLayers(context) { .call(drawOsmItems) .call(drawQAItems) .call(drawCustomDataItems) - // .call(drawLocalPhotos) + .call(drawLocalPhotos) .call(drawVectorItems) // Beta - Detroit mapping challenge .call(drawPanelItems); } @@ -295,104 +295,14 @@ export function uiSectionDataLayers(context) { } // original function - // function drawCustomDataItems(selection) { - // var dataLayer = layers.layer('data'); - // var hasData = dataLayer && dataLayer.hasData(); - // var showsData = hasData && dataLayer.enabled(); - - // var ul = selection - // .selectAll('.layer-list-data') - // .data(dataLayer ? [0] : []); - - // // Exit - // ul.exit() - // .remove(); - - // // Enter - // var ulEnter = ul.enter() - // .append('ul') - // .attr('class', 'layer-list layer-list-data'); - - // var liEnter = ulEnter - // .append('li') - // .attr('class', 'list-item-data'); - - // var labelEnter = liEnter - // .append('label') - // .call(uiTooltip() - // .title(() => t.append('map_data.layers.custom.tooltip')) - // .placement('top') - // ); - - // labelEnter - // .append('input') - // .attr('type', 'checkbox') - // .on('change', function() { toggleLayer('data'); }); - - // labelEnter - // .append('span') - // .call(t.append('map_data.layers.custom.title')); - - // liEnter - // .append('button') - // .attr('class', 'open-data-options') - // .call(uiTooltip() - // .title(() => t.append('settings.custom_data.tooltip')) - // .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') - // ) - // .on('click', function(d3_event) { - // d3_event.preventDefault(); - // editCustom(); - // }) - // .call(svgIcon('#iD-icon-more')); - - // liEnter - // .append('button') - // .attr('class', 'zoom-to-data') - // .call(uiTooltip() - // .title(() => t.append('map_data.layers.custom.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(); - // dataLayer.fitZoom(); - // }) - // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - - // // Update - // ul = ul - // .merge(ulEnter); - - // ul.selectAll('.list-item-data') - // .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); - // } - - // added/testing: new function for local photos - function drawLocalPhotos(selection) { - var dataLayer = layers.layer('local-photos'); - console.log(dataLayer, 'dataLayer'); - // var hasData = dataLayer && dataLayer.hasData(); - // var showsData = hasData && dataLayer.enabled(); - - // var ul = selection - // .selectAll('.layer-list-local-photos') - // .data(dataLayer ? [0] : []); + function drawCustomDataItems(selection) { + var dataLayer = layers.layer('data'); + var hasData = dataLayer && dataLayer.hasData(); + var showsData = hasData && dataLayer.enabled(); var ul = selection - .selectAll('.layer-list-local-photos') + .selectAll('.layer-list-data') .data(dataLayer ? [0] : []); - console.log(ul, 'ul inside drawLocalPhotos'); // Exit ul.exit() @@ -402,7 +312,93 @@ export function uiSectionDataLayers(context) { var ulEnter = ul.enter() .append('ul') .attr('class', 'layer-list layer-list-data'); - console.log(ulEnter, 'ulEnter inside drawLocalPhotos'); + + var liEnter = ulEnter + .append('li') + .attr('class', 'list-item-data'); + + var labelEnter = liEnter + .append('label') + .call(uiTooltip() + .title(() => t.append('map_data.layers.custom.tooltip')) + .placement('top') + ); + + labelEnter + .append('input') + .attr('type', 'checkbox') + .on('change', function() { toggleLayer('data'); }); + + labelEnter + .append('span') + .call(t.append('map_data.layers.custom.title')); + + liEnter + .append('button') + .attr('class', 'open-data-options') + .call(uiTooltip() + .title(() => t.append('settings.custom_data.tooltip')) + .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') + ) + .on('click', function(d3_event) { + d3_event.preventDefault(); + editCustom(); + }) + .call(svgIcon('#iD-icon-more')); + + liEnter + .append('button') + .attr('class', 'zoom-to-data') + .call(uiTooltip() + .title(() => t.append('map_data.layers.custom.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(); + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + + // Update + ul = ul + .merge(ulEnter); + + ul.selectAll('.list-item-data') + .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); + } + + // added/testing: new function for local photos + function drawLocalPhotos(selection) { + var dataLayer = layers.layer('local-photos'); + console.log(dataLayer, 'dataLayer'); + // var hasData = dataLayer && dataLayer.hasData(); + // var showsData = hasData && dataLayer.enabled(); + + // creates ul, if it doesn't exist + var ul = selection + .selectAll('.layer-list-local-photos') + .data(dataLayer ? [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'); @@ -466,136 +462,136 @@ export function uiSectionDataLayers(context) { } // current fn - function drawCustomDataItems(selection) { - var dataLayer = layers.layer('data'); - var hasData = dataLayer && dataLayer.hasData(); - var showsData = hasData && dataLayer.enabled(); + // function drawCustomDataItems(selection) { + // var dataLayer = layers.layer('data'); + // var hasData = dataLayer && dataLayer.hasData(); + // var showsData = hasData && dataLayer.enabled(); - var ul = selection - .selectAll('.layer-list-data') - .data(dataLayer ? [0] : []); + // var ul = selection + // .selectAll('.layer-list-data') + // .data(dataLayer ? [0] : []); - // Exit - ul.exit() - .remove(); + // // Exit + // ul.exit() + // .remove(); - // Enter - var ulEnter = ul.enter() - .append('ul') - .attr('class', 'layer-list layer-list-data'); + // // Enter + // var ulEnter = ul.enter() + // .append('ul') + // .attr('class', 'layer-list layer-list-data'); - // Custom Map Data - var mapEnter = ulEnter - .append('li') - .attr('class', 'list-item-data'); + // // Custom Map Data + // var mapEnter = ulEnter + // .append('li') + // .attr('class', 'list-item-data'); - var mapLabelEnter = mapEnter - .append('label') - .call(uiTooltip() - .title(t.html('map_data.layers.custom.tooltip')) - .placement('top') - ); + // var mapLabelEnter = mapEnter + // .append('label') + // .call(uiTooltip() + // .title(t.html('map_data.layers.custom.tooltip')) + // .placement('top') + // ); - mapLabelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('data'); }); + // mapLabelEnter + // .append('input') + // .attr('type', 'checkbox') + // .on('change', function() { toggleLayer('data'); }); - mapLabelEnter - .append('span') - .call(t.append('map_data.layers.custom.title')); + // mapLabelEnter + // .append('span') + // .call(t.append('map_data.layers.custom.title')); - mapEnter - .append('button') - .attr('class', 'open-data-options') - .call(uiTooltip() - .title(t.html('settings.custom_data.tooltip')) - .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') - ) - .on('click', function(d3_event) { - d3_event.preventDefault(); - editCustom(); - }) - .call(svgIcon('#iD-icon-more')); + // mapEnter + // .append('button') + // .attr('class', 'open-data-options') + // .call(uiTooltip() + // .title(t.html('settings.custom_data.tooltip')) + // .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') + // ) + // .on('click', function(d3_event) { + // d3_event.preventDefault(); + // editCustom(); + // }) + // .call(svgIcon('#iD-icon-more')); - mapEnter - .append('button') - .attr('class', 'zoom-to-data') - .call(uiTooltip() - .title(t.html('map_data.layers.custom.zoom')) - .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') - ) - .on('click', function(d3_event) { - if (d3_select(this).classed('disabled')) return; + // mapEnter + // .append('button') + // .attr('class', 'zoom-to-data') + // .call(uiTooltip() + // .title(t.html('map_data.layers.custom.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(); - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + // d3_event.preventDefault(); + // d3_event.stopPropagation(); + // dataLayer.fitZoom(); + // }) + // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - // new item - local photos - var localPhotosEnter = ulEnter - .append('li') - .attr('class', 'list-item-local-photos'); + // // new item - local photos + // var localPhotosEnter = ulEnter + // .append('li') + // .attr('class', 'list-item-local-photos'); - var localPhotosLabelEnter = localPhotosEnter - .append('label'); - // TODO: Add tooltip + // var localPhotosLabelEnter = localPhotosEnter + // .append('label'); + // // TODO: Add tooltip - localPhotosLabelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('data'); }); + // localPhotosLabelEnter + // .append('input') + // .attr('type', 'checkbox') + // .on('change', function() { toggleLayer('data'); }); - localPhotosLabelEnter - .append('span') - .text('Local Photos'); + // localPhotosLabelEnter + // .append('span') + // .text('Local Photos'); - localPhotosEnter - .append('button') - .attr('class', 'open-data-options') - .call(uiTooltip() - .title(t.html('settings.custom_data.tooltip')) - .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', 'open-data-options') + // .call(uiTooltip() + // .title(t.html('settings.custom_data.tooltip')) + // .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.html('map_data.layers.custom.zoom')) - .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') - ) - .on('click', function(d3_event) { - if (d3_select(this).classed('disabled')) return; + // localPhotosEnter + // .append('button') + // .attr('class', 'zoom-to-data') + // .call(uiTooltip() + // .title(t.html('map_data.layers.custom.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(); - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + // d3_event.preventDefault(); + // d3_event.stopPropagation(); + // dataLayer.fitZoom(); + // }) + // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - // Update - ul = ul - .merge(ulEnter); + // // Update + // ul = ul + // .merge(ulEnter); - ul.selectAll('.list-item-data') - .classed('active', showsData) - .selectAll('label') - .classed('deemphasize', !hasData) - .selectAll('input') - .property('disabled', !hasData) - .property('checked', showsData); + // ul.selectAll('.list-item-data') + // .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); - } + // ul.selectAll('button.zoom-to-data') + // .classed('disabled', !hasData); + // } function editCustom() { context.container() From e86167200da4977d957a5f38418712e319df0503 Mon Sep 17 00:00:00 2001 From: Mukesh Jaiswal Date: Sun, 4 Sep 2022 22:08:42 +0200 Subject: [PATCH 05/23] WIP: displaying images on map --- modules/svg/local_photos.js | 109 ++++++++++++++++++-------------- modules/svg/mapillary_images.js | 11 ++++ 2 files changed, 73 insertions(+), 47 deletions(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 47742bff6..2f2f0b207 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -142,11 +142,12 @@ export function svgLocalPhotos(projection, context, dispatch) { // this is coordinates transformation // converting gps coordinates on screen function transform(d) { - console.log('transform() called'); - let t = svgPointTransform(projection)(d); - if (d.ca) { - t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; - } + console.log('transform() called with d ', d); + // let t = svgPointTransform(projection)(d); + let t = 'translate(' + d.loc[0] + ',' + d.loc[1] + ')'; + // if (d.ca) { + // t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; + // } return t; } @@ -154,55 +155,68 @@ export function svgLocalPhotos(projection, context, dispatch) { // no need to filter sequence // function filterSequences(sequences) {...} - // function getService() { - // console.log('getService() called'); - // if (services.mapillary && !_fileList) { - // _fileList = services.mapillary; - // _fileList.event.on('loadedImages', throttledRedraw); - // } else if (!services.mapillary && _fileList) { - // _fileList = null; - // } - - // return _mapillary; - // } // puts the images on the map function update() { console.log('update() called'); const z = ~~context.map().zoom(); const showMarkers = (z >= minMarkerZoom); - const showViewfields = (z >= minViewfieldZoom); + // const showViewfields = (z >= minViewfieldZoom); + const showViewfields = true; // const service = getService(); // const service = _fileList; // let sequences = (service ? service.sequences(projection) : []); - // let images = (service && showMarkers ? service.images(projection) : []); // supply dummy data and see the rest of the code + // let images = (service && showMarkers ? service.images(projection) : []); - // images = filterImages(images); - // sequences = filterSequences(sequences, service); + // images[0] + // { + // "loc":[13.235349655151367,52.50694232952122], + // "captured_at":1619457514500, + // "ca":0, + // "id":505488307476058, + // "is_pano":false, + // "sequence_id":"zcyumxorbza3dq3twjybam" + // } + let image_1 = { ca: 63.629999999999995, + captured_at: 1629896192000, + id: 1698202743707180, + is_pano: false, + loc: [ 52.50785, 13.23615 ], + sequence_id: "DMyGn8gtvrBwN1xPbVFHAZ", + } - // service.filterViewer(context); + // let image_2 = { ca: 154.6, + // captured_at: 1629892592000, + // id: 331503412089696, + // is_pano: false, + // loc: [52.50783, 13.23618], + // sequence_id: "eIZiowmur0COgFXAh468db" + // } - let traces = layer.selectAll('.sequences').selectAll('.sequence') + let images = [image_1] + + + // let traces = layer.selectAll('.sequences').selectAll('.sequence') // .data(sequences, function(d) { return d.properties.id; }); - .data(_fileList, function(d) { return 33;}); + // .data(_fileList, function(d) { return 33;}); // exit - traces.exit() - .remove(); + // traces.exit() + // .remove(); // enter/update - traces = traces.enter() - .append('path') - .attr('class', 'sequence') - .merge(traces) - .attr('d', svgPath(projection).geojson); + // traces = traces.enter() + // .append('path') + // .attr('class', 'sequence') + // .merge(traces) + // .attr('d', svgPath(projection).geojson); const groups = layer.selectAll('.markers').selectAll('.viewfield-group') // .data(images, function(d) { return d.id; }); - .data(_fileList, function(d) { return 33;}); + .data(images, function(d) { return d.id; }); // exit groups.exit() @@ -236,7 +250,7 @@ export function svgLocalPhotos(projection, context, dispatch) { .append('circle') .attr('dx', '0') .attr('dy', '0') - .attr('r', '6'); + .attr('r', '15'); const viewfields = markers.selectAll('.viewfield') .data(showViewfields ? [0] : []); @@ -244,21 +258,21 @@ export function svgLocalPhotos(projection, context, dispatch) { viewfields.exit() .remove(); - viewfields.enter() // viewfields may or may not be drawn... - .insert('path', 'circle') // but if they are, draw below the circles - .attr('class', 'viewfield') - .classed('pano', function() { return this.parentNode.__data__.is_pano; }) - .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') - .attr('d', viewfieldPath); + // viewfields.enter() // viewfields may or may not be drawn... + // .insert('path', 'circle') // but if they are, draw below the circles + // .attr('class', 'viewfield') + // .classed('pano', function() { return this.parentNode.__data__.is_pano; }) + // .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') + // .attr('d', viewfieldPath); - function viewfieldPath() { - console.log('viewfieldPath() called'); - if (this.parentNode.__data__.is_pano) { - return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; - } else { - return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; - } - } + // function viewfieldPath() { + // console.log('viewfieldPath() called'); + // if (this.parentNode.__data__.is_pano) { + // return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; + // } else { + // return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; + // } + // } } @@ -266,11 +280,12 @@ export function svgLocalPhotos(projection, context, dispatch) { // create your onw css for this function drawPhotos(selection) { console.log('drawPhotos fn called'); - + // const enabled = svgMapillaryImages.enabled; const enabled = _enabled; const fileList = _fileList; + // creates a layer if doesn't exist layer = selection.selectAll('.layer-local-photos') .data(fileList ? [0] : []); diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index a7f9d61a7..0766619e9 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -111,9 +111,11 @@ export function svgMapillaryImages(projection, context, dispatch) { function transform(d) { console.log('transform() called'); let t = svgPointTransform(projection)(d); + console.log('before ', t); if (d.ca) { t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; } + console.log('after', t); return t; } @@ -184,6 +186,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); From 70f36bc3e115f47eaa03eb881bee81f30156b569 Mon Sep 17 00:00:00 2001 From: Mukesh Jaiswal Date: Mon, 5 Sep 2022 12:04:48 +0200 Subject: [PATCH 06/23] WIP: long,lat found for projection --- modules/svg/helpers.js | 1 + modules/svg/local_photos.js | 114 +++++++++++++++----------------- modules/svg/mapillary_images.js | 6 +- 3 files changed, 56 insertions(+), 65 deletions(-) diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 8c946f6fd..39e3358d1 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -184,6 +184,7 @@ export function svgPointTransform(projection) { var svgpoint = function(entity) { // http://jsperf.com/short-array-join var pt = projection(entity.loc); + console.log('projection point', pt); return 'translate(' + pt[0] + ',' + pt[1] + ')'; }; diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 2f2f0b207..6c7d12156 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -19,17 +19,8 @@ export function svgLocalPhotos(projection, context, dispatch) { const minViewfieldZoom = 18; var detected = utilDetect(); let layer = d3_select(null); - // maybe required - // let _mapillary; var _fileList; - // instead of svgLocalPhotos.something, using global variable at top - // function init() { - // if (svgMapillaryImages.initialized) return; // run once - // svgMapillaryImages.enabled = false; - // svgMapillaryImages.initialized = true; - // } - // new function init() { console.log('inti() called'); @@ -109,6 +100,17 @@ export function svgLocalPhotos(projection, context, dispatch) { // opens the image at bottom left function click(d3_event, image) { console.log('click() called'); + + var width = 750, height = 400; + + var canvas = context.container().select('#container') + .append('canvas') + .attr('width', width) + .attr('height', height) + .style('position', 'absolute'); +// style="position: absolute; width: 320px; height: 240px;" + var canvas_context = canvas.node().getContext('2d'); + // const service = getService(); // if (!service) return; @@ -141,16 +143,23 @@ export function svgLocalPhotos(projection, context, dispatch) { // if you want to put any image with geo coordinates // this is coordinates transformation // converting gps coordinates on screen - function transform(d) { - console.log('transform() called with d ', d); - // let t = svgPointTransform(projection)(d); - let t = 'translate(' + d.loc[0] + ',' + d.loc[1] + ')'; - // if (d.ca) { - // t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; - // } - return t; + function fn_transform(projection) { + var svgpoint = function(entity) { + var pt = projection(entity.loc); + console.log('projection point', pt); + return 'translate(' + pt[0] + ',' + pt[1] + ')'; + }; + + return svgpoint; } + function transform(d) { + console.log('transform() called with', d); + // projection expects [long, lat] + let t = fn_transform(projection)(d); + console.log('after svgPointTransform', t); + return t; + } // a sequence is a list of images // no need to filter sequence // function filterSequences(sequences) {...} @@ -159,6 +168,7 @@ export function svgLocalPhotos(projection, context, dispatch) { // puts the images on the map function update() { console.log('update() called'); + console.log(context.map()); const z = ~~context.map().zoom(); const showMarkers = (z >= minMarkerZoom); // const showViewfields = (z >= minViewfieldZoom); @@ -179,13 +189,13 @@ export function svgLocalPhotos(projection, context, dispatch) { // "is_pano":false, // "sequence_id":"zcyumxorbza3dq3twjybam" // } - let image_1 = { ca: 63.629999999999995, - captured_at: 1629896192000, - id: 1698202743707180, - is_pano: false, - loc: [ 52.50785, 13.23615 ], - sequence_id: "DMyGn8gtvrBwN1xPbVFHAZ", - } + // let image_1 = { ca: 63.629999999999995, + // captured_at: 1629896192000, + // id: 1698202743707180, + // is_pano: false, + // loc: [ 125, 280], + // sequence_id: "DMyGn8gtvrBwN1xPbVFHAZ", + // } // let image_2 = { ca: 154.6, // captured_at: 1629892592000, @@ -195,29 +205,27 @@ export function svgLocalPhotos(projection, context, dispatch) { // sequence_id: "eIZiowmur0COgFXAh468db" // } - let images = [image_1] + let image_1 = { + id: 1, + // loc: [12.99306035041809, 51.99935827101777], + loc: [12, 51] + } + let image_2 = { + id: 2, + // loc: [12.993113994598389, 51.999364876443025], + // loc: [13.23618, 52.50783], + loc: [35.014377, 52] + } - // let traces = layer.selectAll('.sequences').selectAll('.sequence') - // .data(sequences, function(d) { return d.properties.id; }); - // .data(_fileList, function(d) { return 33;}); - - // exit - // traces.exit() - // .remove(); - - // enter/update - // traces = traces.enter() - // .append('path') - // .attr('class', 'sequence') - // .merge(traces) - // .attr('d', svgPath(projection).geojson); + let images = [image_1, image_2] const groups = layer.selectAll('.markers').selectAll('.viewfield-group') // .data(images, function(d) { return d.id; }); .data(images, function(d) { return d.id; }); + // exit groups.exit() .remove(); @@ -237,9 +245,9 @@ export function svgLocalPhotos(projection, context, dispatch) { // update const markers = groups .merge(groupsEnter) - .sort(function(a, b) { - return b.loc[1] - a.loc[1]; // sort Y - }) + // .sort(function(a, b) { + // return b.loc[1] - a.loc[1]; // sort Y + // }) .attr('transform', transform) .select('.viewfield-scale'); @@ -258,21 +266,6 @@ export function svgLocalPhotos(projection, context, dispatch) { viewfields.exit() .remove(); - // viewfields.enter() // viewfields may or may not be drawn... - // .insert('path', 'circle') // but if they are, draw below the circles - // .attr('class', 'viewfield') - // .classed('pano', function() { return this.parentNode.__data__.is_pano; }) - // .attr('transform', 'scale(1.5,1.5),translate(-8, -13)') - // .attr('d', viewfieldPath); - - // function viewfieldPath() { - // console.log('viewfieldPath() called'); - // if (this.parentNode.__data__.is_pano) { - // return 'M 8,13 m -10,0 a 10,10 0 1,0 20,0 a 10,10 0 1,0 -20,0'; - // } else { - // return 'M 6,9 C 8,8.4 8,8.4 10,9 L 16,-2 C 12,-5 4,-5 0,-2 z'; - // } - // } } @@ -282,7 +275,8 @@ export function svgLocalPhotos(projection, context, dispatch) { console.log('drawPhotos fn called'); // const enabled = svgMapillaryImages.enabled; - const enabled = _enabled; + // const enabled = _enabled; + const enabled = true; const fileList = _fileList; @@ -298,10 +292,6 @@ export function svgLocalPhotos(projection, context, dispatch) { .attr('class', 'layer-local-photos') .style('display', enabled ? 'block' : 'none'); - layerEnter - .append('g') - .attr('class', 'sequences'); - layerEnter .append('g') .attr('class', 'markers'); diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 0766619e9..7e76a0da3 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -109,13 +109,13 @@ export function svgMapillaryImages(projection, context, dispatch) { function transform(d) { - console.log('transform() called'); + console.log('transform() called with', d); let t = svgPointTransform(projection)(d); - console.log('before ', t); + console.log('after svgPointTransform', t); if (d.ca) { t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; } - console.log('after', t); + // console.log('after adding rotate', t); return t; } From 3ccc434c83ccc46d35a09dc0852e825f14c0e0ff Mon Sep 17 00:00:00 2001 From: Mukesh Jaiswal Date: Mon, 12 Sep 2022 13:41:19 +0200 Subject: [PATCH 07/23] Refactor: working prototype, needs some error handling and translation --- modules/svg/data.js | 29 -- modules/svg/helpers.js | 1 - modules/svg/local_photos.js | 416 +++++++---------------------- modules/svg/mapillary_images.js | 18 -- modules/ui/sections/data_layers.js | 165 +----------- 5 files changed, 101 insertions(+), 528 deletions(-) diff --git a/modules/svg/data.js b/modules/svg/data.js index 0e3b9f372..7760a594e 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -31,14 +31,12 @@ export function svgData(projection, context, dispatch) { function init() { - console.log('init() called'); if (_initialized) return; // run once _geojson = {}; _enabled = true; function over(d3_event) { - console.log('over() called'); d3_event.stopPropagation(); d3_event.preventDefault(); d3_event.dataTransfer.dropEffect = 'copy'; @@ -61,7 +59,6 @@ export function svgData(projection, context, dispatch) { function getService() { - console.log('getService() called'); if (services.vectorTile && !_vtService) { _vtService = services.vectorTile; _vtService.event.on('loadedData', throttledRedraw); @@ -74,7 +71,6 @@ export function svgData(projection, context, dispatch) { function showLayer() { - console.log('showLayer() called'); layerOn(); layer @@ -87,7 +83,6 @@ export function svgData(projection, context, dispatch) { function hideLayer() { - console.log('hideLayer() called'); throttledRedraw.cancel(); layer @@ -99,13 +94,11 @@ export function svgData(projection, context, dispatch) { function layerOn() { - console.log('layerOn() called'); layer.style('display', 'block'); } function layerOff() { - console.log('layerOff() called'); layer.selectAll('.viewfield-group').remove(); layer.style('display', 'none'); } @@ -113,7 +106,6 @@ export function svgData(projection, context, dispatch) { // ensure that all geojson features in a collection have IDs function ensureIDs(gj) { - console.log('ensureIDs() called'); if (!gj) return null; if (gj.type === 'FeatureCollection') { @@ -129,7 +121,6 @@ export function svgData(projection, context, dispatch) { // ensure that each single Feature object has a unique ID function ensureFeatureID(feature) { - console.log('ensureFeatureID() called'); if (!feature) return; feature.__featurehash__ = utilHashcode(stringify(feature)); return feature; @@ -138,7 +129,6 @@ export function svgData(projection, context, dispatch) { // Prefer an array of Features instead of a FeatureCollection function getFeatures(gj) { - console.log('getFeatures() called'); if (!gj) return []; if (gj.type === 'FeatureCollection') { @@ -150,25 +140,21 @@ export function svgData(projection, context, dispatch) { function featureKey(d) { - console.log('featureKey() called'); return d.__featurehash__; } function isPolygon(d) { - console.log('isPolygon() called'); return d.geometry.type === 'Polygon' || d.geometry.type === 'MultiPolygon'; } function clipPathID(d) { - console.log('clipPathID() called'); return 'ideditor-data-' + d.__featurehash__ + '-clippath'; } function featureClasses(d) { - console.log('featureClasses() called'); return [ 'data' + d.__featurehash__, d.geometry.type, @@ -179,8 +165,6 @@ export function svgData(projection, context, dispatch) { function drawData(selection) { - console.log('drawData() called'); - console.log('selection', selection); var vtService = getService(); var getPath = svgPath(projection).geojson; var getAreaPath = svgPath(projection, null, true).geojson; @@ -285,7 +269,6 @@ export function svgData(projection, context, dispatch) { function drawLabels(selection, textClass, data) { - console.log('drawLabels() called'); var labelPath = d3_geoPath(projection); var labelData = data.filter(function(d) { return _showLabels && d.properties && (d.properties.desc || d.properties.name); @@ -319,7 +302,6 @@ export function svgData(projection, context, dispatch) { function getExtension(fileName) { - console.log('getExtension() called'); if (!fileName) return; var re = /\.(gpx|kml|(geo)?json|png)$/i; @@ -329,13 +311,11 @@ export function svgData(projection, context, dispatch) { function xmlToDom(textdata) { - console.log('xmlToDom() called'); return (new DOMParser()).parseFromString(textdata, 'text/xml'); } function stringifyGeojsonProperties(feature) { - console.log('stringifyGeojsonProperties() called'); const properties = feature.properties; for (const key in properties) { const property = properties[key]; @@ -351,7 +331,6 @@ export function svgData(projection, context, dispatch) { drawData.setFile = function(extension, data) { - console.log('drawData.setFile called'); _template = null; _fileList = null; _geojson = null; @@ -389,7 +368,6 @@ export function svgData(projection, context, dispatch) { drawData.showLabels = function(val) { - console.log('drawData.showLabels called'); if (!arguments.length) return _showLabels; _showLabels = val; @@ -398,7 +376,6 @@ export function svgData(projection, context, dispatch) { drawData.enabled = function(val) { - console.log('drawData.enabled called'); if (!arguments.length) return _enabled; _enabled = val; @@ -414,14 +391,12 @@ export function svgData(projection, context, dispatch) { drawData.hasData = function() { - console.log('drawData.hasData called'); var gj = _geojson || {}; return !!(_template || Object.keys(gj).length); }; drawData.template = function(val, src) { - console.log('drawData.template called'); if (!arguments.length) return _template; // test source against OSM imagery blocklists.. @@ -460,7 +435,6 @@ export function svgData(projection, context, dispatch) { drawData.geojson = function(gj, src) { - console.log('drawData.geojson called'); if (!arguments.length) return _geojson; _template = null; @@ -480,7 +454,6 @@ export function svgData(projection, context, dispatch) { drawData.fileList = function(fileList) { - console.log('drawData.fileList called'); if (!arguments.length) return _fileList; _template = null; @@ -505,7 +478,6 @@ export function svgData(projection, context, dispatch) { drawData.url = function(url, defaultExtension) { - console.log('drawData.url called'); _template = null; _fileList = null; _geojson = null; @@ -538,7 +510,6 @@ export function svgData(projection, context, dispatch) { drawData.fitZoom = function() { - console.log('drawData.fitZoom called'); var features = getFeatures(_geojson); if (!features.length) return; diff --git a/modules/svg/helpers.js b/modules/svg/helpers.js index 39e3358d1..8c946f6fd 100644 --- a/modules/svg/helpers.js +++ b/modules/svg/helpers.js @@ -184,7 +184,6 @@ export function svgPointTransform(projection) { var svgpoint = function(entity) { // http://jsperf.com/short-array-join var pt = projection(entity.loc); - console.log('projection point', pt); return 'translate(' + pt[0] + ',' + pt[1] + ')'; }; diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 6c7d12156..ef9d6dd8a 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -3,31 +3,21 @@ import _throttle from 'lodash-es/throttle'; import { utilDetect } from '../util/detect'; import { select as d3_select } from 'd3-selection'; import { svgPath, svgPointTransform } from './helpers'; -// import { services } from '../services'; // Modern Node.js can import CommonJS import exifr from 'exifr'; // => exifr/dist/full.umd.cjs -// new var _initialized = false; -var _enabled = false; export function svgLocalPhotos(projection, context, dispatch) { - // required const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); - const minZoom = 12; - const minMarkerZoom = 16; - const minViewfieldZoom = 18; var detected = utilDetect(); let layer = d3_select(null); var _fileList; + var _imageList= []; - // new function init() { - console.log('inti() called'); if (_initialized) return; // run once - _enabled = true; - function over(d3_event) { d3_event.stopPropagation(); d3_event.preventDefault(); @@ -36,16 +26,12 @@ export function svgLocalPhotos(projection, context, dispatch) { context.container() .attr('dropzone', 'copy') - // .on('drop.svgData', function(d3_event) { .on('drop.svgLocalPhotos', function(d3_event) { d3_event.stopPropagation(); d3_event.preventDefault(); if (!detected.filedrop) return; drawPhotos.fileList(d3_event.dataTransfer.files); }) - // .on('dragenter.svgData', over) - // .on('dragexit.svgData', over) - // .on('dragover.svgData', over); .on('dragenter.svgLocalPhotos', over) .on('dragexit.svgLocalPhotos', over) .on('dragover.svgLocalPhotos', over); @@ -53,178 +39,53 @@ export function svgLocalPhotos(projection, context, dispatch) { _initialized = true; } - // TODO: after checkbox is implemented - function showLayer() { - console.log('showLayer() called'); - // if images are not available return - // const service = getService(); - // if (!service) return; - - // same as layerOn() in data.js - editOn(); - - layer - .style('opacity', 0) - .transition() - .duration(250) - .style('opacity', 1) - .on('end', function () { dispatch.call('change'); }); - } - - // TODO: after checkbox is implemented - function hideLayer() { - console.log('hideLayer() called'); - throttledRedraw.cancel(); - - layer - .transition() - .duration(250) - .style('opacity', 0) - .on('end', editOff); - } - - // same as layerOn() in data.js - function editOn() { - console.log('editOn() called'); - layer.style('display', 'block'); - } - - - // same as layerOff() in data.js - function editOff() { - console.log('editOff() called'); - layer.selectAll('.viewfield-group').remove(); - layer.style('display', 'none'); + function closePhotoViewer() { + d3_select('.over-map').selectAll('.local-photo-viewer').remove(); } // opens the image at bottom left function click(d3_event, image) { - console.log('click() called'); + // removes old div(s), if any + closePhotoViewer(); - var width = 750, height = 400; + var image_container = d3_select('.over-map') + .append('div') + .attr('style', 'position: relative;margin: 5px;border: 5px solid white;') + .attr('class', 'local-photo-viewer'); - var canvas = context.container().select('#container') - .append('canvas') - .attr('width', width) - .attr('height', height) - .style('position', 'absolute'); -// style="position: absolute; width: 320px; height: 240px;" - var canvas_context = canvas.node().getContext('2d'); + var close_button = image_container + .append('button') + .text('X') + .on('click', function(d3_event) { + d3_event.preventDefault(); + closePhotoViewer(); + }) + .attr('style', 'position: absolute;right: 0;padding: 3px 10px;font-size: medium;border-radius:0;'); - // const service = getService(); - // if (!service) return; + var myimage = image_container + .append('img') + .attr('src', image.src) + .attr('width', 400) + .attr('height', 300); - // service - // .ensureViewerLoaded(context) - // .then(function() { - // service - // .selectImage(context, image.id) - // .showViewer(context); - // }); + // centers the map with image location context.map().centerEase(image.loc); } - // TODO: later - function mouseover(d3_event, image) { - console.log('mouseover() called'); - // const service = getService(); - - // if (service) service.setStyles(context, image); - } - - // TODO: later - function mouseout() { - console.log('mouseout() called'); - // const service = getService(); - // if (service) service.setStyles(context, null); - } - - // if you want to put any image with geo coordinates - // this is coordinates transformation - // converting gps coordinates on screen - function fn_transform(projection) { - var svgpoint = function(entity) { - var pt = projection(entity.loc); - console.log('projection point', pt); - return 'translate(' + pt[0] + ',' + pt[1] + ')'; - }; - - return svgpoint; - } function transform(d) { - console.log('transform() called with', d); // projection expects [long, lat] - let t = fn_transform(projection)(d); - console.log('after svgPointTransform', t); - return t; + var svgpoint = projection(d.loc); + return 'translate(' + svgpoint[0] + ',' + svgpoint[1] + ')'; } - // a sequence is a list of images - // no need to filter sequence - // function filterSequences(sequences) {...} - - - // puts the images on the map - function update() { - console.log('update() called'); - console.log(context.map()); - const z = ~~context.map().zoom(); - const showMarkers = (z >= minMarkerZoom); - // const showViewfields = (z >= minViewfieldZoom); - const showViewfields = true; - - // const service = getService(); - // const service = _fileList; - // let sequences = (service ? service.sequences(projection) : []); - // supply dummy data and see the rest of the code - // 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" - // } - // let image_1 = { ca: 63.629999999999995, - // captured_at: 1629896192000, - // id: 1698202743707180, - // is_pano: false, - // loc: [ 125, 280], - // sequence_id: "DMyGn8gtvrBwN1xPbVFHAZ", - // } - - // let image_2 = { ca: 154.6, - // captured_at: 1629892592000, - // id: 331503412089696, - // is_pano: false, - // loc: [52.50783, 13.23618], - // sequence_id: "eIZiowmur0COgFXAh468db" - // } - - let image_1 = { - id: 1, - // loc: [12.99306035041809, 51.99935827101777], - loc: [12, 51] - } - - let image_2 = { - id: 2, - // loc: [12.993113994598389, 51.999364876443025], - // loc: [13.23618, 52.50783], - loc: [35.014377, 52] - } - - let images = [image_1, image_2] + // puts the image markers on the map + function display_markers(imageList) { + console.log('display_markers() called'); const groups = layer.selectAll('.markers').selectAll('.viewfield-group') - // .data(images, function(d) { return d.id; }); - .data(images, function(d) { return d.id; }); - + .data(imageList, function(d) { return d.id; }); // exit groups.exit() @@ -234,8 +95,6 @@ export function svgLocalPhotos(projection, context, dispatch) { const groupsEnter = groups.enter() .append('g') .attr('class', 'viewfield-group') - // .on('mouseenter', mouseover) - // .on('mouseleave', mouseout) .on('click', click); groupsEnter @@ -245,9 +104,6 @@ export function svgLocalPhotos(projection, context, dispatch) { // update const markers = groups .merge(groupsEnter) - // .sort(function(a, b) { - // return b.loc[1] - a.loc[1]; // sort Y - // }) .attr('transform', transform) .select('.viewfield-scale'); @@ -258,39 +114,29 @@ export function svgLocalPhotos(projection, context, dispatch) { .append('circle') .attr('dx', '0') .attr('dy', '0') - .attr('r', '15'); + .attr('r', '20') + .attr('fill', 'red'); const viewfields = markers.selectAll('.viewfield') - .data(showViewfields ? [0] : []); + .data([0]); viewfields.exit() .remove(); } - - // draws the actual images - // create your onw css for this function drawPhotos(selection) { console.log('drawPhotos fn called'); - // const enabled = svgMapillaryImages.enabled; - // const enabled = _enabled; - const enabled = true; - const fileList = _fileList; - - - // creates a layer if doesn't exist layer = selection.selectAll('.layer-local-photos') - .data(fileList ? [0] : []); + .data(_fileList ? [0] : []); layer.exit() .remove(); const layerEnter = layer.enter() .append('g') - .attr('class', 'layer-local-photos') - .style('display', enabled ? 'block' : 'none'); + .attr('class', 'layer-local-photos'); layerEnter .append('g') @@ -299,170 +145,96 @@ export function svgLocalPhotos(projection, context, dispatch) { layer = layerEnter .merge(layer); - // if (enabled) { - // if (~~context.map().zoom() >= minZoom) { - // editOn(); - // update(); - // // service.loadImages(projection); - // } else { - // editOff(); - // } - // } - - if (_fileList) { - editOn(); - update(); - } else { - editOff(); + // if (_imageList.length !== 0) { + // if (_fileList && _fileList.length !== 0) { + if (_imageList && _imageList.length !== 0) { + display_markers(_imageList); } } - // drawImages.enabled = function(_) { - // if (!arguments.length) return svgMapillaryImages.enabled; - // svgMapillaryImages.enabled = _; - // if (svgMapillaryImages.enabled) { - // showLayer(); - // context.photos().on('change.mapillary_images', update); - // } else { - // hideLayer(); - // context.photos().on('change.mapillary_images', null); - // } - // dispatch.call('change'); - // return this; - // }; + /** + * Reads and parses files + * @param {Array} arrayFiles - Holds array of file - [file_1, file_2, ...] + */ + async function readmultifiles(arrayFiles) { + const filePromises = arrayFiles.map((file, i) => { + // Return a promise per file + return new Promise((resolve, reject) => { + const reader = new FileReader(); + // converts image to base64 + reader.readAsDataURL(file); - // new - // use this since using global value - // slightly modified for photos - drawPhotos.enabled = function(val) { - console.log('drawPhotos.enabled called'); - if (!arguments.length) return _enabled; + reader.onload = async () => { + try { + const response = await exifr.parse(file) + .then(output => { - _enabled = val; - if (_enabled) { - showLayer(); - // context.photos().on('change.mapillary_images', update); - context.photos().on('change.', update); - } else { - hideLayer(); - // context.photos().on('change.mapillary_images', null); - context.photos().on('change.', null); - } + _imageList.push( + { + id: i, + name: file.name, + src: reader.result, + loc: [output.longitude, output.latitude] + } + ); + }); + // Resolve the promise with the response value + resolve(response); + } catch (err) { + reject(err); + } + }; + reader.onerror = (error) => { + reject(error); + }; + }); + }); + + // Wait for all promises to be resolved + const fileInfos = await Promise.all(filePromises); dispatch.call('change'); - return this; }; - function extract_exif(image) { - var reader = new FileReader(); - - reader.onload = function () { - exifr.parse(image) - .then(output => console.log('Image parsed', output)); - }; - - reader.readAsText(image); - } - - // Step 2 - // this is where the exif parsing library comes into play - // get all info from the image - // drawPhotos.setFile = function(extension, data) { - // drawPhotos.setFile = function(fileList) { - drawPhotos.setFile = function(file) { + drawPhotos.setFile = function(fileList) { console.log('drawPhotos.setFile called'); - // _fileList = null; + /** + * Holds array of file - [file_1, file_2, ...] + * file_1 = {name: "Das.png", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: "", size: 859658, …} + * @type {Array} + */ + var arrayFiles = Object.keys(fileList).map(function(k) { return fileList[k]; }); - // fileList.forEach(image => - // extract_exif(image) - // .then(console.log('All images parsed successfully')) - // .catch(err => console.log(err)) - // ); - - extract_exif(file); + // read and parse asynchronously + readmultifiles(arrayFiles); dispatch.call('change'); 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, …} + */ drawPhotos.fileList = function(fileList) { console.log('drawPhotos.fileList called'); - console.log('Step 2: fileList read', fileList); if (!arguments.length) return _fileList; _fileList = fileList; if (!fileList || !fileList.length) return this; - // its just fetching one entry - // fetch all for local photos - // probablay a promise is required - var f = fileList[0]; - drawPhotos.setFile(f); - // var reader = new FileReader(); - // reader.onload = (function() { - // return function(e) { - // // Step 2 - // drawPhotos.setFile(extension, e.target.result); - // }; - // })(f); + drawPhotos.setFile(_fileList); return this; }; - // TODO: later - // new - // when all photos are uploaded, zoom to see them all - drawPhotos.fitZoom = function() { - console.log('drawPhotos.fitZoom called'); - // var features = getFeatures(_geojson); - // if (!features.length) return; - - var map = context.map(); - var viewport = map.trimmedExtent().polygon(); - // features is not defined - // var coords = features.reduce(function(coords) { - // var geom = feature.geometry; - // if (!geom) return coords; - - // var c = geom.coordinates; - - // /* eslint-disable no-fallthrough */ - // switch (geom.type) { - // case 'Point': - // c = [c]; - // case 'MultiPoint': - // case 'LineString': - // break; - - // case 'MultiPolygon': - // // c = utilArrayFlatten(c); - // case 'Polygon': - // case 'MultiLineString': - // // c = utilArrayFlatten(c); - // break; - // } - // /* eslint-enable no-fallthrough */ - - // return utilArrayUnion(coords, c); - // }, []); - - // if (!geoPolygonIntersectsPolygon(viewport, coords, true)) { - // var extent = geoExtent(d3_geoBounds({ type: 'LineString', coordinates: coords })); - // map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); - // } - - return this; - }; - - // TODO: later - drawPhotos.supported = function() { - console.log('drawPhotos.supported called'); - // return !!getService(); - }; - + // TODO: when all photos are uploaded, zoom to see them all + // drawPhotos.fitZoom = function() { + // }; init(); return drawPhotos; diff --git a/modules/svg/mapillary_images.js b/modules/svg/mapillary_images.js index 7e76a0da3..ffa7f88ad 100644 --- a/modules/svg/mapillary_images.js +++ b/modules/svg/mapillary_images.js @@ -15,7 +15,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function init() { - console.log('init() called'); if (svgMapillaryImages.initialized) return; // run once svgMapillaryImages.enabled = false; svgMapillaryImages.initialized = true; @@ -23,7 +22,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function getService() { - console.log('getService() called'); if (services.mapillary && !_mapillary) { _mapillary = services.mapillary; _mapillary.event.on('loadedImages', throttledRedraw); @@ -36,7 +34,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function showLayer() { - console.log('showLayer() called'); const service = getService(); if (!service) return; @@ -52,7 +49,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function hideLayer() { - console.log('hideLayer() called'); throttledRedraw.cancel(); layer @@ -64,20 +60,17 @@ export function svgMapillaryImages(projection, context, dispatch) { function editOn() { - console.log('editOn() called'); layer.style('display', 'block'); } function editOff() { - console.log('editOff() called'); layer.selectAll('.viewfield-group').remove(); layer.style('display', 'none'); } function click(d3_event, image) { - console.log('click() called'); const service = getService(); if (!service) return; @@ -94,7 +87,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function mouseover(d3_event, image) { - console.log('mouseover() called'); const service = getService(); if (service) service.setStyles(context, image); @@ -102,26 +94,21 @@ export function svgMapillaryImages(projection, context, dispatch) { function mouseout() { - console.log('mouseout() called'); const service = getService(); if (service) service.setStyles(context, null); } function transform(d) { - console.log('transform() called with', d); let t = svgPointTransform(projection)(d); - console.log('after svgPointTransform', t); if (d.ca) { t += ' rotate(' + Math.floor(d.ca) + ',0,0)'; } - // console.log('after adding rotate', t); return t; } function filterImages(images) { - console.log('filterImages() called'); const showsPano = context.photos().showsPanoramic(); const showsFlat = context.photos().showsFlat(); const fromDate = context.photos().fromDate(); @@ -148,7 +135,6 @@ export function svgMapillaryImages(projection, context, dispatch) { } function filterSequences(sequences) { - console.log('filterSequences() called'); const showsPano = context.photos().showsPanoramic(); const showsFlat = context.photos().showsFlat(); const fromDate = context.photos().fromDate(); @@ -178,7 +164,6 @@ export function svgMapillaryImages(projection, context, dispatch) { } function update() { - console.log('update() called'); const z = ~~context.map().zoom(); const showMarkers = (z >= minMarkerZoom); const showViewfields = (z >= minViewfieldZoom); @@ -277,7 +262,6 @@ export function svgMapillaryImages(projection, context, dispatch) { function drawImages(selection) { - console.log('drawImages() called'); const enabled = svgMapillaryImages.enabled; const service = getService(); @@ -316,7 +300,6 @@ export function svgMapillaryImages(projection, context, dispatch) { drawImages.enabled = function(_) { - console.log('drawImages.enabled called'); if (!arguments.length) return svgMapillaryImages.enabled; svgMapillaryImages.enabled = _; if (svgMapillaryImages.enabled) { @@ -332,7 +315,6 @@ export function svgMapillaryImages(projection, context, dispatch) { drawImages.supported = function() { - console.log('drawImages.supported called'); return !!getService(); }; diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index 3e88226da..591ae1b96 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -294,7 +294,6 @@ export function uiSectionDataLayers(context) { } } - // original function function drawCustomDataItems(selection) { var dataLayer = layers.layer('data'); var hasData = dataLayer && dataLayer.hasData(); @@ -378,18 +377,13 @@ export function uiSectionDataLayers(context) { .classed('disabled', !hasData); } - // added/testing: new function for local photos function drawLocalPhotos(selection) { var dataLayer = layers.layer('local-photos'); - console.log(dataLayer, 'dataLayer'); - // var hasData = dataLayer && dataLayer.hasData(); - // var showsData = hasData && dataLayer.enabled(); - // creates ul, if it doesn't exist var ul = selection .selectAll('.layer-list-local-photos') .data(dataLayer ? [0] : []); - + // Exit ul.exit() .remove(); @@ -398,7 +392,7 @@ export function uiSectionDataLayers(context) { 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'); @@ -407,10 +401,11 @@ export function uiSectionDataLayers(context) { .append('label'); // TODO: Add tooltip - localPhotosLabelEnter - .append('input') - .attr('type', 'checkbox') - .on('change', function() { toggleLayer('local-photos'); }); + // TODO + // localPhotosLabelEnter + // .append('input') + // .attr('type', 'checkbox') + // .on('change', function() { toggleLayer('local-photos'); }); localPhotosLabelEnter .append('span') @@ -449,158 +444,14 @@ export function uiSectionDataLayers(context) { ul = ul .merge(ulEnter); - // ul.selectAll('.list-item-data') - // .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); } - // current fn - // function drawCustomDataItems(selection) { - // var dataLayer = layers.layer('data'); - // var hasData = dataLayer && dataLayer.hasData(); - // var showsData = hasData && dataLayer.enabled(); - - // var ul = selection - // .selectAll('.layer-list-data') - // .data(dataLayer ? [0] : []); - - // // Exit - // ul.exit() - // .remove(); - - // // Enter - // var ulEnter = ul.enter() - // .append('ul') - // .attr('class', 'layer-list layer-list-data'); - - // // Custom Map Data - // var mapEnter = ulEnter - // .append('li') - // .attr('class', 'list-item-data'); - - // var mapLabelEnter = mapEnter - // .append('label') - // .call(uiTooltip() - // .title(t.html('map_data.layers.custom.tooltip')) - // .placement('top') - // ); - - // mapLabelEnter - // .append('input') - // .attr('type', 'checkbox') - // .on('change', function() { toggleLayer('data'); }); - - // mapLabelEnter - // .append('span') - // .call(t.append('map_data.layers.custom.title')); - - // mapEnter - // .append('button') - // .attr('class', 'open-data-options') - // .call(uiTooltip() - // .title(t.html('settings.custom_data.tooltip')) - // .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') - // ) - // .on('click', function(d3_event) { - // d3_event.preventDefault(); - // editCustom(); - // }) - // .call(svgIcon('#iD-icon-more')); - - // mapEnter - // .append('button') - // .attr('class', 'zoom-to-data') - // .call(uiTooltip() - // .title(t.html('map_data.layers.custom.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(); - // dataLayer.fitZoom(); - // }) - // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - - // // new item - local photos - // var localPhotosEnter = ulEnter - // .append('li') - // .attr('class', 'list-item-local-photos'); - - // var localPhotosLabelEnter = localPhotosEnter - // .append('label'); - // // TODO: Add tooltip - - // localPhotosLabelEnter - // .append('input') - // .attr('type', 'checkbox') - // .on('change', function() { toggleLayer('data'); }); - - // localPhotosLabelEnter - // .append('span') - // .text('Local Photos'); - - // localPhotosEnter - // .append('button') - // .attr('class', 'open-data-options') - // .call(uiTooltip() - // .title(t.html('settings.custom_data.tooltip')) - // .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.html('map_data.layers.custom.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(); - // dataLayer.fitZoom(); - // }) - // .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - - // // Update - // ul = ul - // .merge(ulEnter); - - // ul.selectAll('.list-item-data') - // .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 editCustom() { context.container() .call(settingsCustomData); } function customChanged(d) { - console.log('customChanged called'); - console.log(layers); var dataLayer = layers.layer('data'); if (d && d.url) { @@ -616,11 +467,9 @@ export function uiSectionDataLayers(context) { } function localPhotosChanged(d) { - console.log('localPhotosChanged called'); var localPhotosLayer = layers.layer('local-photos'); if (d && d.fileList) { - console.log('Step 1: fileList set', d.fileList); localPhotosLayer.fileList(d.fileList); } } From 1a3d8a630495a1070f96caf4a6040d622d0cfdb7 Mon Sep 17 00:00:00 2001 From: Mukesh Jaiswal Date: Mon, 12 Sep 2022 16:41:30 +0200 Subject: [PATCH 08/23] Refactor: removed console log --- modules/svg/local_photos.js | 6 ------ modules/ui/sections/data_layers.js | 1 + 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index ef9d6dd8a..43300b91d 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -82,8 +82,6 @@ export function svgLocalPhotos(projection, context, dispatch) { // puts the image markers on the map function display_markers(imageList) { - console.log('display_markers() called'); - const groups = layer.selectAll('.markers').selectAll('.viewfield-group') .data(imageList, function(d) { return d.id; }); @@ -126,8 +124,6 @@ export function svgLocalPhotos(projection, context, dispatch) { } function drawPhotos(selection) { - console.log('drawPhotos fn called'); - layer = selection.selectAll('.layer-local-photos') .data(_fileList ? [0] : []); @@ -198,7 +194,6 @@ export function svgLocalPhotos(projection, context, dispatch) { }; drawPhotos.setFile = function(fileList) { - console.log('drawPhotos.setFile called'); /** * Holds array of file - [file_1, file_2, ...] * file_1 = {name: "Das.png", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: "", size: 859658, …} @@ -220,7 +215,6 @@ export function svgLocalPhotos(projection, context, dispatch) { * @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, …} */ drawPhotos.fileList = function(fileList) { - console.log('drawPhotos.fileList called'); if (!arguments.length) return _fileList; _fileList = fileList; diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index 591ae1b96..929e9f247 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -436,6 +436,7 @@ export function uiSectionDataLayers(context) { d3_event.preventDefault(); d3_event.stopPropagation(); + //TODO dataLayer.fitZoom(); }) .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); From 8c1b185fb37fdc1a71009ecfe0e34e592bf4101d Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 11:03:44 +0200 Subject: [PATCH 09/23] remove unused imports/vars, fix code indentation, lint --- modules/svg/local_photos.js | 67 +++++++++++------------- modules/ui/settings/local_photos_data.js | 2 +- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 43300b91d..045d9bb67 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -1,19 +1,15 @@ -import _throttle from 'lodash-es/throttle'; +import exifr from 'exifr'; import { utilDetect } from '../util/detect'; import { select as d3_select } from 'd3-selection'; -import { svgPath, svgPointTransform } from './helpers'; -// Modern Node.js can import CommonJS -import exifr from 'exifr'; // => exifr/dist/full.umd.cjs var _initialized = false; export function svgLocalPhotos(projection, context, dispatch) { - const throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); var detected = utilDetect(); let layer = d3_select(null); var _fileList; - var _imageList= []; + var _imageList = []; function init() { if (_initialized) return; // run once @@ -49,24 +45,24 @@ export function svgLocalPhotos(projection, context, dispatch) { closePhotoViewer(); var image_container = d3_select('.over-map') - .append('div') - .attr('style', 'position: relative;margin: 5px;border: 5px solid white;') - .attr('class', 'local-photo-viewer'); + .append('div') + .attr('style', 'position: relative;margin: 5px;border: 5px solid white;') + .attr('class', 'local-photo-viewer'); - var close_button = image_container - .append('button') - .text('X') - .on('click', function(d3_event) { - d3_event.preventDefault(); - closePhotoViewer(); - }) - .attr('style', 'position: absolute;right: 0;padding: 3px 10px;font-size: medium;border-radius:0;'); + image_container + .append('button') + .text('X') + .on('click', function(d3_event) { + d3_event.preventDefault(); + closePhotoViewer(); + }) + .attr('style', 'position: absolute;right: 0;padding: 3px 10px;font-size: medium;border-radius:0;'); - var myimage = image_container - .append('img') - .attr('src', image.src) - .attr('width', 400) - .attr('height', 300); + image_container + .append('img') + .attr('src', image.src) + .attr('width', 400) + .attr('height', 300); // centers the map with image location @@ -141,9 +137,9 @@ export function svgLocalPhotos(projection, context, dispatch) { layer = layerEnter .merge(layer); - // if (_imageList.length !== 0) { - // if (_fileList && _fileList.length !== 0) { - if (_imageList && _imageList.length !== 0) { + // if (_imageList.length !== 0) { + // if (_fileList && _fileList.length !== 0) { + if (_imageList && _imageList.length !== 0) { display_markers(_imageList); } } @@ -164,16 +160,13 @@ export function svgLocalPhotos(projection, context, dispatch) { reader.onload = async () => { try { const response = await exifr.parse(file) - .then(output => { - - _imageList.push( - { - id: i, - name: file.name, - src: reader.result, - loc: [output.longitude, output.latitude] - } - ); + .then(output => { + _imageList.push({ + id: i, + name: file.name, + src: reader.result, + loc: [output.longitude, output.latitude] + }); }); // Resolve the promise with the response value resolve(response); @@ -189,9 +182,9 @@ export function svgLocalPhotos(projection, context, dispatch) { }); // Wait for all promises to be resolved - const fileInfos = await Promise.all(filePromises); + await Promise.all(filePromises); dispatch.call('change'); - }; + } drawPhotos.setFile = function(fileList) { /** diff --git a/modules/ui/settings/local_photos_data.js b/modules/ui/settings/local_photos_data.js index c6278dcdc..766c2d781 100644 --- a/modules/ui/settings/local_photos_data.js +++ b/modules/ui/settings/local_photos_data.js @@ -3,7 +3,7 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { prefs } from '../../core/preferences'; import { t } from '../../core/localizer'; import { uiConfirm } from '../confirm'; -import { utilNoAuto, utilRebind } from '../../util'; +import { utilRebind } from '../../util'; export function uiSettingsLocalPhotosData (context) { From 617d580ccb761a24ba63549b9cabf27446413a09 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 11:28:45 +0200 Subject: [PATCH 10/23] implement fitZoom --- modules/svg/local_photos.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 045d9bb67..ebd2ca8c9 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -2,6 +2,7 @@ import exifr from 'exifr'; import { utilDetect } from '../util/detect'; import { select as d3_select } from 'd3-selection'; +import { geoExtent } from '../geo'; var _initialized = false; @@ -187,15 +188,8 @@ export function svgLocalPhotos(projection, context, dispatch) { } drawPhotos.setFile = function(fileList) { - /** - * Holds array of file - [file_1, file_2, ...] - * file_1 = {name: "Das.png", lastModified: 1625064498536, lastModifiedDate: Wed Jun 30 2021 20:18:18 GMT+0530 (India Standard Time), webkitRelativePath: "", size: 859658, …} - * @type {Array} - */ - var arrayFiles = Object.keys(fileList).map(function(k) { return fileList[k]; }); - // read and parse asynchronously - readmultifiles(arrayFiles); + readmultifiles(Array.from(fileList)); dispatch.call('change'); return this; @@ -216,12 +210,19 @@ export function svgLocalPhotos(projection, context, dispatch) { drawPhotos.setFile(_fileList); + // TODO: when all photos are uploaded, zoom to see them all return this; }; - // TODO: when all photos are uploaded, zoom to see them all - // drawPhotos.fitZoom = function() { - // }; + drawPhotos.fitZoom = function() { + let extent = _imageList + .map(image => image.loc) + .map(l => geoExtent(l, l)) + .reduce((a, b) => a.extend(b)); + + const map = context.map(); + map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); + }; init(); return drawPhotos; From 55ad5aa8569adafab0ed32ea8258eec436432e4f Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 11:39:01 +0200 Subject: [PATCH 11/23] skip images with no location when rendering --- modules/svg/local_photos.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index ebd2ca8c9..2465207c1 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -3,6 +3,7 @@ import exifr from 'exifr'; import { utilDetect } from '../util/detect'; import { select as d3_select } from 'd3-selection'; import { geoExtent } from '../geo'; +import { isArray, isNumber } from 'lodash-es'; var _initialized = false; @@ -79,6 +80,7 @@ export function svgLocalPhotos(projection, context, dispatch) { // 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; }); @@ -168,7 +170,7 @@ export function svgLocalPhotos(projection, context, dispatch) { src: reader.result, loc: [output.longitude, output.latitude] }); - }); + }); // Resolve the promise with the response value resolve(response); } catch (err) { @@ -217,6 +219,7 @@ export function svgLocalPhotos(projection, context, dispatch) { drawPhotos.fitZoom = function() { let extent = _imageList .map(image => image.loc) + .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1])) .map(l => geoExtent(l, l)) .reduce((a, b) => a.extend(b)); From 1572835a484d9d8e5fdb05a288660701042e0fe8 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 11:52:33 +0200 Subject: [PATCH 12/23] move local photos to "Photo Overlays" section --- modules/ui/sections/data_layers.js | 88 ----------------- modules/ui/sections/photo_overlays.js | 97 ++++++++++++++++++- .../{local_photos_data.js => local_photos.js} | 2 +- 3 files changed, 93 insertions(+), 94 deletions(-) rename modules/ui/settings/{local_photos_data.js => local_photos.js} (98%) diff --git a/modules/ui/sections/data_layers.js b/modules/ui/sections/data_layers.js index cf3fee20f..41afb19f9 100644 --- a/modules/ui/sections/data_layers.js +++ b/modules/ui/sections/data_layers.js @@ -12,16 +12,12 @@ import { modeBrowse } from '../../modes/browse'; import { uiCmd } from '../cmd'; import { uiSection } from '../section'; import { uiSettingsCustomData } from '../settings/custom_data'; -import { uiSettingsLocalPhotosData } from '../settings/local_photos_data'; export function uiSectionDataLayers(context) { var settingsCustomData = uiSettingsCustomData(context) .on('change', customChanged); - var settingsLocalPhotosData = uiSettingsLocalPhotosData(context) - .on('change', localPhotosChanged); - // refers to `modules/svg/layers.js` -> function drawLayers(selection) {...} var layers = context.layers(); @@ -40,7 +36,6 @@ export function uiSectionDataLayers(context) { .call(drawOsmItems) .call(drawQAItems) .call(drawCustomDataItems) - .call(drawLocalPhotos) .call(drawVectorItems) // Beta - Detroit mapping challenge .call(drawPanelItems); } @@ -377,76 +372,6 @@ export function uiSectionDataLayers(context) { .classed('disabled', !hasData); } - function drawLocalPhotos(selection) { - var dataLayer = layers.layer('local-photos'); - - var ul = selection - .selectAll('.layer-list-local-photos') - .data(dataLayer ? [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'); - // TODO: Add tooltip - - // TODO - // localPhotosLabelEnter - // .append('input') - // .attr('type', 'checkbox') - // .on('change', function() { toggleLayer('local-photos'); }); - - localPhotosLabelEnter - .append('span') - .text('Local Photos'); - - localPhotosEnter - .append('button') - .attr('class', 'open-data-options') - .call(uiTooltip() - .title(t.html('settings.custom_data.tooltip')) - .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.html('map_data.layers.custom.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(); - //TODO - dataLayer.fitZoom(); - }) - .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); - - // Update - ul = ul - .merge(ulEnter); - - } - function editCustom() { context.container() .call(settingsCustomData); @@ -462,19 +387,6 @@ export function uiSectionDataLayers(context) { } } - function editLocalPhotos() { - context.container() - .call(settingsLocalPhotosData); - } - - function localPhotosChanged(d) { - var localPhotosLayer = layers.layer('local-photos'); - - if (d && d.fileList) { - localPhotosLayer.fileList(d.fileList); - } - } - function drawPanelItems(selection) { var panelsListEnter = selection.selectAll('.md-extras-list') diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index b9a9b5876..99f8abc96 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -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,89 @@ export function uiSectionPhotoOverlays(context) { } } + function drawLocalPhotos(selection) { + var dataLayer = layers.layer('local-photos'); + + var ul = selection + .selectAll('.layer-list-local-photos') + .data(dataLayer ? [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'); + // TODO: Add tooltip + + // TODO + // localPhotosLabelEnter + // .append('input') + // .attr('type', 'checkbox') + // .on('change', function() { toggleLayer('local-photos'); }); + + localPhotosLabelEnter + .append('span') + .text('Local Photos'); + + localPhotosEnter + .append('button') + .attr('class', 'open-data-options') + .call(uiTooltip() + .title(t.html('settings.custom_data.tooltip')) + .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.html('map_data.layers.custom.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(); + //TODO + dataLayer.fitZoom(); + }) + .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); + + // Update + ul = ul + .merge(ulEnter); + + } + + function editLocalPhotos() { + context.container() + .call(settingsLocalPhotos); + } + + function localPhotosChanged(d) { + var localPhotosLayer = layers.layer('local-photos'); + + if (d && d.fileList) { + localPhotosLayer.fileList(d.fileList); + } + } + context.layers().on('change.uiSectionPhotoOverlays', section.reRender); context.photos().on('change.uiSectionPhotoOverlays', section.reRender); diff --git a/modules/ui/settings/local_photos_data.js b/modules/ui/settings/local_photos.js similarity index 98% rename from modules/ui/settings/local_photos_data.js rename to modules/ui/settings/local_photos.js index 766c2d781..423cb5558 100644 --- a/modules/ui/settings/local_photos_data.js +++ b/modules/ui/settings/local_photos.js @@ -6,7 +6,7 @@ import { uiConfirm } from '../confirm'; import { utilRebind } from '../../util'; -export function uiSettingsLocalPhotosData (context) { +export function uiSettingsLocalPhotos(context) { var dispatch = d3_dispatch('change'); function render(selection) { From e94087c5d440371f3da9b387bba4b678f8ca2262 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 16:00:10 +0200 Subject: [PATCH 13/23] don't zoom in too far when centering on a single photo --- modules/svg/local_photos.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 2465207c1..b99efaab7 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -224,7 +224,7 @@ export function svgLocalPhotos(projection, context, dispatch) { .reduce((a, b) => a.extend(b)); const map = context.map(); - map.centerZoom(extent.center(), map.trimmedExtentZoom(extent)); + map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent))); }; init(); From 634ce10d24a6d4052bfeffc957dd422bfbefe5a8 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Mon, 7 Aug 2023 16:07:16 +0200 Subject: [PATCH 14/23] add proper local photo strings, enable layer switcher checkbox --- data/core.yaml | 7 +++++ modules/svg/layers.js | 2 +- modules/svg/local_photos.js | 45 +++++++++++++++++++++++++++ modules/ui/sections/photo_overlays.js | 40 +++++++++++++++--------- modules/ui/settings/local_photos.js | 30 +++--------------- 5 files changed, 82 insertions(+), 42 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 6320908e5..f2e911a8c 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1442,6 +1442,13 @@ 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 + file: + instructions: "Choose georeferenced photos to be displayed. Supported types are:\n .jpg with exif location data" + label: "Browse files" note: note: Note title: Edit note diff --git a/modules/svg/layers.js b/modules/svg/layers.js index b61af08ad..b213334d4 100644 --- a/modules/svg/layers.js +++ b/modules/svg/layers.js @@ -30,7 +30,6 @@ export function svgLayers(projection, context) { { id: 'osm', layer: svgOsm(projection, context, dispatch) }, { id: 'notes', layer: svgNotes(projection, context, dispatch) }, { id: 'data', layer: svgData(projection, context, dispatch) }, - { id: 'local-photos', layer: svgLocalPhotos(projection, context, dispatch) }, { id: 'keepRight', layer: svgKeepRight(projection, context, dispatch) }, { id: 'improveOSM', layer: svgImproveOSM(projection, context, dispatch) }, { id: 'osmose', layer: svgOsmose(projection, context, dispatch) }, @@ -42,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) }, diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index b99efaab7..974da61a4 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -6,6 +6,7 @@ import { geoExtent } from '../geo'; import { isArray, isNumber } from 'lodash-es'; var _initialized = false; +var _enabled = false; export function svgLocalPhotos(projection, context, dispatch) { var detected = utilDetect(); @@ -16,6 +17,8 @@ export function svgLocalPhotos(projection, context, dispatch) { function init() { if (_initialized) return; // run once + _enabled = true; + function over(d3_event) { d3_event.stopPropagation(); d3_event.preventDefault(); @@ -227,6 +230,48 @@ export function svgLocalPhotos(projection, context, dispatch) { 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(_imageList) && _imageList.length > 0; + }; + + init(); return drawPhotos; } diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 99f8abc96..3680f98be 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -340,11 +340,13 @@ export function uiSectionPhotoOverlays(context) { } function drawLocalPhotos(selection) { - var dataLayer = layers.layer('local-photos'); + 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(dataLayer ? [0] : []); + .data(photoLayer ? [0] : []); // Exit ul.exit() @@ -360,24 +362,22 @@ export function uiSectionPhotoOverlays(context) { .attr('class', 'list-item-local-photos'); var localPhotosLabelEnter = localPhotosEnter - .append('label'); - // TODO: Add tooltip - - // TODO - // localPhotosLabelEnter - // .append('input') - // .attr('type', 'checkbox') - // .on('change', function() { toggleLayer('local-photos'); }); + .append('label') + .call(uiTooltip().title(() => t.append('local_photos.tooltip'))); localPhotosLabelEnter - .append('span') - .text('Local Photos'); + .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.html('settings.custom_data.tooltip')) + .title(() => t.append('local_photos.tooltip_edit')) .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') ) .on('click', function(d3_event) { @@ -390,7 +390,7 @@ export function uiSectionPhotoOverlays(context) { .append('button') .attr('class', 'zoom-to-data') .call(uiTooltip() - .title(t.html('map_data.layers.custom.zoom')) + .title(() => t.append('map_data.layers.custom.zoom')) .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') ) .on('click', function(d3_event) { @@ -399,7 +399,7 @@ export function uiSectionPhotoOverlays(context) { d3_event.preventDefault(); d3_event.stopPropagation(); //TODO - dataLayer.fitZoom(); + photoLayer.fitZoom(); }) .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); @@ -407,6 +407,16 @@ export function uiSectionPhotoOverlays(context) { 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() { diff --git a/modules/ui/settings/local_photos.js b/modules/ui/settings/local_photos.js index 423cb5558..2cf542135 100644 --- a/modules/ui/settings/local_photos.js +++ b/modules/ui/settings/local_photos.js @@ -1,6 +1,5 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; -import { prefs } from '../../core/preferences'; import { t } from '../../core/localizer'; import { uiConfirm } from '../confirm'; import { utilRebind } from '../../util'; @@ -12,14 +11,8 @@ export function uiSettingsLocalPhotos(context) { function render(selection) { var dataLayer = context.layers().layer('local-photos'); - // keep separate copies of original and current settings - var _origSettings = { - fileList: (dataLayer && dataLayer.fileList()) || null, - url: prefs('settings-custom-data-url') - }; var _currSettings = { - fileList: (dataLayer && dataLayer.fileList()) || null, - url: prefs('settings-custom-data-url') + fileList: (dataLayer && dataLayer.fileList()) || null }; // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; @@ -30,17 +23,15 @@ export function uiSettingsLocalPhotos(context) { modal.select('.modal-section.header') .append('h3') - .call(t.append('settings.custom_data.header')); + .call(t.append('local_photos.header')); var textSection = modal.select('.modal-section.message-text'); - //TODO: Add translation textSection .append('pre') - .text('Choose local photos'); - // .attr('class', 'instructions-file') - // .call(t.append('settings.custom_data.file.instructions')); + .attr('class', 'instructions-local-photos') + .call(t.append('local_photos.file.instructions')); textSection .append('input') @@ -52,8 +43,6 @@ export function uiSettingsLocalPhotos(context) { .on('change', function(d3_event) { var files = d3_event.target.files; if (files && files.length) { - // _currSettings.url = ''; - // textSection.select('.field-url').property('value', ''); _currSettings.fileList = files; } else { _currSettings.fileList = null; @@ -83,23 +72,12 @@ export function uiSettingsLocalPhotos(context) { } - // restore the original url function clickCancel() { - textSection.select('.field-url').property('value', _origSettings.url); - prefs('settings-custom-data-url', _origSettings.url); this.blur(); modal.close(); } - // accept the current url function clickSave() { - // _currSettings.url = textSection.select('.field-url').property('value').trim(); - - // one or the other but not both - // if (_currSettings.url) { _currSettings.fileList = null; } - if (_currSettings.fileList) { _currSettings.url = ''; } - - // prefs('settings-custom-data-url', _currSettings.url); this.blur(); modal.close(); dispatch.call('change', this, _currSettings); From 471354af4fb779f2ee36885b7e510cdf1ea73d16 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Tue, 8 Aug 2023 17:12:35 +0200 Subject: [PATCH 15/23] add list of loaded local photos --- css/60_photos.css | 95 +++++++++++++++++ css/80_app.css | 5 +- data/core.yaml | 6 +- modules/svg/local_photos.js | 56 +++++++---- modules/ui/sections/photo_overlays.js | 6 +- modules/ui/sections/raw_tag_editor.js | 6 +- modules/ui/settings/local_photos.js | 140 ++++++++++++++++++-------- 7 files changed, 244 insertions(+), 70 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 4b655c853..4a5a247a9 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -471,3 +471,98 @@ label.streetside-hires { color: #fff; } } + +/* local georeferenced photos */ +.local-photos { + display: flex; +} +.local-photos > div { + width: 50%; +} +.local-photos > div:first-child { + margin-right: 20px; +} + +.preview-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; +} +.preview-local-photos::-webkit-scrollbar { + border-left: none; +} +.preview-local-photos li { + list-style: none; + display: flex; + justify-content: space-between; + height: 30px; +} +.preview-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; +} +.preview-local-photos li:first-child span.filename { + border-top: 1px solid #ccc; + border-top-left-radius: 4px; +} +.preview-local-photos li:first-child button { + border-top: 1px solid #ccc; +} +.preview-local-photos li:first-child button.remove { + border-top-right-radius: 4px; +} +.preview-local-photos li:last-child span.filename { + border-bottom-left-radius: 4px; +} +.preview-local-photos li:last-child button.remove { + border-bottom-right-radius: 4px; +} +.preview-local-photos li.invalid span.filename { + color: #ccc; +} +/*.preview-local-photos li.invalid span.filename::before { + content: "! "; + color: red; +}*/ +.preview-local-photos li.invalid button.zoom-to-data { + display: none; +} +.preview-local-photos li button.no-geolocation { + display: none; +} +.preview-local-photos li.invalid button.no-geolocation { + display: block; +} +.preview-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; +} \ No newline at end of file diff --git a/css/80_app.css b/css/80_app.css index faab82563..ca774fc5f 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -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 diff --git a/data/core.yaml b/data/core.yaml index f2e911a8c..4eb68fd64 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1446,9 +1446,13 @@ en: 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:\n .jpg with exif location data" + 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 diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 974da61a4..7cca6aafe 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -1,7 +1,7 @@ +import { select as d3_select } from 'd3-selection'; import exifr from 'exifr'; import { utilDetect } from '../util/detect'; -import { select as d3_select } from 'd3-selection'; import { geoExtent } from '../geo'; import { isArray, isNumber } from 'lodash-es'; @@ -12,7 +12,8 @@ export function svgLocalPhotos(projection, context, dispatch) { var detected = utilDetect(); let layer = d3_select(null); var _fileList; - var _imageList = []; + var _photos = []; + var _idAutoinc = 0; function init() { if (_initialized) return; // run once @@ -45,7 +46,7 @@ export function svgLocalPhotos(projection, context, dispatch) { } // opens the image at bottom left - function click(d3_event, image) { + function click(d3_event, image, zoomTo) { // removes old div(s), if any closePhotoViewer(); @@ -71,7 +72,9 @@ export function svgLocalPhotos(projection, context, dispatch) { // centers the map with image location - context.map().centerEase(image.loc); + if (zoomTo) { + context.map().centerEase(image.loc); + } } @@ -127,7 +130,7 @@ export function svgLocalPhotos(projection, context, dispatch) { function drawPhotos(selection) { layer = selection.selectAll('.layer-local-photos') - .data(_fileList ? [0] : []); + .data(_photos ? [0] : []); layer.exit() .remove(); @@ -143,20 +146,18 @@ export function svgLocalPhotos(projection, context, dispatch) { layer = layerEnter .merge(layer); - // if (_imageList.length !== 0) { - // if (_fileList && _fileList.length !== 0) { - if (_imageList && _imageList.length !== 0) { - display_markers(_imageList); + if (_photos && _photos.length !== 0) { + display_markers(_photos); } } /** * Reads and parses files - * @param {Array} arrayFiles - Holds array of file - [file_1, file_2, ...] + * @param {Array} files - Holds array of file - [file_1, file_2, ...] */ - async function readmultifiles(arrayFiles) { - const filePromises = arrayFiles.map((file, i) => { + async function readmultifiles(files) { + const filePromises = files.map(file => { // Return a promise per file return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -167,8 +168,12 @@ export function svgLocalPhotos(projection, context, dispatch) { try { const response = await exifr.parse(file) .then(output => { - _imageList.push({ - id: i, + if (_photos.find(i => i.name === file.name && i.src === reader.result)) { + // skip if already loaded photos + return; + } + _photos.push({ + id: _idAutoinc++, name: file.name, src: reader.result, loc: [output.longitude, output.latitude] @@ -177,10 +182,12 @@ export function svgLocalPhotos(projection, context, dispatch) { // Resolve the promise with the response value resolve(response); } catch (err) { + console.error(err); // eslint-disable-line no-console reject(err); } }; reader.onerror = (error) => { + console.error(err); // eslint-disable-line no-console reject(error); }; @@ -188,15 +195,14 @@ export function svgLocalPhotos(projection, context, dispatch) { }); // Wait for all promises to be resolved - await Promise.all(filePromises); + await Promise.allSettled(filePromises); + _photos = _photos.sort((a, b) => a.id - b.id); dispatch.call('change'); } drawPhotos.setFile = function(fileList) { // read and parse asynchronously readmultifiles(Array.from(fileList)); - - dispatch.call('change'); return this; }; @@ -219,8 +225,20 @@ export function svgLocalPhotos(projection, context, dispatch) { 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() { - let extent = _imageList + let extent = _photos .map(image => image.loc) .filter(l => isArray(l) && isNumber(l[0]) && isNumber(l[1])) .map(l => geoExtent(l, l)) @@ -268,7 +286,7 @@ export function svgLocalPhotos(projection, context, dispatch) { }; drawPhotos.hasData = function() { - return isArray(_imageList) && _imageList.length > 0; + return isArray(_photos) && _photos.length > 0; }; diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 3680f98be..04beb8bde 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -390,7 +390,7 @@ export function uiSectionPhotoOverlays(context) { .append('button') .attr('class', 'zoom-to-data') .call(uiTooltip() - .title(() => t.append('map_data.layers.custom.zoom')) + .title(() => t.append('local_photos.zoom')) .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') ) .on('click', function(d3_event) { @@ -427,9 +427,7 @@ export function uiSectionPhotoOverlays(context) { function localPhotosChanged(d) { var localPhotosLayer = layers.layer('local-photos'); - if (d && d.fileList) { - localPhotosLayer.fileList(d.fileList); - } + localPhotosLayer.fileList(d); } context.layers().on('change.uiSectionPhotoOverlays', section.reRender); diff --git a/modules/ui/sections/raw_tag_editor.js b/modules/ui/sections/raw_tag_editor.js index 43bd043d5..52d98b668 100644 --- a/modules/ui/sections/raw_tag_editor.js +++ b/modules/ui/sections/raw_tag_editor.js @@ -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); + }); } diff --git a/modules/ui/settings/local_photos.js b/modules/ui/settings/local_photos.js index 2cf542135..a7de7a721 100644 --- a/modules/ui/settings/local_photos.js +++ b/modules/ui/settings/local_photos.js @@ -1,87 +1,139 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { select as d3_select } from 'd3-selection'; import { t } from '../../core/localizer'; import { uiConfirm } from '../confirm'; import { utilRebind } from '../../util'; +import { isArray, isNumber } from 'lodash-es'; +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) { - var dataLayer = context.layers().layer('local-photos'); - var _currSettings = { - fileList: (dataLayer && dataLayer.fileList()) || null - }; - - // var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; - var modal = uiConfirm(selection).okButton(); + modal = uiConfirm(selection).okButton(); modal - .classed('settings-modal settings-custom-data', true); + .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 textSection = modal.select('.modal-section.message-text'); + var instructionsSection = modal.select('.modal-section.message-text .local-photos') + .append('div') + .classed('instructions', true); - textSection - .append('pre') - .attr('class', 'instructions-local-photos') + instructionsSection + .append('p') + .classed('instructions-local-photos', true) .call(t.append('local_photos.file.instructions')); - textSection + instructionsSection .append('input') - .attr('class', 'field-file') + .classed('field-file', true) .attr('type', 'file') .attr('multiple', 'multiple') - // .attr('accept', '.gpx,.kml,.geojson,.json,application/gpx+xml,application/vnd.google-earth.kml+xml,application/geo+json,application/json') - .property('files', _currSettings.fileList) + .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) { - _currSettings.fileList = files; - } else { - _currSettings.fileList = null; + previews + .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 previews = modal.select('.modal-section.message-text .local-photos') + .append('div') + .append('div') + .classed('preview-local-photos', true) - // insert a cancel button - var buttonSection = modal.select('.modal-section.buttons'); + previews + .append('ul'); - buttonSection - .insert('button', '.ok-button') - .attr('class', 'button cancel-button secondary-action') - .call(t.append('confirm.cancel')); + updatePreviews(previews.select('ul')); + context.layers().on('change', () => updatePreviews(previews.select('ul'))); + } - buttonSection.select('.cancel-button') - .on('click.cancel', clickCancel); - - buttonSection.select('.ok-button') - .attr('disabled', isSaveDisabled) - .on('click.save', clickSave); - - - function isSaveDisabled() { - return null; + function updatePreviews(container) { + function locationUnavailable(d) { + return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1])); } + container.selectAll('li.placeholder').remove(); - function clickCancel() { - this.blur(); - modal.close(); - } + let selection = container.selectAll('li') + .data(photoLayer.getPhotos() ?? [], d => d.id); + selection.exit() + .remove(); - function clickSave() { - this.blur(); - modal.close(); - dispatch.call('change', this, _currSettings); - } + 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); + updatePreviews(container); + }); } return utilRebind(render, dispatch, 'on'); From 97fcb18d2a6001f0baa917f5e6c0798b083a60ec Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 11:52:07 +0200 Subject: [PATCH 16/23] switch to (vegbilder's) zoomable plane photo viewer --- css/60_photos.css | 6 +-- modules/services/plane_photo.js | 8 +-- modules/services/vegbilder.js | 26 +++++----- modules/svg/local_photos.js | 80 ++++++++++++++++++++--------- modules/ui/settings/local_photos.js | 2 +- 5 files changed, 76 insertions(+), 46 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 4a5a247a9..dcd9a290c 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -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; diff --git a/modules/services/plane_photo.js b/modules/services/plane_photo.js index f654ef1ea..980d01c22 100644 --- a/modules/services/plane_photo.js +++ b/modules/services/plane_photo.js @@ -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'); diff --git a/modules/services/vegbilder.js b/modules/services/vegbilder.js index 9735fd25b..fe6f506e7 100644 --- a/modules/services/vegbilder.js +++ b/modules/services/vegbilder.js @@ -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. diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 7cca6aafe..c2c5a967b 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -1,19 +1,21 @@ 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 } from '../geo'; -import { isArray, isNumber } from 'lodash-es'; +import planePhotoFrame from '../services/plane_photo'; var _initialized = false; var _enabled = false; export function svgLocalPhotos(projection, context, dispatch) { - var detected = utilDetect(); + const detected = utilDetect(); let layer = d3_select(null); - var _fileList; - var _photos = []; - var _idAutoinc = 0; + let _fileList; + let _photos = []; + let _idAutoinc = 0; + let _photoFrame; function init() { if (_initialized) return; // run once @@ -41,35 +43,63 @@ export function svgLocalPhotos(projection, context, dispatch) { _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; + //_photoFrame.event.on('viewerChanged', () => …); + }); + } + function closePhotoViewer() { - d3_select('.over-map').selectAll('.local-photo-viewer').remove(); + const viewer = context.container().select('.photoviewer'); + if (!viewer.empty()) viewer.datum(null); + + viewer + .classed('hide', true) + .selectAll('.photo-wrapper') + .classed('hide', true); } // opens the image at bottom left function click(d3_event, image, zoomTo) { - // removes old div(s), if any - closePhotoViewer(); + ensureViewerLoaded(context).then(() => { + const viewer = context.container().select('.photoviewer') + .classed('hide', false); - var image_container = d3_select('.over-map') - .append('div') - .attr('style', 'position: relative;margin: 5px;border: 5px solid white;') - .attr('class', 'local-photo-viewer'); + const viewerWrap = viewer.select('.local-photos-wrapper') + .classed('hide', false); - image_container - .append('button') - .text('X') - .on('click', function(d3_event) { - d3_event.preventDefault(); - closePhotoViewer(); - }) - .attr('style', 'position: absolute;right: 0;padding: 3px 10px;font-size: medium;border-radius:0;'); + const attribution = viewerWrap.selectAll('.photo-attribution').text(''); - image_container - .append('img') - .attr('src', image.src) - .attr('width', 400) - .attr('height', 300); + if (image.name) { + attribution + .append('span') + .classed('filename', true) + .text(image.name); + } + _photoFrame + .selectPhoto({ image_path: image.src }, false) + .showPhotoFrame(viewerWrap); + }); // centers the map with image location if (zoomTo) { diff --git a/modules/ui/settings/local_photos.js b/modules/ui/settings/local_photos.js index a7de7a721..07c0b834c 100644 --- a/modules/ui/settings/local_photos.js +++ b/modules/ui/settings/local_photos.js @@ -1,10 +1,10 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; import { select as d3_select } from 'd3-selection'; +import { isArray, isNumber } from 'lodash-es'; import { t } from '../../core/localizer'; import { uiConfirm } from '../confirm'; import { utilRebind } from '../../util'; -import { isArray, isNumber } from 'lodash-es'; import { uiTooltip } from '../tooltip'; import { svgIcon } from '../../svg'; From ab008eb29e6e5e9a6a8b826092a5f5536f628222 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 16:33:46 +0200 Subject: [PATCH 17/23] show direction/viewfield on local photo markers in high zoom --- css/60_photos.css | 6 ++++++ modules/svg/local_photos.js | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index dcd9a290c..3c9122315 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -473,6 +473,12 @@ label.streetside-hires { } /* local georeferenced photos */ +.layer-local-photos { + pointer-events: none; +} +.layer-local-photos .viewfield-group * { + fill: #ed00d9; +} .local-photos { display: flex; } diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index c2c5a967b..883378c78 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -8,6 +8,7 @@ import planePhotoFrame from '../services/plane_photo'; var _initialized = false; var _enabled = false; +const minViewfieldZoom = 16; export function svgLocalPhotos(projection, context, dispatch) { const detected = utilDetect(); @@ -147,15 +148,30 @@ export function svgLocalPhotos(projection, context, dispatch) { .append('circle') .attr('dx', '0') .attr('dy', '0') - .attr('r', '20') - .attr('fill', 'red'); + .attr('r', '6'); + + const showViewfields = context.map().zoom() >= minViewfieldZoom; const viewfields = markers.selectAll('.viewfield') - .data([0]); + .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) { @@ -206,7 +222,8 @@ export function svgLocalPhotos(projection, context, dispatch) { id: _idAutoinc++, name: file.name, src: reader.result, - loc: [output.longitude, output.latitude] + loc: [output.longitude, output.latitude], + direction: output.GPSImgDirection }); }); // Resolve the promise with the response value From a7685f6bf0e074ead356bccca0e70086a4402178 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 16:49:10 +0200 Subject: [PATCH 18/23] highlight selected and/or hovered photos --- modules/svg/local_photos.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 883378c78..99a171d43 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -83,6 +83,7 @@ export function svgLocalPhotos(projection, context, dispatch) { 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') @@ -115,6 +116,16 @@ export function svgLocalPhotos(projection, context, dispatch) { 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])); @@ -129,6 +140,8 @@ export function svgLocalPhotos(projection, context, dispatch) { const groupsEnter = groups.enter() .append('g') .attr('class', 'viewfield-group') + .on('mouseenter', (d3_event, d) => setStyles(d)) + .on('mouseleave', (d3_event, d) => setStyles(null)) .on('click', click); groupsEnter @@ -165,7 +178,7 @@ export function svgLocalPhotos(projection, context, dispatch) { .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)`; + 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() { From 0b03f49da144d83149f5176a3dc70950c1dec493 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 17:36:50 +0200 Subject: [PATCH 19/23] improve drag&drop behavior --- modules/svg/data.js | 12 ++++++++- modules/svg/local_photos.js | 49 +++++++++++++++++++++++-------------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/modules/svg/data.js b/modules/svg/data.js index 7760a594e..9bff20755 100644 --- a/modules/svg/data.js +++ b/modules/svg/data.js @@ -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) @@ -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]; diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 99a171d43..abfd799df 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -3,7 +3,7 @@ import exifr from 'exifr'; import { isArray, isNumber } from 'lodash-es'; import { utilDetect } from '../util/detect'; -import { geoExtent } from '../geo'; +import { geoExtent, geoPolygonIntersectsPolygon } from '../geo'; import planePhotoFrame from '../services/plane_photo'; var _initialized = false; @@ -35,7 +35,11 @@ export function svgLocalPhotos(projection, context, dispatch) { d3_event.stopPropagation(); d3_event.preventDefault(); if (!detected.filedrop) return; - drawPhotos.fileList(d3_event.dataTransfer.files); + drawPhotos.fileList(d3_event.dataTransfer.files, loaded => { + if (loaded.length > 0) { + drawPhotos.fitZoom(); + } + }); }) .on('dragenter.svgLocalPhotos', over) .on('dragexit.svgLocalPhotos', over) @@ -215,7 +219,8 @@ export function svgLocalPhotos(projection, context, dispatch) { * Reads and parses files * @param {Array} files - Holds array of file - [file_1, file_2, ...] */ - async function readmultifiles(files) { + async function readmultifiles(files, callback) { + const loaded = []; const filePromises = files.map(file => { // Return a promise per file return new Promise((resolve, reject) => { @@ -227,22 +232,23 @@ export function svgLocalPhotos(projection, context, dispatch) { try { const response = await exifr.parse(file) .then(output => { - if (_photos.find(i => i.name === file.name && i.src === reader.result)) { - // skip if already loaded photos - return; - } - _photos.push({ + const photo = { id: _idAutoinc++, name: file.name, src: reader.result, loc: [output.longitude, output.latitude], direction: output.GPSImgDirection - }); + }; + loaded.push(photo); + if (!_photos.some(i => i.name === photo.name && i.src === photo.src)) { + // only add photos which have not yet been parsed + _photos.push(photo); + } }); // Resolve the promise with the response value resolve(response); } catch (err) { - console.error(err); // eslint-disable-line no-console + // skip files which are not a supported image file reject(err); } }; @@ -257,12 +263,14 @@ export function svgLocalPhotos(projection, context, dispatch) { // Wait for all promises to be resolved await Promise.allSettled(filePromises); _photos = _photos.sort((a, b) => a.id - b.id); + + if (typeof callback === 'function') callback(loaded); dispatch.call('change'); } - drawPhotos.setFile = function(fileList) { + drawPhotos.setFiles = function(fileList, callback) { // read and parse asynchronously - readmultifiles(Array.from(fileList)); + readmultifiles(Array.from(fileList), callback); return this; }; @@ -271,17 +279,17 @@ export function svgLocalPhotos(projection, context, dispatch) { * 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) { + drawPhotos.fileList = function(fileList, callback) { if (!arguments.length) return _fileList; _fileList = fileList; if (!fileList || !fileList.length) return this; - drawPhotos.setFile(_fileList); + drawPhotos.setFiles(_fileList, callback); - // TODO: when all photos are uploaded, zoom to see them all return this; }; @@ -298,14 +306,19 @@ export function svgLocalPhotos(projection, context, dispatch) { drawPhotos.openPhoto = click; drawPhotos.fitZoom = function() { - let extent = _photos - .map(image => image.loc) + 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(); - map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent))); + var viewport = map.trimmedExtent().polygon(); + + if (!geoPolygonIntersectsPolygon(viewport, coords, true)) { + map.centerZoom(extent.center(), Math.min(18, map.trimmedExtentZoom(extent))); + } }; function showLayer() { From 4419b36eae8f63c3864b4aa9c485234b867d9cf9 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 18:12:42 +0200 Subject: [PATCH 20/23] lazily load local photo files --- modules/svg/local_photos.js | 82 ++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index abfd799df..9812d304b 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -102,9 +102,12 @@ export function svgLocalPhotos(projection, context, dispatch) { .text(image.name); } - _photoFrame - .selectPhoto({ image_path: image.src }, false) - .showPhotoFrame(viewerWrap); + _photoFrame.selectPhoto({ image_path: '' }); + image.getSrc().then(src => { + _photoFrame + .selectPhoto({ image_path: src }) + .showPhotoFrame(viewerWrap); + }); }); // centers the map with image location @@ -215,54 +218,47 @@ export function svgLocalPhotos(projection, context, dispatch) { } + 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} files - Holds array of file - [file_1, file_2, ...] */ async function readmultifiles(files, callback) { const loaded = []; - const filePromises = files.map(file => { - // Return a promise per file - return new Promise((resolve, reject) => { - const reader = new FileReader(); - // converts image to base64 - reader.readAsDataURL(file); - reader.onload = async () => { - try { - const response = await exifr.parse(file) - .then(output => { - const photo = { - id: _idAutoinc++, - name: file.name, - src: reader.result, - loc: [output.longitude, output.latitude], - direction: output.GPSImgDirection - }; - loaded.push(photo); - if (!_photos.some(i => i.name === photo.name && i.src === photo.src)) { - // only add photos which have not yet been parsed - _photos.push(photo); - } - }); - // Resolve the promise with the response value - resolve(response); - } catch (err) { - // skip files which are not a supported image file - reject(err); + for (const file of files) { + try { + const exifData = await exifr.parse(file); + 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(); + const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); + if (!sameNameContent.some(i => i.value === thisContent)) { + _photos.push(photo); } - }; - reader.onerror = (error) => { - console.error(err); // eslint-disable-line no-console - reject(error); - }; - - }); - }); - - // Wait for all promises to be resolved - await Promise.allSettled(filePromises); - _photos = _photos.sort((a, b) => a.id - b.id); + } + } catch (err) { + // skip files which are not a supported image file + } + } if (typeof callback === 'function') callback(loaded); dispatch.call('change'); From cf2e4c990e8c4f0139569dd898436f8682fc5b1e Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 18:24:16 +0200 Subject: [PATCH 21/23] lint --- css/60_photos.css | 32 ++++++++++++--------------- modules/svg/local_photos.js | 19 ++++------------ modules/ui/sections/photo_overlays.js | 1 - modules/ui/settings/local_photos.js | 17 +++++++------- 4 files changed, 26 insertions(+), 43 deletions(-) diff --git a/css/60_photos.css b/css/60_photos.css index 3c9122315..b5f7957a3 100644 --- a/css/60_photos.css +++ b/css/60_photos.css @@ -489,7 +489,7 @@ label.streetside-hires { margin-right: 20px; } -.preview-local-photos { +.list-local-photos { max-height: 40vh; overflow-y: scroll; overflow-x: auto; @@ -498,16 +498,16 @@ label.streetside-hires { margin-left: -100px; padding-left: 100px; } -.preview-local-photos::-webkit-scrollbar { +.list-local-photos::-webkit-scrollbar { border-left: none; } -.preview-local-photos li { +.list-local-photos li { list-style: none; display: flex; justify-content: space-between; height: 30px; } -.preview-local-photos span.filename { +.list-local-photos span.filename { display: block; width: 100%; white-space: nowrap; @@ -519,39 +519,35 @@ label.streetside-hires { border-left: 1px solid #ccc; border-right: 1px solid #ccc; } -.preview-local-photos li:first-child span.filename { +.list-local-photos li:first-child span.filename { border-top: 1px solid #ccc; border-top-left-radius: 4px; } -.preview-local-photos li:first-child button { +.list-local-photos li:first-child button { border-top: 1px solid #ccc; } -.preview-local-photos li:first-child button.remove { +.list-local-photos li:first-child button.remove { border-top-right-radius: 4px; } -.preview-local-photos li:last-child span.filename { +.list-local-photos li:last-child span.filename { border-bottom-left-radius: 4px; } -.preview-local-photos li:last-child button.remove { +.list-local-photos li:last-child button.remove { border-bottom-right-radius: 4px; } -.preview-local-photos li.invalid span.filename { +.list-local-photos li.invalid span.filename { color: #ccc; } -/*.preview-local-photos li.invalid span.filename::before { - content: "! "; - color: red; -}*/ -.preview-local-photos li.invalid button.zoom-to-data { +.list-local-photos li.invalid button.zoom-to-data { display: none; } -.preview-local-photos li button.no-geolocation { +.list-local-photos li button.no-geolocation { display: none; } -.preview-local-photos li.invalid button.no-geolocation { +.list-local-photos li.invalid button.no-geolocation { display: block; } -.preview-local-photos .placeholder div { +.list-local-photos .placeholder div { display: block; height: 40px; width: 40px; diff --git a/modules/svg/local_photos.js b/modules/svg/local_photos.js index 9812d304b..dd8681c1c 100644 --- a/modules/svg/local_photos.js +++ b/modules/svg/local_photos.js @@ -69,20 +69,9 @@ export function svgLocalPhotos(projection, context, dispatch) { return planePhotoFrame.init(context, viewerEnter) .then(planePhotoFrame => { _photoFrame = planePhotoFrame; - //_photoFrame.event.on('viewerChanged', () => …); }); } - function closePhotoViewer() { - const viewer = context.container().select('.photoviewer'); - if (!viewer.empty()) viewer.datum(null); - - viewer - .classed('hide', true) - .selectAll('.photo-wrapper') - .classed('hide', true); - } - // opens the image at bottom left function click(d3_event, image, zoomTo) { ensureViewerLoaded(context).then(() => { @@ -148,7 +137,7 @@ export function svgLocalPhotos(projection, context, dispatch) { .append('g') .attr('class', 'viewfield-group') .on('mouseenter', (d3_event, d) => setStyles(d)) - .on('mouseleave', (d3_event, d) => setStyles(null)) + .on('mouseleave', () => setStyles(null)) .on('click', click); groupsEnter @@ -235,7 +224,7 @@ export function svgLocalPhotos(projection, context, dispatch) { for (const file of files) { try { - const exifData = await exifr.parse(file); + const exifData = await exifr.parse(file); // eslint-disable-line no-await-in-loop const photo = { id: _idAutoinc++, name: file.name, @@ -249,8 +238,8 @@ export function svgLocalPhotos(projection, context, dispatch) { if (sameName.length === 0) { _photos.push(photo); } else { - const thisContent = await photo.getSrc(); - const sameNameContent = await Promise.allSettled(sameName.map(i => i.getSrc())); + 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); } diff --git a/modules/ui/sections/photo_overlays.js b/modules/ui/sections/photo_overlays.js index 04beb8bde..fe76d6dbe 100644 --- a/modules/ui/sections/photo_overlays.js +++ b/modules/ui/sections/photo_overlays.js @@ -398,7 +398,6 @@ export function uiSectionPhotoOverlays(context) { d3_event.preventDefault(); d3_event.stopPropagation(); - //TODO photoLayer.fitZoom(); }) .call(svgIcon('#iD-icon-framed-dot', 'monochrome')); diff --git a/modules/ui/settings/local_photos.js b/modules/ui/settings/local_photos.js index 07c0b834c..1d580e5f1 100644 --- a/modules/ui/settings/local_photos.js +++ b/modules/ui/settings/local_photos.js @@ -1,5 +1,4 @@ import { dispatch as d3_dispatch } from 'd3-dispatch'; -import { select as d3_select } from 'd3-selection'; import { isArray, isNumber } from 'lodash-es'; import { t } from '../../core/localizer'; @@ -49,7 +48,7 @@ export function uiSettingsLocalPhotos(context) { .on('change', function(d3_event) { var files = d3_event.target.files; if (files && files.length) { - previews + photoList .select('ul') .append('li') .classed('placeholder', true) @@ -64,20 +63,20 @@ export function uiSettingsLocalPhotos(context) { .classed('button', true) .call(t.append('local_photos.file.label')); - const previews = modal.select('.modal-section.message-text .local-photos') + const photoList = modal.select('.modal-section.message-text .local-photos') .append('div') .append('div') - .classed('preview-local-photos', true) + .classed('list-local-photos', true); - previews + photoList .append('ul'); - updatePreviews(previews.select('ul')); + updatePhotoList(photoList.select('ul')); - context.layers().on('change', () => updatePreviews(previews.select('ul'))); + context.layers().on('change', () => updatePhotoList(photoList.select('ul'))); } - function updatePreviews(container) { + function updatePhotoList(container) { function locationUnavailable(d) { return !(isArray(d.loc) && isNumber(d.loc[0]) && isNumber(d.loc[1])); } @@ -132,7 +131,7 @@ export function uiSettingsLocalPhotos(context) { selection.select('button.remove') .on('click', (d3_event, d) => { photoLayer.removePhoto(d.id); - updatePreviews(container); + updatePhotoList(container); }); } From 77f31cad0792b8cfe4873b1b637c53364fa9ac9d Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 18:32:23 +0200 Subject: [PATCH 22/23] fix tests --- modules/util/trigger_event.js | 5 ++++- test/spec/svg/layers.js | 9 +++++---- test/spec/ui/sections/raw_tag_editor.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/util/trigger_event.js b/modules/util/trigger_event.js index 2639d60de..817fea141 100644 --- a/modules/util/trigger_event.js +++ b/modules/util/trigger_event.js @@ -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); }); } diff --git a/test/spec/svg/layers.js b/test/spec/svg/layers.js index 0bc48daf9..6440d8b2b 100644 --- a/test/spec/svg/layers.js +++ b/test/spec/svg/layers.js @@ -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; }); }); diff --git a/test/spec/ui/sections/raw_tag_editor.js b/test/spec/ui/sections/raw_tag_editor.js index 5a9c9466e..e34c4f575 100644 --- a/test/spec/ui/sections/raw_tag_editor.js +++ b/test/spec/ui/sections/raw_tag_editor.js @@ -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) { From 2ea9dd1e4d93b0c9f71b5b53b6ea309c6ef31fc7 Mon Sep 17 00:00:00 2001 From: Martin Raifer Date: Wed, 9 Aug 2023 18:39:29 +0200 Subject: [PATCH 23/23] add to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0dd6e132..70e06d3a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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