diff --git a/package.json b/package.json
index 402a4b109..0c50a8f43 100644
--- a/package.json
+++ b/package.json
@@ -104,7 +104,7 @@
"mirror-folder": "^2.1.1",
"mkdirp": "^0.5.1",
"object-assign": "^4.1.1",
- "once": "^1.3.3",
+ "once": "^1.4.0",
"osm-p2p": "^4.0.0",
"osm-p2p-server": "^5.0.0",
"prop-types": "^15.6.0",
@@ -116,6 +116,7 @@
"react-mapfilter": "^3.0.0-beta.7",
"react-spinners": "^0.6.1",
"react-transition-group": "^4.3.0",
+ "request": "^2.88.0",
"run-parallel": "^1.1.9",
"run-series": "^1.1.4",
"safe-fs-blob-store": "^1.0.0",
@@ -129,7 +130,6 @@
"to2": "^1.0.0",
"traverse": "^0.6.6",
"websocket-stream": "^3.1.0",
- "xhr": "^2.5.0",
"xtend": "^4.0.1"
},
"devDependencies": {
diff --git a/src/renderer/components/MapFilter/MapExportDialog.js b/src/renderer/components/MapFilter/MapExportDialog.js
new file mode 100644
index 000000000..464299512
--- /dev/null
+++ b/src/renderer/components/MapFilter/MapExportDialog.js
@@ -0,0 +1,301 @@
+import React, { useState } from 'react'
+import { makeStyles } from '@material-ui/core/styles'
+import Button from '@material-ui/core/Button'
+import Dialog from '@material-ui/core/Dialog'
+import DialogActions from '@material-ui/core/DialogActions'
+import DialogContent from '@material-ui/core/DialogContent'
+import DialogTitle from '@material-ui/core/DialogTitle'
+import { TextField, DialogContentText } from '@material-ui/core'
+import { defineMessages, useIntl, FormattedMessage } from 'react-intl'
+import archiver from 'archiver'
+import logger from 'electron-timber'
+import fs from 'fs'
+import path from 'path'
+import request from 'request'
+import { remote } from 'electron'
+
+import ViewWrapper from 'react-mapfilter/commonjs/ViewWrapper'
+
+const msgs = defineMessages({
+ // Title for webmaps export dialog
+ title: 'Export a map to share online',
+ // Save button
+ save: 'Save',
+ // cancel button
+ cancel: 'Cancel',
+ // Label for field to enter map title
+ titleLabel: 'Map Title',
+ // Label for field to enter map description
+ descriptionLabel: 'Map Description',
+ // Label for field to enter terms and conditions
+ termsLabel: 'Terms & Limitations',
+ // Helper text explaining terms and conditions field
+ termsHint: 'Add terms & limitations about how this data can be used',
+ // Label for field to enter custom map style
+ styleLabel: 'Map Style'
+})
+
+// const defaultMapStyle = 'mapbox://styles/mapbox/outdoors-v11'
+
+const EditDialogContent = ({
+ open,
+ observations,
+ getPreset,
+ getMediaUrl,
+ onClose
+}) => {
+ const classes = useStyles()
+ const { formatMessage } = useIntl()
+ const [saving, setSaving] = useState()
+ const [title, setTitle] = useState()
+ const [description, setDescription] = useState()
+ // const [terms, setTerms] = useState()
+ // const [mapStyle, setMapStyle] = useState(defaultMapStyle)
+
+ const handleClose = () => {
+ setSaving(false)
+ setTitle(undefined)
+ setDescription(undefined)
+ // setTerms(undefined)
+ // setMapStyle(defaultMapStyle)
+ onClose()
+ }
+
+ const handleSave = e => {
+ e.preventDefault()
+ setSaving(true)
+ const points = observationsToGeoJson(observations, getPreset)
+ const metadata = { title: title || '', description: description || '' }
+
+ remote.dialog.showSaveDialog(
+ {
+ title: 'Guardar Mapa',
+ defaultPath: 'mapa-para-web',
+ filters: [{ extensions: ['mapeomap'] }]
+ },
+ filepath => {
+ const filepathWithExtension = path.join(
+ path.dirname(filepath),
+ path.basename(filepath, '.mapeomap') + '.mapeomap'
+ )
+ createArchive(filepathWithExtension, err => {
+ console.log('done', err)
+ handleClose()
+ })
+ }
+ )
+
+ function createArchive (filepath, cb) {
+ var output = fs.createWriteStream(filepath)
+
+ var archive = archiver('zip', {
+ zlib: { level: 9 } // Sets the compression level.
+ })
+
+ output.on('end', function () {
+ console.log('Data has been drained')
+ })
+
+ // listen for all archive data to be written
+ // 'close' event is fired only when a file descriptor is involved
+ output.on('close', function () {
+ logger.log('Exported map ' + archive.pointer() + ' total bytes')
+ cb()
+ })
+
+ archive.on('warning', err => {
+ if (err.code === 'ENOENT') {
+ logger.warn(err)
+ } else {
+ cb(err)
+ }
+ })
+
+ archive.on('error', err => {
+ cb(err)
+ })
+
+ archive.pipe(output)
+
+ archive.append(JSON.stringify(points, null, 2), { name: 'points.json' })
+ archive.append(JSON.stringify(metadata, null, 2), {
+ name: 'metadata.json'
+ })
+
+ points.features.forEach(point => {
+ const imageId = point.properties.image
+ if (!imageId) return
+ const imageStream = request(getMediaUrl(imageId, 'original'))
+ console.log(getMediaUrl(imageId))
+ imageStream.on('error', console.error)
+ archive.append(imageStream, { name: 'images/' + imageId })
+ })
+
+ archive.finalize()
+ }
+ }
+
+ return (
+
+ )
+}
+
+export default function EditDialog ({
+ observations,
+ presets,
+ filter,
+ getMediaUrl,
+ ...otherProps
+}) {
+ return (
+
+ {({ onClickObservation, filteredObservations, getPreset }) => (
+
+ )}
+
+ )
+}
+
+const useStyles = makeStyles(theme => ({
+ appBar: {
+ position: 'relative'
+ },
+ title: {
+ marginLeft: theme.spacing(2),
+ flex: 1
+ },
+ content: {
+ display: 'flex',
+ flexDirection: 'column',
+ paddingTop: 0
+ }
+}))
+
+function observationsToGeoJson (observations = [], getPreset) {
+ const features = observations.map(obs => {
+ const preset = getPreset(obs)
+ const title = preset ? preset.name : 'Observación'
+ const description = obs.tags && (obs.tags.notes || obs.tags.note)
+ const date = obs.created_at
+ const image =
+ obs.attachments && obs.attachments.length > 0
+ ? obs.attachments[obs.attachments.length - 1].id
+ : undefined
+ const coords = obs.lon != null && obs.lat != null && [obs.lon, obs.lat]
+ return {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: coords
+ },
+ properties: {
+ title,
+ description,
+ date,
+ image
+ }
+ }
+ })
+ return {
+ type: 'FeatureCollection',
+ features
+ }
+}
diff --git a/src/renderer/components/MapFilter/MapExportDialog.stories.js b/src/renderer/components/MapFilter/MapExportDialog.stories.js
new file mode 100644
index 000000000..9018d1d09
--- /dev/null
+++ b/src/renderer/components/MapFilter/MapExportDialog.stories.js
@@ -0,0 +1,227 @@
+import React from 'react'
+import { action } from '@storybook/addon-actions'
+
+import MapExportDialog from './MapExportDialog'
+
+export default {
+ title: 'Map Export'
+}
+
+const testObs = [
+ {
+ lon: -76.2040014,
+ lat: -0.1983257,
+ attachments: [
+ {
+ id: '4abe1a942e0a5cd5846d4dcb6f96ce2d.jpg',
+ type: 'image/jpeg'
+ }
+ ],
+ tags: {
+ categoryId: 'asentamiento-ilegal',
+ notes:
+ 'Invasión,sembrios de cacao está hectárea 2 tiene aproximadamente de los invasores'
+ },
+ metadata: {
+ location: {
+ error: false,
+ permission: 'granted',
+ provider: {
+ gpsAvailable: true,
+ passiveAvailable: true,
+ locationServicesEnabled: true,
+ networkAvailable: true
+ },
+ position: {
+ timestamp: 1560894283000,
+ mocked: false,
+ coords: {
+ altitude: 241.2,
+ heading: 282,
+ longitude: -76.2040014,
+ speed: 0,
+ latitude: -0.1983257,
+ accuracy: 3
+ }
+ }
+ }
+ },
+ schemaVersion: 3,
+ type: 'observation',
+ timestamp: '2019-10-02T19:21:44.211Z',
+ created_at: '2019-06-18T21:46:45.703Z',
+ id: '002346c8a8921f5c',
+ links: [
+ '08764aa2dbfd49f42f879950d364c8c28d2e6374de48d9982281ffc88dfb7223@0'
+ ],
+ version:
+ '08764aa2dbfd49f42f879950d364c8c28d2e6374de48d9982281ffc88dfb7223@1'
+ },
+ {
+ lon: -76.1821115,
+ lat: -0.2078845,
+ attachments: [
+ {
+ id: '05d48eb4b7f6de9f3d801f4c875c8c74.jpg',
+ type: 'image/jpeg'
+ },
+ {
+ id: 'c511570a32c64881730db16f8a3e16d0.jpg',
+ type: 'image/jpeg'
+ },
+ {
+ id: '99e44ee66b4d8528753d45a3bd7f8f09.jpg',
+ type: 'image/jpeg'
+ }
+ ],
+ tags: {
+ categoryId: 'tala-ilegal',
+ notes:
+ 'Amarillo canelo talado de más o menos 30 metros y de unos 30 @ños de vida.... 😭 Para vender son tablones más o menos 200 \nTerrible '
+ },
+ metadata: {
+ location: {
+ error: false,
+ permission: 'granted',
+ provider: {
+ backgroundModeEnabled: true,
+ gpsAvailable: true,
+ passiveAvailable: true,
+ locationServicesEnabled: true,
+ networkAvailable: true
+ },
+ position: {
+ timestamp: 1562097313000,
+ mocked: false,
+ coords: {
+ altitude: 218.7,
+ heading: 269,
+ longitude: -76.1821115,
+ speed: 0.5677385926246643,
+ latitude: -0.2078845,
+ accuracy: 3
+ }
+ }
+ }
+ },
+ schemaVersion: 3,
+ type: 'observation',
+ timestamp: '2019-07-02T19:59:34.359Z',
+ created_at: '2019-07-02T19:56:48.373Z',
+ id: '00e45c12cb026d67',
+ links: [
+ '9dd59c141187453d6232c0600ea577b7e09dbb20a42a8c463057bb1f7c20ea54@86'
+ ],
+ version:
+ '9dd59c141187453d6232c0600ea577b7e09dbb20a42a8c463057bb1f7c20ea54@87'
+ },
+ {
+ lon: -76.0480426,
+ lat: 0.0957959,
+ attachments: [
+ {
+ id: 'cbbb8569d886a4a8e0f0a2c22512c53b.jpg',
+ type: 'image/jpeg'
+ }
+ ],
+ tags: {
+ categoryId: 'tala-ilegal',
+ notes: 'Guabillo tumbado directo sobre el lindero hace un mes'
+ },
+ metadata: {
+ location: {
+ error: false,
+ permission: 'granted',
+ provider: {
+ backgroundModeEnabled: true,
+ gpsAvailable: true,
+ passiveAvailable: true,
+ locationServicesEnabled: true,
+ networkAvailable: true
+ },
+ position: {
+ timestamp: 1563312915000,
+ mocked: false,
+ coords: {
+ altitude: 250.3,
+ heading: 264,
+ longitude: -76.0480426,
+ speed: 0,
+ latitude: 0.0957959,
+ accuracy: 3
+ }
+ }
+ }
+ },
+ schemaVersion: 3,
+ type: 'observation',
+ timestamp: '2019-07-16T21:36:11.035Z',
+ created_at: '2019-07-16T21:36:11.035Z',
+ id: '01227dae92589572',
+ links: [],
+ version:
+ '9dd59c141187453d6232c0600ea577b7e09dbb20a42a8c463057bb1f7c20ea54@109'
+ },
+ {
+ lon: -76.1001875,
+ lat: 0.0897293,
+ attachments: [
+ {
+ id: 'b937221a693ab0dd5369110c24e206ff.jpg',
+ type: 'image/jpeg'
+ },
+ {
+ id: '760b2e0006efb14e9e76308b6031e562.jpg',
+ type: 'image/jpeg'
+ }
+ ],
+ tags: {
+ categoryId: 'sendero-cazadores',
+ notes: 'Cruze del lindero de Tase'
+ },
+ metadata: {
+ location: {
+ error: false,
+ permission: 'granted',
+ provider: {
+ backgroundModeEnabled: true,
+ gpsAvailable: true,
+ passiveAvailable: true,
+ locationServicesEnabled: true,
+ networkAvailable: true
+ },
+ position: {
+ timestamp: 1563388482000,
+ mocked: false,
+ coords: {
+ altitude: 242.1,
+ heading: 8,
+ longitude: -76.1001875,
+ speed: 0.273832231760025,
+ latitude: 0.0897293,
+ accuracy: 3
+ }
+ }
+ }
+ },
+ schemaVersion: 3,
+ type: 'observation',
+ timestamp: '2019-07-17T18:35:18.640Z',
+ created_at: '2019-07-17T18:35:18.640Z',
+ id: '016de602f27dde8b',
+ links: [],
+ version:
+ '9dd59c141187453d6232c0600ea577b7e09dbb20a42a8c463057bb1f7c20ea54@136'
+ }
+]
+
+const getMediaUrl = id => `http://127.0.0.1:5000/media/preview/${id}`
+
+export const defaultStory = () => (
+
+)
diff --git a/src/renderer/components/MapFilter/MapFilter.js b/src/renderer/components/MapFilter/MapFilter.js
index a7147f84b..d0baa705a 100644
--- a/src/renderer/components/MapFilter/MapFilter.js
+++ b/src/renderer/components/MapFilter/MapFilter.js
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import { MapView, ReportView, MediaView } from 'react-mapfilter'
import debounce from 'lodash/debounce'
-import { defineMessages } from 'react-intl'
import logger from 'electron-timber'
import Toolbar from './Toolbar'
@@ -169,7 +168,8 @@ const MapFilter = () => {
)
}
- console.log(position.current)
+ if (observationsError) console.error(observationsError)
+ if (presetsError) console.error(presetsError)
return (
@@ -181,7 +181,14 @@ const MapFilter = () => {
fields={fields}
/>
-
+
{
)
}
-const MapFilterToolbar = ({ view, onChange }) => {
+const MapFilterToolbar = ({
+ view,
+ onChange,
+ observations,
+ filter,
+ presets,
+ getMediaUrl
+}) => {
const cx = useStyles()
const { formatMessage: t } = useIntl()
+ const [mapExportOpen, setMapExportOpen] = useState(false)
+
const handleChange = view => e => onChange(view)
return (
-
-
-
-
-
- Map
-
-
-
- Media
-
-
-
- Report
-
-
-
-
-
-
-
-
-
+ <>
+
+
+
+
+
+ Map
+
+
+
+ Media
+
+
+
+ Report
+
+
+
+ setMapExportOpen(true)}
+ >
+
+
+
+
+
+ setMapExportOpen(false)}
+ />
+ >
)
}
diff --git a/src/renderer/new-api.js b/src/renderer/new-api.js
index 1df6d1577..64048ba3c 100644
--- a/src/renderer/new-api.js
+++ b/src/renderer/new-api.js
@@ -164,7 +164,7 @@ export function Api ({ baseUrl }) {
},
// Return the url for a media attachment
- getMediaUrl: function getMediaUrl (attachmentId, size) {
+ getMediaUrl: function getMediaUrl (attachmentId, size = 'preview') {
return `${baseUrl}media/${size}/${attachmentId}`
},