注意!!!: 仓库代码node版本使用的是 16,vue-cli4.5不支持使用 node18,vue-cli5使用的是webpack5,polyfill需要自行引用

请注意jszipmapshaperol 的版本,不同版本库代码可能有变。


shapefile(.shp,.dbf,.prj)导入导出

网页应用(Vue) 导入导出 shapefile(.shp,.dbf,.prj)

说明

如果网页应用只需要导入功能,只需使用 shapefile 库即可,但是 shapefile 库是无法实现导出功能的。
如果同时需导入导出功能,则需要使用 mapshaper 库。

shapefile-import

shapefile-export


1. 获取示例源码

https://gitee.com/liushuai1125/shapefile-import-export

  1. 获取源码
  • 使用 shapefile 导入
    git clone https://gitee.com/liushuai1125/shapefile-import-export.git
    
  • 使用 mapshaper 导入导出
    git clone -b mapshaper https://gitee.com/liushuai1125/shapefile-import-export.git
    
  1. 安装依赖

    npm install # cnpm install
    
  2. 启动项目

    npm run serve
    

预览地址: http://localhost:9566/


2. 使用 Shapefile 导入

流式 Shapefile 解析器

2.1. 文档

2.2. 说明

在网页应用中,我们使用文档里面的解析示例解析时,发现是无法读取选上传的文件的。观察 shapefile 的源码可知

export function openShp(source, options) {
  if (typeof source === "string") {
    if (!/\.shp$/.test(source)) source += ".shp";
    source = path(source, options);
  } else if (source instanceof ArrayBuffer || source instanceof Uint8Array) {
    source = array(source);
  } else {
    source = stream(source);
  }
  return Promise.resolve(source).then(shp);
}

shpdbf 的参数类型可以是 ArrayBuffer、Uint8Array等类型,这样我们可以使用网页里的 FileReaderreadAsArrayBuffer 方法获取选择上传的文件数据,再传给 shapefile 进行解析。

2.3. 代码示例

<template>
  <div class="shapefile-parser">
    <div class="panel">
      <input ref="file" type="file" multiple accept=".shp,.dbf">
      <button @click="handleSelect">确认</button>
    </div>
  </div>
</template>

<script>
// eslint-disable-next-line no-unused-vars
import { open as shapeOpen, read as shapeRead } from 'shapefile'

export default {
  name: 'ShapefileParser',
  data() {
    return {
    }
  },
  methods: {
    handleSelect() {
      let files = this.$refs.file.files
      console.log(files, typeof files, 'files FileList')
      // files = Array.from(new Array(files.length), (i, idx) => files[idx]) // 等效下面写法
      files = Array.from(files) // FileList => Array, 方便使用 Array 方法
      console.log(files, typeof files, 'files Array')
      this.parseShapefile(files) // 解析选择的 shp 并绘制显示
    },
    parseShapefile(files) {
      const shpFile = files.find(f => f.name.endsWith('.shp'))
      const dbfFile = files.find(f => f.name.endsWith('.dbf'))

      // 可使用 Promise.all 优化为下面写法
      // this.readInputFile(shpFile, 'ArrayBuffer').then(async(fileData) => {
      //   const featureJsons = []
      //
      //   await shapeOpen(fileData).then(async(source) => {
      //     await source.read().then(function log(result) {
      //       if (result.done) {
      //         return
      //       }
      //       featureJsons.push(result.value)
      //       console.log(result.value)
      //       return source.read().then(log)
      //     })
      //   }).catch(error => console.error(error.stack))
      //
      //   console.log(featureJsons, typeof featureJsons, 'featureJsons')
      // }).catch(e => {
      //   console.log(e, typeof e, 'l e')
      // })

      const promises = [shpFile, dbfFile].map(i => this.readInputFile(i))

      Promise.all(promises).then(([shp, dbf]) => {
        // return shapeOpen(shp, dbf)

        //  `shapefile` 提供了 read 方法直接返回一个 FeatureCollection geojson,封装了类似于下面注释掉的代码
        // 指定 dbf 编码 'utf-8'
        return shapeRead(shp, dbf, { encoding:'utf-8' }) 
      }).then(async(source) => {
        // const featureJsons = []
        // await source.read().then(function log(result) {
        //   if (result.done) {
        //     return
        //   }
        //   featureJsons.push(result.value)
        //   console.log(result.value)
        //   return source.read().then(log)
        // })
        // console.log(featureJsons, typeof featureJsons, 'shapeOpen featureJsons')

        console.log(source, typeof source, 'shapeRead source')
        // do something
      }).catch(error => console.error(error.stack))
    },
    async readInputFile(file, type = 'ArrayBuffer') {
      // 读取文件
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        switch (type) {
          case 'ArrayBuffer':
            reader.readAsArrayBuffer(file)
            break
          case 'Text':
            reader.readAsText(file)
            break
          case 'BinaryString':
            reader.readAsBinaryString(file)
            break
          case 'DataURL':
            reader.readAsDataURL(file)
            break
        }

        reader.onload = function() {
          // this.result 就是读取到的文件的数据
          resolve(this.result)
        }

        reader.onerror = function() {
          reject(this)
        }
      })
    }
  }
}
</script>

<style scoped lang="scss">
.shapefile-parser {
  width: 1000px;
  height: 700px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;

  .panel {
    padding: 10px 0;
  }
}
</style>


3. 使用 mapshaper 导入导出

作为一个 WebGISer ,你如果不知道 mapshaper,那你一定是一个不合格的 WebGISer

mapshaper官网传送门

3.1. 文档

3.2. 说明

阅读 mapshaper.js 主要源码部分添加了以下一些 书签
bookmarks.png

有兴趣者可自行查看。

简而言之,在 mapshaper 里导入的数据被抽象为一个 datasetdataset里存放着解析的数据,后续的操作都是在此基础上。
GeoJSON 文件生成的dataset 的格式如下图所示。
dataset.png

获取 dataset 需要用到 importContent 方法,该方法可以导入 .json.shp 类型的文件。
由源码上的说明可知,参数 obj 是一个对象,具有 filenamecontent 属性,content 属性的类型可以是 BufferArrayBufferStringObject

  // Parse content of one or more input files and return a dataset
  // @obj: file data, indexed by file type
  // File data objects have two properties:
  //    content: Buffer, ArrayBuffer, String or Object
  //    filename: String or null
  //
  function importContent(obj, opts) {
   // ...
  }

3.3. 导入代码示例

主要代码如下:

<script>
import mapshaper from 'mapshaper'
import { GeoJSON } from 'ol/format'
import GEOJSON from '@/assets/411600.json'

const {
  importContent,
  guessInputType,
  exportDatasetAsGeoJSON
} = mapshaper.internal

export default {
  created() {
    // 直接 解析 geojson 字符串
    const content = this.analyze({ json: { filename: '411600.json', content: JSON.stringify(GEOJSON) }})
    console.log(content, typeof content, 'content')
  },
  methods: {
    handleSelect() {
      let files = this.$refs.file.files
      console.log(files, typeof files, 'files FileList')
      // files = Array.from(new Array(files.length), (i, idx) => files[idx]) // 等效下面写法
      files = Array.from(files) // FileList => Array, 方便使用 Array 方法
      console.log(files, typeof files, 'files Array')

      // 解析 shp
      const promises = files.map(i => this.readInputFileByType(i))
      Promise.all(promises).then((res) => {
        console.log(res, typeof res, 'readInputFileByType res')

        // 将选取的多个文件作为一个数据集
        return Object.fromEntries(res.map((i, idx) => {
          const filename = files[idx].name
          // 推断文件的解析类型
          return [guessInputType(filename, i), { filename: filename, content: i }]
        }))
      }).then((res) => {
        console.log(res, typeof res, 'merge res')

        const content = this.analyze(res)

        const readFeatures = new GeoJSON().readFeatures(content)
        console.log(readFeatures, typeof readFeatures, 'readFeatures')
      }).catch(error => console.error(error.stack))
    },
    analyze(obj) {
      // @obj: file data, indexed by file type
      // File data objects have two properties:
      //    content: Buffer, ArrayBuffer, String or Object
      //    filename: String or null

      // obj: {json:{filename:'',content:''}}
      const dataset = importContent(obj)
      console.log(dataset, typeof dataset, 'dataset')

      const content = exportDatasetAsGeoJSON(dataset, {})
      console.log(content, typeof content, 'exportDatasetAsGeoJSON content')
      return content
    },
    readInputFileByType(file) {
      // 以 以下 后缀名结尾的文件 读取成文本内容
      const asTest = ['.prj', '.cpg', '.json'].map(i => file.name.endsWith(i)).includes(true)
      return this.readInputFile(file, asTest ? 'Text' : 'ArrayBuffer')
    },
    async readInputFile(file, type = 'ArrayBuffer') {
      // 读取文件
      return new Promise((resolve, reject) => {
        const reader = new FileReader()
        switch (type) {
          case 'ArrayBuffer':
            reader.readAsArrayBuffer(file)
            break
          case 'Text':
            reader.readAsText(file)
            break
          case 'BinaryString':
            reader.readAsBinaryString(file)
            break
          case 'DataURL':
            reader.readAsDataURL(file)
            break
        }

        reader.onload = function() {
          // this.result 就是读取到的文件的数据
          resolve(this.result)
        }

        reader.onerror = function() {
          reject(this)
        }
      })
    }
  }
}
</script>

3.4. 导出 shapefile 代码示例

主要代码如下:

<script>
import mapshaper from 'mapshaper'
import GEOJSON from '@/assets/411600.json'
import JSZip from 'jszip'
import { saveAs } from 'file-saver'

const {
  importContent,
  setDatasetCRS,
  exportFileContent
} = mapshaper.internal

export default {
  methods: {
    handleExport() {
      const dataset = importContent({ json: { filename: '411600.json', content: JSON.stringify(GEOJSON) }})

      // https://epsg.io/4326.esriwkt
      setDatasetCRS(dataset, { prj: 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]]' })
      console.log(dataset, typeof dataset, 'dataset')

      this.saveDataset(dataset)
    },
    saveDataset(dataset, type = 'shapefile') {
      // format=shapefile|geojson|topojson|json|dbf|csv|tsv|svg
      const fileContent = exportFileContent(dataset, { format: type })
      console.log(fileContent, typeof fileContent, 'fileContent')

      this.saveFileContent(fileContent)
    },
    saveFileContent(fileContent) {
      var zip = new JSZip()

      fileContent.forEach(i => {
        zip.file(i.filename, i.content)
      })

      zip.generateAsync({ type: 'blob' }, function updateCallback(metadata) {
        var msg = 'progression : ' + metadata.percent.toFixed(2) + ' %'
        if (metadata.currentFile) {
          msg += ', current file = ' + metadata.currentFile
        }
        console.log(msg)
      }).then((blob) => {
        // see FileSaver.js
        saveAs(blob, 'example.zip')
        console.log('done !')
      }).catch(error => console.error(error.stack))
    }
  }
}
</script>
Logo

前往低代码交流专区

更多推荐