@@ -6,6 +6,7 @@ import { discoveryKey } from 'hypercore-crypto'
6
6
import { TypedEmitter } from 'tiny-typed-emitter'
7
7
import ZipArchive from 'zip-stream-promise'
8
8
import * as b4a from 'b4a'
9
+ import mime from 'mime/lite'
9
10
// @ts -expect-error
10
11
import { Readable , pipelinePromise } from 'streamx'
11
12
@@ -69,6 +70,10 @@ import { createWriteStream } from 'fs'
69
70
/** @typedef {Omit<ProjectSettingsValue, 'schemaName'> } EditableProjectSettings */
70
71
/** @typedef {ProjectSettingsValue['configMetadata'] } ConfigMetadata */
71
72
/** @typedef {Map<string,Attachment> } SeenAttachments*/
73
+ /** @typedef {object } BlobRef
74
+ * @prop {string|undefined } mimeType
75
+ * @prop {BlobId } blobId
76
+ */
72
77
73
78
const CORESTORE_STORAGE_FOLDER_NAME = 'corestore'
74
79
const INDEXER_STORAGE_FOLDER_NAME = 'indexer'
@@ -83,8 +88,6 @@ export const kClearDataIfLeft = Symbol('clear data if left project')
83
88
export const kSetIsArchiveDevice = Symbol ( 'set isArchiveDevice' )
84
89
export const kIsArchiveDevice = Symbol ( 'isArchiveDevice (temp - test only)' )
85
90
export const kGeoJSONFileName = Symbol ( 'geoJSONFileName' )
86
- export const kExportGeoJSONStream = Symbol ( 'exportGeoJSONStream' )
87
- export const kExportZipStream = Symbol ( 'exportZipStream' )
88
91
89
92
const EMPTY_PROJECT_SETTINGS = Object . freeze ( { } )
90
93
@@ -786,40 +789,43 @@ export class MapeoProject extends TypedEmitter {
786
789
}
787
790
}
788
791
789
- let latitude = lat
790
- let longitude = lon
791
- let altitude = null
792
- const position = observation ?. metadata ?. position ?. coords
793
- if ( position ) {
794
- latitude = position . latitude
795
- longitude = position . longitude
796
- if ( position . altitude !== undefined ) {
797
- altitude = position . altitude
792
+ const metadataCoords = observation . metadata ?. position ?. coords
793
+ const altitude = metadataCoords ?. altitude
794
+
795
+ /** @type {[number, number] | [number, number, number] | null } */
796
+ let coordinates = null
797
+
798
+ // Prioritize using the observation's `lat` and `lon` fields
799
+ if ( typeof lat === 'number' && typeof lon === 'number' ) {
800
+ coordinates =
801
+ typeof altitude === 'number' ? [ lon , lat , altitude ] : [ lon , lat ]
802
+ } else {
803
+ // Fall back to using the observation metadata's position if possible
804
+ if (
805
+ typeof metadataCoords ?. latitude === 'number' &&
806
+ typeof metadataCoords ?. longitude === 'number'
807
+ ) {
808
+ coordinates =
809
+ typeof altitude === 'number'
810
+ ? [ metadataCoords . longitude , metadataCoords . latitude , altitude ]
811
+ : [ metadataCoords . longitude , metadataCoords . latitude ]
798
812
}
799
813
}
800
814
801
- const coordinates = [ longitude , latitude ]
802
- if ( typeof altitude === 'number' ) {
803
- coordinates . push ( altitude )
815
+ /** @type {import('geojson').Feature<import('geojson').Point | null> } */
816
+ const feature = {
817
+ type : 'Feature' ,
818
+ properties : observation ,
819
+ geometry : coordinates
820
+ ? {
821
+ type : 'Point' ,
822
+ coordinates,
823
+ }
824
+ : null ,
804
825
}
805
- const hasLatLon =
806
- typeof longitude === 'number' && typeof latitude === 'number'
807
- const geometry = hasLatLon
808
- ? {
809
- type : 'Point' ,
810
- coordinates,
811
- }
812
- : null
813
826
const comma = first ? '' : ','
814
827
first = false
815
- yield b4a . from (
816
- `${ comma } \n ` +
817
- JSON . stringify ( {
818
- type : 'Feature' ,
819
- properties : observation ,
820
- geometry,
821
- } )
822
- )
828
+ yield b4a . from ( `${ comma } \n ` + JSON . stringify ( feature ) )
823
829
}
824
830
}
825
831
@@ -941,7 +947,7 @@ export class MapeoProject extends TypedEmitter {
941
947
* @param {string } [options.lang]
942
948
* @returns {Readable<Buffer | Uint8Array> }
943
949
*/
944
- [ kExportGeoJSONStream ] ( {
950
+ #exportGeoJSONStream ( {
945
951
observations = true ,
946
952
tracks = true ,
947
953
lang,
@@ -1027,7 +1033,7 @@ export class MapeoProject extends TypedEmitter {
1027
1033
) {
1028
1034
const fileName = await this [ kGeoJSONFileName ] ( observations , tracks )
1029
1035
const filePath = path . join ( exportFolder , fileName )
1030
- const source = this [ kExportGeoJSONStream ] ( { observations, tracks, lang } )
1036
+ const source = this . #exportGeoJSONStream ( { observations, tracks, lang } )
1031
1037
const sink = createWriteStream ( filePath )
1032
1038
await pipelinePromise ( source , sink )
1033
1039
@@ -1036,15 +1042,37 @@ export class MapeoProject extends TypedEmitter {
1036
1042
1037
1043
/**
1038
1044
* @param {Attachment } attachment
1039
- * @returns {Promise<null | BlobId > }
1045
+ * @returns {Promise<null | BlobRef > }
1040
1046
*/
1041
- async #tryGetBlobId ( attachment ) {
1047
+ async #tryGetAttachmentBlob ( attachment ) {
1042
1048
// Audio must not have variants
1043
1049
for ( const variant of VARIANT_EXPORT_ORDER ) {
1044
- const blobId = buildBlobId ( attachment , variant )
1045
- const entry = await this . #blobStore. entry ( blobId )
1046
- if ( ! entry ) continue
1047
- return blobId
1050
+ try {
1051
+ const blobId = buildBlobId ( attachment , variant )
1052
+ const entry = await this . #blobStore. entry ( blobId )
1053
+ if ( ! entry ) continue
1054
+ const metadata = entry . value . metadata
1055
+ if ( ! metadata || typeof metadata !== 'object' ) continue
1056
+ let mimeType = undefined
1057
+ if ( 'mimeType' in metadata ) {
1058
+ if ( typeof metadata . mimeType === 'string' ) {
1059
+ mimeType = metadata . mimeType
1060
+ } else {
1061
+ this . #l. log ( 'Invalid type for mimeType in blob' , blobId , entry )
1062
+ continue
1063
+ }
1064
+ }
1065
+ return { blobId, mimeType }
1066
+ } catch ( e ) {
1067
+ if ( ! ( e instanceof Error ) ) throw e
1068
+ this . #l. log (
1069
+ 'Error loading blob id for attachment' ,
1070
+ attachment ,
1071
+ variant ,
1072
+ e . message
1073
+ )
1074
+ continue
1075
+ }
1048
1076
}
1049
1077
1050
1078
return null
@@ -1066,7 +1094,7 @@ export class MapeoProject extends TypedEmitter {
1066
1094
// GeoJSON
1067
1095
const geoJSONFileName = await this [ kGeoJSONFileName ] ( observations , tracks )
1068
1096
const seenAttachments = new Map ( )
1069
- const geoJSONStream = this [ kExportGeoJSONStream ] ( {
1097
+ const geoJSONStream = this . #exportGeoJSONStream ( {
1070
1098
observations,
1071
1099
tracks,
1072
1100
lang,
@@ -1079,16 +1107,31 @@ export class MapeoProject extends TypedEmitter {
1079
1107
const missingAttachments = [ ]
1080
1108
// Attachments
1081
1109
if ( attachments ) {
1082
- const mediaFolder = this . #exportPrefix( 'Media' ) + '/'
1110
+ const mediaFolder = ( await this . #exportPrefix( 'Media' ) ) + '/'
1083
1111
for ( const attachment of seenAttachments . values ( ) ) {
1084
- const blobId = await this . #tryGetBlobId ( attachment )
1085
- if ( blobId === null ) {
1112
+ const ref = await this . #tryGetAttachmentBlob ( attachment )
1113
+ if ( ref === null ) {
1086
1114
missingAttachments . push ( attachment )
1087
1115
continue
1088
1116
}
1089
1117
1118
+ const { blobId, mimeType } = ref
1119
+ let extensionString = ''
1120
+ if ( mimeType ) {
1121
+ const extension = mime . getExtension ( mimeType )
1122
+ if ( extension ) {
1123
+ extensionString = '.' + extension
1124
+ } else {
1125
+ this . #l. log ( 'Got unknown mime type in attachment blob' , attachment )
1126
+ }
1127
+ }
1128
+
1090
1129
const stream = this . #blobStore. createReadStream ( blobId )
1091
- const name = mediaFolder + blobId . variant + '/' + attachment . name
1130
+ const name = path . posix . join (
1131
+ mediaFolder ,
1132
+ blobId . variant ,
1133
+ `${ attachment . name } ${ extensionString } `
1134
+ )
1092
1135
1093
1136
// @ts -expect-error
1094
1137
await archive . entry ( stream , { name } )
@@ -1118,7 +1161,7 @@ export class MapeoProject extends TypedEmitter {
1118
1161
* @param {string } [options.lang]
1119
1162
* @returns {Readable<Buffer | Uint8Array> }
1120
1163
*/
1121
- [ kExportZipStream ] ( {
1164
+ #exportZipStream ( {
1122
1165
observations = true ,
1123
1166
tracks = true ,
1124
1167
attachments = true ,
@@ -1153,7 +1196,7 @@ export class MapeoProject extends TypedEmitter {
1153
1196
) {
1154
1197
const fileName = await this . #zipFileName( observations , tracks )
1155
1198
const filePath = path . join ( exportFolder , fileName )
1156
- const source = this [ kExportZipStream ] ( {
1199
+ const source = this . #exportZipStream ( {
1157
1200
observations,
1158
1201
tracks,
1159
1202
attachments,
0 commit comments