map_view_vue2 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4545,7 +4545,7 @@ es_iterator_constructor_namespaceFn();
4545
4545
  ;// ./node_modules/core-js/modules/es.iterator.map.js
4546
4546
  es_iterator_map_namespaceFn();
4547
4547
 
4548
- ;// ./node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib/index.js??clonedRuleSet-82.use[1]!./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[3]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/package/map_view/mapView.vue?vue&type=template&id=007e62b0
4548
+ ;// ./node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib/index.js??clonedRuleSet-82.use[1]!./node_modules/@vue/vue-loader-v15/lib/loaders/templateLoader.js??ruleSet[1].rules[3]!./node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./src/package/map_view/mapView.vue?vue&type=template&id=4496a68c
4549
4549
 
4550
4550
 
4551
4551
  var render = function render() {
@@ -5014,6 +5014,18 @@ var render = function render() {
5014
5014
  "id": "map"
5015
5015
  }
5016
5016
  }), _c('div', {
5017
+ directives: [{
5018
+ name: "show",
5019
+ rawName: "v-show",
5020
+ value: _vm.mapLoading,
5021
+ expression: "mapLoading"
5022
+ }],
5023
+ staticClass: "map-loading-mask"
5024
+ }, [_c('span', {
5025
+ staticClass: "map-loading-spinner"
5026
+ }), _c('span', {
5027
+ staticClass: "map-loading-text"
5028
+ }, [_vm._v("地图加载中...")])]), _c('div', {
5017
5029
  staticClass: "dialog-overlay",
5018
5030
  class: {
5019
5031
  show: _vm.loadDialogVisible
@@ -5593,7 +5605,7 @@ var staticRenderFns = [function () {
5593
5605
  }, [_vm._v("📥")]), _c('div', [_c('h1', [_vm._v("加载地图")]), _c('p', [_vm._v("选择数据源并加载")])])]);
5594
5606
  }];
5595
5607
 
5596
- ;// ./src/package/map_view/mapView.vue?vue&type=template&id=007e62b0
5608
+ ;// ./src/package/map_view/mapView.vue?vue&type=template&id=4496a68c
5597
5609
 
5598
5610
  ;// ./node_modules/core-js/modules/es.array.includes.js
5599
5611
  es_array_includes_namespaceFn();
@@ -64666,10 +64678,6 @@ register(lib);
64666
64678
  default: false
64667
64679
  },
64668
64680
  // 是否显示「加载地图」按钮与对话框。
64669
- showLoader: {
64670
- type: Boolean,
64671
- default: true
64672
- },
64673
64681
  // 固定地图查看器宽度:数字按 px,字符串原样使用(如 '600px' / '100%');不传保持铺满父容器。
64674
64682
  width: {
64675
64683
  type: [String, Number],
@@ -64698,6 +64706,10 @@ register(lib);
64698
64706
  // 地图查看器是否已完成初始化。
64699
64707
  sidebarWidth: 342,
64700
64708
  // 侧栏当前宽度(px),可通过右缘拖拽手柄调整。
64709
+ loadingTasks: 0,
64710
+ // 进行中的数据请求任务数(清单/文件拉取、解析等)。
64711
+ olLoading: false,
64712
+ // OpenLayers 图层渲染中(loadstart ~ loadend 之间)。
64701
64713
  dropVisible: false,
64702
64714
  // 是否显示全屏拖放遮罩层。
64703
64715
  sourceCategory: 'raster',
@@ -64829,6 +64841,10 @@ register(lib);
64829
64841
  // 地图画布背景色:传入时覆盖主题默认值(#map 的 background 引用该变量)。
64830
64842
  if (this.mapBackground) style['--map-bg'] = this.mapBackground;
64831
64843
  return style;
64844
+ },
64845
+ // 是否显示「地图加载中」遮罩:有数据请求任务或图层正在渲染时为 true。
64846
+ mapLoading() {
64847
+ return this.loadingTasks > 0 || this.olLoading;
64832
64848
  }
64833
64849
  },
64834
64850
  // 生命周期:挂载后初始化地图查看器、注册拖放加载与 URL 参数自动加载。
@@ -64961,6 +64977,13 @@ register(lib);
64961
64977
  this.updateStatus();
64962
64978
  this.$emit('view-changed', this.getViewState());
64963
64979
  });
64980
+ // 图层渲染开始 / 结束:瓦片、图片等加载期间显示遮罩。
64981
+ this.map.on('loadstart', () => {
64982
+ this.olLoading = true;
64983
+ });
64984
+ this.map.on('loadend', () => {
64985
+ this.olLoading = false;
64986
+ });
64964
64987
 
64965
64988
  /* ============================================================
64966
64989
  * 状态栏 / 提示
@@ -65122,6 +65145,13 @@ register(lib);
65122
65145
  this.updateUrlTemplateHint();
65123
65146
  this.viewerReady = true;
65124
65147
  },
65148
+ // 数据请求任务计数 +1 / -1:遮罩在任务数归零后隐藏。
65149
+ pushLoading() {
65150
+ this.loadingTasks += 1;
65151
+ },
65152
+ popLoading() {
65153
+ this.loadingTasks = Math.max(0, this.loadingTasks - 1);
65154
+ },
65125
65155
  // 按住侧栏右缘拖动:mousemove 实时更新宽度,限制在 280 ~ 620px 之间。
65126
65156
  startSidebarResize(e) {
65127
65157
  e.preventDefault();
@@ -65967,15 +65997,11 @@ register(lib);
65967
65997
  if (r1 || r2) throw new Error('不支持带旋转的世界文件');
65968
65998
  return [cx - dx / 2, cy + (h - 0.5) * dy, cx + (w - 0.5) * dx, cy - dy / 2];
65969
65999
  },
65970
- // 根据范围推断并提示 UTM/GK 分区对应的 EPSG。
65971
- updateZoneHint(extent) {
65972
- this.form.zoneHint = {
65973
- message: '',
65974
- options: []
65975
- };
65976
- if (!extent) return;
66000
+ // 根据范围数值模式猜测中国高斯投影带号 EPSG(3度带/6度带):命中返回候选码,未命中返回 null。
66001
+ guessCrsFromExtent(extent) {
66002
+ if (!extent) return null;
65977
66003
  const x = extent[2];
65978
- if (x <= 1e6 || x >= 7e7) return;
66004
+ if (x <= 1e6 || x >= 7e7) return null;
65979
66005
  const zone = Math.round(x / 1e6);
65980
66006
  const options = [];
65981
66007
  if (zone >= 25 && zone <= 45) {
@@ -65990,13 +66016,22 @@ register(lib);
65990
66016
  label: `6度带 ${zone},中央经线 ${zone * 6 - 3}°E(EPSG:${4491 + zone - 13},含带号)`
65991
66017
  });
65992
66018
  }
65993
- if (options.length) {
65994
- this.form.dataCrs = options[0].code;
65995
- this.form.zoneHint = {
65996
- message: '检测到高斯带号坐标,可能是:',
65997
- options
65998
- };
65999
- }
66019
+ if (!options.length) return null;
66020
+ this.form.zoneHint = {
66021
+ message: '检测到高斯带号坐标,可能是:',
66022
+ options
66023
+ };
66024
+ return options[0].code;
66025
+ },
66026
+ // 根据范围推断并提示 UTM/GK 分区对应的 EPSG。
66027
+ updateZoneHint(extent) {
66028
+ this.form.zoneHint = {
66029
+ message: '',
66030
+ options: []
66031
+ };
66032
+ if (!extent) return;
66033
+ const code = this.guessCrsFromExtent(extent);
66034
+ if (code) this.form.dataCrs = code;
66000
66035
  },
66001
66036
  // 将已选世界文件应用到范围输入框。
66002
66037
  applyWorldFileToExtent() {
@@ -66070,95 +66105,109 @@ register(lib);
66070
66105
  },
66071
66106
  // 加载 PNG/JPG 图片地图(配世界文件或手动填写范围)。
66072
66107
  async loadImageLayer() {
66073
- if (!this.pickedImage) {
66074
- this.toast('请先选择图片文件', 'warn');
66075
- return;
66076
- }
66077
- const proj = this.ensureProjection(this.form.dataCrs);
66078
- if (!proj) {
66079
- this.toast('请先选择或注册有效的坐标系', 'warn');
66080
- return;
66081
- }
66082
- let {
66083
- width,
66084
- height
66085
- } = this.pickedImage;
66086
- if (!width || !height) {
66087
- try {
66088
- const d = await this.getImageSize(this.pickedImage.objectUrl);
66089
- width = this.pickedImage.width = d.width;
66090
- height = this.pickedImage.height = d.height;
66091
- } catch {
66092
- this.toast('无法读取图片尺寸', 'err');
66108
+ this.pushLoading();
66109
+ try {
66110
+ if (!this.pickedImage) {
66111
+ this.toast('请先选择图片文件', 'warn');
66093
66112
  return;
66094
66113
  }
66095
- }
66096
- const er = this.readExtentInputs();
66097
- if (!er.ok) return;
66098
- const extent = er.extent;
66099
- const layerUrl = URL.createObjectURL(this.pickedImage.file);
66100
- const layer = new layer_Image({
66101
- source: new ImageStatic({
66102
- url: layerUrl,
66103
- imageExtent: extent,
66104
- projection: proj
66105
- })
66106
- });
66107
- this.addMapEntry({
66108
- name: this.pickedImage.name,
66109
- kind: 'image',
66110
- layer,
66111
- extent,
66112
- projection: proj,
66113
- cleanup() {
66114
- URL.revokeObjectURL(layerUrl);
66114
+ const proj = this.ensureProjection(this.form.dataCrs);
66115
+ if (!proj) {
66116
+ this.toast('请先选择或注册有效的坐标系', 'warn');
66117
+ return;
66115
66118
  }
66116
- });
66119
+ let {
66120
+ width,
66121
+ height
66122
+ } = this.pickedImage;
66123
+ if (!width || !height) {
66124
+ try {
66125
+ const d = await this.getImageSize(this.pickedImage.objectUrl);
66126
+ width = this.pickedImage.width = d.width;
66127
+ height = this.pickedImage.height = d.height;
66128
+ } catch {
66129
+ this.toast('无法读取图片尺寸', 'err');
66130
+ return;
66131
+ }
66132
+ }
66133
+ const er = this.readExtentInputs();
66134
+ if (!er.ok) return;
66135
+ const extent = er.extent;
66136
+ const layerUrl = URL.createObjectURL(this.pickedImage.file);
66137
+ const layer = new layer_Image({
66138
+ source: new ImageStatic({
66139
+ url: layerUrl,
66140
+ imageExtent: extent,
66141
+ projection: proj
66142
+ })
66143
+ });
66144
+ this.addMapEntry({
66145
+ name: this.pickedImage.name,
66146
+ kind: 'image',
66147
+ layer,
66148
+ extent,
66149
+ projection: proj,
66150
+ cleanup() {
66151
+ URL.revokeObjectURL(layerUrl);
66152
+ }
66153
+ });
66154
+ } finally {
66155
+ this.popLoading();
66156
+ }
66117
66157
  },
66118
66158
  // 从 URL(含可选世界文件或范围参数)创建并加载图片地图。
66119
66159
  async addImageFromUrl({
66120
66160
  url,
66121
66161
  name,
66122
66162
  worldText,
66123
- extent
66163
+ extent,
66164
+ crs = null
66124
66165
  }) {
66125
- let meta;
66166
+ this.pushLoading();
66126
66167
  try {
66127
- meta = await this.getImageSize(url);
66128
- } catch {
66129
- this.toast(`无法加载图片:${url}`, 'err');
66130
- return;
66131
- }
66132
- if (worldText) {
66168
+ let meta;
66133
66169
  try {
66134
- const wf = this.parseWorldFile(worldText);
66135
- extent = this.extentFromWorldFile(meta.width, meta.height, wf);
66136
- } catch (err) {
66137
- this.toast(`世界文件解析失败:${err.message}`, 'err');
66170
+ meta = await this.getImageSize(url);
66171
+ } catch {
66172
+ this.toast(`无法加载图片:${url}`, 'err');
66173
+ return;
66138
66174
  }
66175
+ if (worldText) {
66176
+ try {
66177
+ const wf = this.parseWorldFile(worldText);
66178
+ extent = this.extentFromWorldFile(meta.width, meta.height, wf);
66179
+ } catch (err) {
66180
+ this.toast(`世界文件解析失败:${err.message}`, 'err');
66181
+ }
66182
+ }
66183
+ extent = extent || [0, -meta.height, meta.width, 0];
66184
+ // 坐标系决策:显式传入的 crs 优先;未传入时按范围数值模式猜测高斯带号,未命中默认 EPSG:3857。
66185
+ this.updateZoneHint(extent);
66186
+ const crsCode = crs || this.guessCrsFromExtent(extent) || 'EPSG:3857';
66187
+ this.form.dataCrs = crsCode;
66188
+ const proj = this.ensureProjection(crsCode) || this.PIXEL;
66189
+ const layer = new layer_Image({
66190
+ source: new ImageStatic({
66191
+ url,
66192
+ imageExtent: extent,
66193
+ projection: proj
66194
+ })
66195
+ });
66196
+ this.$refs.extMinx.value = extent[0];
66197
+ this.$refs.extMiny.value = extent[1];
66198
+ this.$refs.extMaxx.value = extent[2];
66199
+ this.$refs.extMaxy.value = extent[3];
66200
+ this.addMapEntry({
66201
+ name,
66202
+ kind: 'image',
66203
+ layer,
66204
+ extent,
66205
+ projection: proj,
66206
+ cleanup() {}
66207
+ });
66208
+ } finally {
66209
+ this.popLoading();
66139
66210
  }
66140
- extent = extent || [0, -meta.height, meta.width, 0];
66141
- const proj = this.ensureProjection(this.form.dataCrs) || this.PIXEL;
66142
- const layer = new layer_Image({
66143
- source: new ImageStatic({
66144
- url,
66145
- imageExtent: extent,
66146
- projection: proj
66147
- })
66148
- });
66149
- this.$refs.extMinx.value = extent[0];
66150
- this.$refs.extMiny.value = extent[1];
66151
- this.$refs.extMaxx.value = extent[2];
66152
- this.$refs.extMaxy.value = extent[3];
66153
- this.updateZoneHint(extent);
66154
- this.addMapEntry({
66155
- name,
66156
- kind: 'image',
66157
- layer,
66158
- extent,
66159
- projection: proj,
66160
- cleanup() {}
66161
- });
66162
66211
  },
66163
66212
  // 处理用户选择的本地切片目录。
66164
66213
  onTilesDirChosen(fileList) {
@@ -66186,57 +66235,62 @@ register(lib);
66186
66235
  },
66187
66236
  // 加载本地 XYZ 切片目录为瓦片图层。
66188
66237
  loadTilesLayer() {
66189
- if (!this.pickedTiles || !this.pickedTiles.files.length) {
66190
- this.toast('请先选择本地切片XYZ目录', 'warn');
66191
- return;
66192
- }
66193
- const proj = this.ensureProjection(this.form.dataCrs);
66194
- if (!proj) {
66195
- this.toast('请先选择或注册有效的坐标系', 'warn');
66196
- return;
66197
- }
66198
- const tmap = this.scanTileFiles(this.pickedTiles.files, this.form.tilesTms);
66199
- if (!tmap.size) {
66200
- this.toast('所选目录中未找到 z/x/y 结构的切片文件', 'err');
66201
- return;
66202
- }
66203
- const stats = this.tileStats(tmap);
66204
- // 先按全球网格计算覆盖范围,再据此构建受限网格:
66205
- // 1) 层级钳制在 [minZ, maxZ],过度缩放时使用最近可用层级(拉伸显示而非空白)
66206
- // 2) 网格范围限制在实际覆盖区(配合 wrapX:false),缩小时只枚举数据范围内的瓦片,避免卡顿
66207
- const worldGrid = this.buildTileGrid(proj, stats.maxZ, stats.minZ);
66208
- const covered = this.tilesExtent(worldGrid, stats);
66209
- const grid = this.buildTileGrid(proj, stats.maxZ, stats.minZ, covered);
66210
- const urlCache = new Map();
66211
- const source = new source_XYZ({
66212
- projection: proj,
66213
- tileGrid: grid,
66214
- wrapX: false,
66215
- minZoom: stats.minZ,
66216
- maxZoom: stats.maxZ,
66217
- tileUrlFunction: ([z, x, y]) => {
66218
- const f = tmap.get(`${z}/${x}/${y}`);
66219
- if (!f) return undefined;
66220
- let u = urlCache.get(f);
66221
- if (!u) {
66222
- u = URL.createObjectURL(f);
66223
- urlCache.set(f, u);
66224
- }
66225
- return u;
66238
+ this.pushLoading();
66239
+ try {
66240
+ if (!this.pickedTiles || !this.pickedTiles.files.length) {
66241
+ this.toast('请先选择本地切片XYZ目录', 'warn');
66242
+ return;
66226
66243
  }
66227
- });
66228
- this.addMapEntry({
66229
- name: this.pickedTiles.name,
66230
- kind: 'tiles',
66231
- layer: new layer_Tile({
66232
- source
66233
- }),
66234
- extent: covered,
66235
- projection: proj,
66236
- cleanup() {
66237
- urlCache.forEach(u => URL.revokeObjectURL(u));
66244
+ const proj = this.ensureProjection(this.form.dataCrs);
66245
+ if (!proj) {
66246
+ this.toast('请先选择或注册有效的坐标系', 'warn');
66247
+ return;
66238
66248
  }
66239
- });
66249
+ const tmap = this.scanTileFiles(this.pickedTiles.files, this.form.tilesTms);
66250
+ if (!tmap.size) {
66251
+ this.toast('所选目录中未找到 z/x/y 结构的切片文件', 'err');
66252
+ return;
66253
+ }
66254
+ const stats = this.tileStats(tmap);
66255
+ // 先按全球网格计算覆盖范围,再据此构建受限网格:
66256
+ // 1) 层级钳制在 [minZ, maxZ],过度缩放时使用最近可用层级(拉伸显示而非空白)
66257
+ // 2) 网格范围限制在实际覆盖区(配合 wrapX:false),缩小时只枚举数据范围内的瓦片,避免卡顿
66258
+ const worldGrid = this.buildTileGrid(proj, stats.maxZ, stats.minZ);
66259
+ const covered = this.tilesExtent(worldGrid, stats);
66260
+ const grid = this.buildTileGrid(proj, stats.maxZ, stats.minZ, covered);
66261
+ const urlCache = new Map();
66262
+ const source = new source_XYZ({
66263
+ projection: proj,
66264
+ tileGrid: grid,
66265
+ wrapX: false,
66266
+ minZoom: stats.minZ,
66267
+ maxZoom: stats.maxZ,
66268
+ tileUrlFunction: ([z, x, y]) => {
66269
+ const f = tmap.get(`${z}/${x}/${y}`);
66270
+ if (!f) return undefined;
66271
+ let u = urlCache.get(f);
66272
+ if (!u) {
66273
+ u = URL.createObjectURL(f);
66274
+ urlCache.set(f, u);
66275
+ }
66276
+ return u;
66277
+ }
66278
+ });
66279
+ this.addMapEntry({
66280
+ name: this.pickedTiles.name,
66281
+ kind: 'tiles',
66282
+ layer: new layer_Tile({
66283
+ source
66284
+ }),
66285
+ extent: covered,
66286
+ projection: proj,
66287
+ cleanup() {
66288
+ urlCache.forEach(u => URL.revokeObjectURL(u));
66289
+ }
66290
+ });
66291
+ } finally {
66292
+ this.popLoading();
66293
+ }
66240
66294
  },
66241
66295
  // 更新在线 XYZ URL 模板提示。
66242
66296
  updateUrlTemplateHint() {
@@ -66321,41 +66375,46 @@ register(lib);
66321
66375
  },
66322
66376
  // 根据表单中的在线 XYZ 参数加载图层。
66323
66377
  async loadUrlLayer() {
66324
- const template = String(this.form.urlTemplate || '').trim();
66325
- if (!template) {
66326
- this.toast('请填写在线切片XYZ URL 模板', 'warn');
66327
- return;
66328
- }
66329
- if (String(this.form.urlMinzoom || '').trim() === '') {
66330
- this.toast('请填写最小层级', 'warn');
66331
- return;
66332
- }
66333
- if (String(this.form.urlMaxzoom || '').trim() === '') {
66334
- this.toast('请填写最大层级', 'warn');
66335
- return;
66336
- }
66337
- // 显示范围必填:坐标系可与切片坐标系不同,加载时自动换算
66338
- const extVals = [this.$refs.urlExtMinx.value, this.$refs.urlExtMiny.value, this.$refs.urlExtMaxx.value, this.$refs.urlExtMaxy.value];
66339
- if (extVals.some(v => v.trim() === '')) {
66340
- this.toast('请填写完整的范围:X/Y 的最小值和最大值', 'warn');
66341
- return;
66342
- }
66343
- const nums = extVals.map(Number);
66344
- if (!nums.every(Number.isFinite)) {
66345
- this.toast('范围数值无效', 'warn');
66346
- return;
66347
- }
66348
- const extentOpt = {
66349
- extent: nums,
66350
- crs: this.form.urlExtentCrs
66351
- };
66352
- const r = await this.createXyzEntry(template, this.form.dataCrs, null, null, extentOpt);
66353
- if (!r.entry) {
66354
- this.toast(r.error || '加载失败', 'warn');
66355
- return;
66378
+ this.pushLoading();
66379
+ try {
66380
+ const template = String(this.form.urlTemplate || '').trim();
66381
+ if (!template) {
66382
+ this.toast('请填写在线切片XYZ URL 模板', 'warn');
66383
+ return;
66384
+ }
66385
+ if (String(this.form.urlMinzoom || '').trim() === '') {
66386
+ this.toast('请填写最小层级', 'warn');
66387
+ return;
66388
+ }
66389
+ if (String(this.form.urlMaxzoom || '').trim() === '') {
66390
+ this.toast('请填写最大层级', 'warn');
66391
+ return;
66392
+ }
66393
+ // 显示范围必填:坐标系可与切片坐标系不同,加载时自动换算
66394
+ const extVals = [this.$refs.urlExtMinx.value, this.$refs.urlExtMiny.value, this.$refs.urlExtMaxx.value, this.$refs.urlExtMaxy.value];
66395
+ if (extVals.some(v => v.trim() === '')) {
66396
+ this.toast('请填写完整的范围:X/Y 的最小值和最大值', 'warn');
66397
+ return;
66398
+ }
66399
+ const nums = extVals.map(Number);
66400
+ if (!nums.every(Number.isFinite)) {
66401
+ this.toast('范围数值无效', 'warn');
66402
+ return;
66403
+ }
66404
+ const extentOpt = {
66405
+ extent: nums,
66406
+ crs: this.form.urlExtentCrs
66407
+ };
66408
+ const r = await this.createXyzEntry(template, this.form.dataCrs, null, null, extentOpt);
66409
+ if (!r.entry) {
66410
+ this.toast(r.error || '加载失败', 'warn');
66411
+ return;
66412
+ }
66413
+ this.form.urlInfo = '使用指定范围加载,已自动定位。';
66414
+ this.addMapEntry(r.entry);
66415
+ } finally {
66416
+ this.popLoading();
66356
66417
  }
66357
- this.form.urlInfo = '使用指定范围加载,已自动定位。';
66358
- this.addMapEntry(r.entry);
66359
66418
  },
66360
66419
  // 由图层名生成稳定的矢量默认颜色。
66361
66420
  vectorColor(name) {
@@ -66791,120 +66850,125 @@ register(lib);
66791
66850
  },
66792
66851
  // 解析配对文件、套用 SLD 样式并加载为矢量图层组。
66793
66852
  async loadGeoJsonLayers() {
66794
- if (!this.pickedGeoJson || !this.pickedGeoJson.pairs.length) {
66795
- this.toast('请先选择包含 GeoJSON 文件的目录', 'warn');
66796
- return;
66797
- }
66798
- const format = new format_GeoJSON();
66799
- const allFeatures = [];
66800
- let firstProjection = null;
66801
- let firstCode = null;
66802
- let sldCount = 0;
66803
- let orderedPairs = this.pickedGeoJson.pairs;
66804
- const preparedPairs = [];
66853
+ this.pushLoading();
66805
66854
  try {
66806
- if (this.pickedGeoJson.metadataFile) {
66807
- const ordered = await this.orderGeoJsonPairsByMetadata(this.pickedGeoJson.pairs, this.pickedGeoJson.metadataFile);
66808
- orderedPairs = ordered.pairs;
66809
- this.pickedGeoJson.metadataName = ordered.metadata && ordered.metadata.name ? ordered.metadata.name : null;
66855
+ if (!this.pickedGeoJson || !this.pickedGeoJson.pairs.length) {
66856
+ this.toast('请先选择包含 GeoJSON 文件的目录', 'warn');
66857
+ return;
66810
66858
  }
66811
- for (const pair of orderedPairs) {
66812
- const text = await pair.file.text();
66813
- const object = JSON.parse(text);
66814
- // 优先使用每个文件自身声明的 CRS,避免不同文件坐标系不一致时被统一误读
66815
- const code = this.geoJsonCrsCode(object) || this.form.dataCrs;
66816
- const proj = this.ensureProjection(code);
66817
- if (!proj) throw new Error('无效坐标系:' + code);
66818
- let features = format.readFeatures(object, {
66819
- dataProjection: code,
66820
- featureProjection: code
66821
- });
66822
- if (firstProjection) {
66823
- if (proj.getCode() !== firstCode) {
66824
- for (const feature of features) {
66825
- const geometry = feature.getGeometry();
66826
- if (geometry) geometry.transform(proj, firstProjection);
66859
+ const format = new format_GeoJSON();
66860
+ const allFeatures = [];
66861
+ let firstProjection = null;
66862
+ let firstCode = null;
66863
+ let sldCount = 0;
66864
+ let orderedPairs = this.pickedGeoJson.pairs;
66865
+ const preparedPairs = [];
66866
+ try {
66867
+ if (this.pickedGeoJson.metadataFile) {
66868
+ const ordered = await this.orderGeoJsonPairsByMetadata(this.pickedGeoJson.pairs, this.pickedGeoJson.metadataFile);
66869
+ orderedPairs = ordered.pairs;
66870
+ this.pickedGeoJson.metadataName = ordered.metadata && ordered.metadata.name ? ordered.metadata.name : null;
66871
+ }
66872
+ for (const pair of orderedPairs) {
66873
+ const text = await pair.file.text();
66874
+ const object = JSON.parse(text);
66875
+ // 优先使用每个文件自身声明的 CRS,避免不同文件坐标系不一致时被统一误读
66876
+ const code = this.geoJsonCrsCode(object) || this.form.dataCrs;
66877
+ const proj = this.ensureProjection(code);
66878
+ if (!proj) throw new Error('无效坐标系:' + code);
66879
+ let features = format.readFeatures(object, {
66880
+ dataProjection: code,
66881
+ featureProjection: code
66882
+ });
66883
+ if (firstProjection) {
66884
+ if (proj.getCode() !== firstCode) {
66885
+ for (const feature of features) {
66886
+ const geometry = feature.getGeometry();
66887
+ if (geometry) geometry.transform(proj, firstProjection);
66888
+ }
66827
66889
  }
66890
+ } else {
66891
+ firstProjection = proj;
66892
+ firstCode = proj.getCode();
66828
66893
  }
66829
- } else {
66830
- firstProjection = proj;
66831
- firstCode = proj.getCode();
66832
- }
66833
- if (pair.sld) {
66834
- const sldText = await pair.sld.text();
66835
- const styles = this.parseSldStyles(sldText);
66836
- if (styles) {
66837
- const labelProperty = this.sldLabelProperty(sldText);
66838
- const styleFunction = this.makeSldStyleFunction(styles, labelProperty);
66839
- for (const feature of features) {
66840
- feature.set('_sldStyle', styleFunction, true);
66841
- if (pair.layerTitle) feature.set('_layerName', pair.layerTitle, true);
66894
+ if (pair.sld) {
66895
+ const sldText = await pair.sld.text();
66896
+ const styles = this.parseSldStyles(sldText);
66897
+ if (styles) {
66898
+ const labelProperty = this.sldLabelProperty(sldText);
66899
+ const styleFunction = this.makeSldStyleFunction(styles, labelProperty);
66900
+ for (const feature of features) {
66901
+ feature.set('_sldStyle', styleFunction, true);
66902
+ if (pair.layerTitle) feature.set('_layerName', pair.layerTitle, true);
66903
+ }
66904
+ sldCount++;
66842
66905
  }
66843
- sldCount++;
66844
66906
  }
66907
+ preparedPairs.push({
66908
+ pair,
66909
+ features
66910
+ });
66911
+ allFeatures.push(...features);
66845
66912
  }
66846
- preparedPairs.push({
66847
- pair,
66848
- features
66913
+ } catch (err) {
66914
+ this.toast('GeoJSON 加载失败:' + err.message, 'err');
66915
+ return;
66916
+ }
66917
+ if (!allFeatures.length) {
66918
+ this.toast('所选文件中没有有效要素', 'warn');
66919
+ return;
66920
+ }
66921
+ // 每个 GeoJSON 独立建层,便于左侧图层列表单独显示/隐藏
66922
+ const layerRecords = preparedPairs.map(({
66923
+ pair,
66924
+ features
66925
+ }) => {
66926
+ const source = this.createReprojectingVectorSource(features, firstProjection);
66927
+ const layer = new Vector({
66928
+ source,
66929
+ style: (feature, resolution) => this.defaultVectorStyle(feature, resolution)
66849
66930
  });
66850
- allFeatures.push(...features);
66931
+ return {
66932
+ name: pair.layerTitle || pair.file.name,
66933
+ kind: 'geojson',
66934
+ layer,
66935
+ featureCount: features.length,
66936
+ visible: true,
66937
+ group: this.inferGeoJsonGroup(features),
66938
+ groupVisible: true,
66939
+ source
66940
+ };
66941
+ });
66942
+ // 缺少 layer 属性的图层归入“0”组;只有确实存在“0”组时才归组,否则保持顶层普通图层
66943
+ if (layerRecords.some(record => record.group === '0')) {
66944
+ for (const record of layerRecords) {
66945
+ if (!record.group) record.group = '0';
66946
+ }
66851
66947
  }
66852
- } catch (err) {
66853
- this.toast('GeoJSON 加载失败:' + err.message, 'err');
66854
- return;
66855
- }
66856
- if (!allFeatures.length) {
66857
- this.toast('所选文件中没有有效要素', 'warn');
66858
- return;
66859
- }
66860
- // 每个 GeoJSON 独立建层,便于左侧图层列表单独显示/隐藏
66861
- const layerRecords = preparedPairs.map(({
66862
- pair,
66863
- features
66864
- }) => {
66865
- const source = this.createReprojectingVectorSource(features, firstProjection);
66866
- const layer = new Vector({
66867
- source,
66868
- style: (feature, resolution) => this.defaultVectorStyle(feature, resolution)
66948
+ const layer = new Group({
66949
+ layers: layerRecords.map(record => record.layer)
66869
66950
  });
66870
- return {
66871
- name: pair.layerTitle || pair.file.name,
66951
+ this.addMapEntry({
66952
+ name: this.pickedGeoJson.metadataName || this.pickedGeoJson.name,
66872
66953
  kind: 'geojson',
66873
66954
  layer,
66874
- featureCount: features.length,
66875
- visible: true,
66876
- group: this.inferGeoJsonGroup(features),
66877
- groupVisible: true,
66878
- source
66879
- };
66880
- });
66881
- // 缺少 layer 属性的图层归入“0”组;只有确实存在“0”组时才归组,否则保持顶层普通图层
66882
- if (layerRecords.some(record => record.group === '0')) {
66883
- for (const record of layerRecords) {
66884
- if (!record.group) record.group = '0';
66885
- }
66955
+ sublayers: layerRecords.map(record => {
66956
+ const rest = {
66957
+ ...record
66958
+ };
66959
+ delete rest.source;
66960
+ return rest;
66961
+ }),
66962
+ extent: this.featuresExtent(allFeatures),
66963
+ projection: firstProjection,
66964
+ sourceCrs: firstCode,
66965
+ sources: layerRecords.map(record => record.source),
66966
+ cleanup() {}
66967
+ });
66968
+ this.form.geojsonInfo = '已按 metadata.json 顺序加载 ' + orderedPairs.length + ' 个图层、' + allFeatures.length + ' 个要素,应用 ' + sldCount + ' 个 SLD 样式,数据坐标系 ' + firstCode + '。';
66969
+ } finally {
66970
+ this.popLoading();
66886
66971
  }
66887
- const layer = new Group({
66888
- layers: layerRecords.map(record => record.layer)
66889
- });
66890
- this.addMapEntry({
66891
- name: this.pickedGeoJson.metadataName || this.pickedGeoJson.name,
66892
- kind: 'geojson',
66893
- layer,
66894
- sublayers: layerRecords.map(record => {
66895
- const rest = {
66896
- ...record
66897
- };
66898
- delete rest.source;
66899
- return rest;
66900
- }),
66901
- extent: this.featuresExtent(allFeatures),
66902
- projection: firstProjection,
66903
- sourceCrs: firstCode,
66904
- sources: layerRecords.map(record => record.source),
66905
- cleanup() {}
66906
- });
66907
- this.form.geojsonInfo = '已按 metadata.json 顺序加载 ' + orderedPairs.length + ' 个图层、' + allFeatures.length + ' 个要素,应用 ' + sldCount + ' 个 SLD 样式,数据坐标系 ' + firstCode + '。';
66908
66972
  },
66909
66973
  // 切换视图坐标系并保持视野位置与比例。
66910
66974
  switchViewProjection() {
@@ -67092,17 +67156,17 @@ register(lib);
67092
67156
  this.addMapEntry(r.entry);
67093
67157
  return r.entry;
67094
67158
  },
67095
- // 从 URL 加载图片地图;可配世界文件(URL 或文本)与范围。
67159
+ // 从 URL 加载图片地图;可配世界文件(URL 或文本)与范围。crs 未传时按范围自动猜测高斯带号,未命中默认 EPSG:3857。
67096
67160
  async loadImageFromUrl({
67097
67161
  url,
67098
67162
  name = null,
67099
- crs = 'EPSG:3857',
67163
+ crs = null,
67100
67164
  worldFileUrl = null,
67101
67165
  worldText = null,
67102
67166
  extent = null
67103
67167
  } = {}) {
67104
67168
  if (!this.viewerReady) await this.waitViewerReady();
67105
- this.form.dataCrs = crs;
67169
+ if (crs) this.form.dataCrs = crs;
67106
67170
  if (!worldText && worldFileUrl) {
67107
67171
  try {
67108
67172
  const r = await fetch(worldFileUrl);
@@ -67113,7 +67177,8 @@ register(lib);
67113
67177
  url: url,
67114
67178
  name: name || decodeURIComponent(String(url).split('/').pop() || '图片'),
67115
67179
  worldText: worldText,
67116
- extent: extent
67180
+ extent: extent,
67181
+ crs: crs
67117
67182
  });
67118
67183
  return this.activeEntry;
67119
67184
  },
@@ -67260,22 +67325,27 @@ register(lib);
67260
67325
  },
67261
67326
  // 按 autoLoad prop 自动加载数据源:type 为 manifest 加载矢量清单,为 image 加载 PNG 栅格地图。
67262
67327
  async processAutoLoad() {
67263
- const item = this.autoLoad;
67264
- if (!item || !item.url) return;
67265
- if (item.type === 'manifest') {
67266
- const r = await fetch(item.url);
67267
- if (!r.ok) throw new Error('获取失败:' + item.url + '(' + r.status + ')');
67268
- const manifest = await r.json();
67269
- await this.loadGeoJsonFromManifest(manifest, item.baseUrl || '');
67270
- } else if (item.type === 'image') {
67271
- await this.loadImageFromUrl({
67272
- url: item.url,
67273
- name: item.name || null,
67274
- crs: item.crs || 'EPSG:3857',
67275
- worldFileUrl: item.worldFile || null,
67276
- worldText: item.worldText || null,
67277
- extent: item.extent || null
67278
- });
67328
+ this.pushLoading();
67329
+ try {
67330
+ const item = this.autoLoad;
67331
+ if (!item || !item.url) return;
67332
+ if (item.type === 'manifest') {
67333
+ const r = await fetch(item.url);
67334
+ if (!r.ok) throw new Error('获取失败:' + item.url + '(' + r.status + ')');
67335
+ const manifest = await r.json();
67336
+ await this.loadGeoJsonFromManifest(manifest, item.baseUrl || '');
67337
+ } else if (item.type === 'image') {
67338
+ await this.loadImageFromUrl({
67339
+ url: item.url,
67340
+ name: item.name || null,
67341
+ crs: item.crs || null,
67342
+ worldFileUrl: item.worldFile || null,
67343
+ worldText: item.worldText || null,
67344
+ extent: item.extent || null
67345
+ });
67346
+ }
67347
+ } finally {
67348
+ this.popLoading();
67279
67349
  }
67280
67350
  },
67281
67351
  // 应用明暗主题(初始值来自 theme prop,切换时发出 theme-changed 事件)。