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.
@@ -4535,7 +4535,7 @@ es_iterator_constructor_namespaceFn();
4535
4535
  ;// ./node_modules/core-js/modules/es.iterator.map.js
4536
4536
  es_iterator_map_namespaceFn();
4537
4537
 
4538
- ;// ./node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.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
4538
+ ;// ./node_modules/thread-loader/dist/cjs.js!./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.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
4539
4539
 
4540
4540
 
4541
4541
  var render = function render() {
@@ -5004,6 +5004,18 @@ var render = function render() {
5004
5004
  "id": "map"
5005
5005
  }
5006
5006
  }), _c('div', {
5007
+ directives: [{
5008
+ name: "show",
5009
+ rawName: "v-show",
5010
+ value: _vm.mapLoading,
5011
+ expression: "mapLoading"
5012
+ }],
5013
+ staticClass: "map-loading-mask"
5014
+ }, [_c('span', {
5015
+ staticClass: "map-loading-spinner"
5016
+ }), _c('span', {
5017
+ staticClass: "map-loading-text"
5018
+ }, [_vm._v("地图加载中...")])]), _c('div', {
5007
5019
  staticClass: "dialog-overlay",
5008
5020
  class: {
5009
5021
  show: _vm.loadDialogVisible
@@ -5583,7 +5595,7 @@ var staticRenderFns = [function () {
5583
5595
  }, [_vm._v("📥")]), _c('div', [_c('h1', [_vm._v("加载地图")]), _c('p', [_vm._v("选择数据源并加载")])])]);
5584
5596
  }];
5585
5597
 
5586
- ;// ./src/package/map_view/mapView.vue?vue&type=template&id=007e62b0
5598
+ ;// ./src/package/map_view/mapView.vue?vue&type=template&id=4496a68c
5587
5599
 
5588
5600
  ;// ./node_modules/core-js/modules/es.array.includes.js
5589
5601
  es_array_includes_namespaceFn();
@@ -64656,10 +64668,6 @@ register(lib);
64656
64668
  default: false
64657
64669
  },
64658
64670
  // 是否显示「加载地图」按钮与对话框。
64659
- showLoader: {
64660
- type: Boolean,
64661
- default: true
64662
- },
64663
64671
  // 固定地图查看器宽度:数字按 px,字符串原样使用(如 '600px' / '100%');不传保持铺满父容器。
64664
64672
  width: {
64665
64673
  type: [String, Number],
@@ -64688,6 +64696,10 @@ register(lib);
64688
64696
  // 地图查看器是否已完成初始化。
64689
64697
  sidebarWidth: 342,
64690
64698
  // 侧栏当前宽度(px),可通过右缘拖拽手柄调整。
64699
+ loadingTasks: 0,
64700
+ // 进行中的数据请求任务数(清单/文件拉取、解析等)。
64701
+ olLoading: false,
64702
+ // OpenLayers 图层渲染中(loadstart ~ loadend 之间)。
64691
64703
  dropVisible: false,
64692
64704
  // 是否显示全屏拖放遮罩层。
64693
64705
  sourceCategory: 'raster',
@@ -64819,6 +64831,10 @@ register(lib);
64819
64831
  // 地图画布背景色:传入时覆盖主题默认值(#map 的 background 引用该变量)。
64820
64832
  if (this.mapBackground) style['--map-bg'] = this.mapBackground;
64821
64833
  return style;
64834
+ },
64835
+ // 是否显示「地图加载中」遮罩:有数据请求任务或图层正在渲染时为 true。
64836
+ mapLoading() {
64837
+ return this.loadingTasks > 0 || this.olLoading;
64822
64838
  }
64823
64839
  },
64824
64840
  // 生命周期:挂载后初始化地图查看器、注册拖放加载与 URL 参数自动加载。
@@ -64951,6 +64967,13 @@ register(lib);
64951
64967
  this.updateStatus();
64952
64968
  this.$emit('view-changed', this.getViewState());
64953
64969
  });
64970
+ // 图层渲染开始 / 结束:瓦片、图片等加载期间显示遮罩。
64971
+ this.map.on('loadstart', () => {
64972
+ this.olLoading = true;
64973
+ });
64974
+ this.map.on('loadend', () => {
64975
+ this.olLoading = false;
64976
+ });
64954
64977
 
64955
64978
  /* ============================================================
64956
64979
  * 状态栏 / 提示
@@ -65112,6 +65135,13 @@ register(lib);
65112
65135
  this.updateUrlTemplateHint();
65113
65136
  this.viewerReady = true;
65114
65137
  },
65138
+ // 数据请求任务计数 +1 / -1:遮罩在任务数归零后隐藏。
65139
+ pushLoading() {
65140
+ this.loadingTasks += 1;
65141
+ },
65142
+ popLoading() {
65143
+ this.loadingTasks = Math.max(0, this.loadingTasks - 1);
65144
+ },
65115
65145
  // 按住侧栏右缘拖动:mousemove 实时更新宽度,限制在 280 ~ 620px 之间。
65116
65146
  startSidebarResize(e) {
65117
65147
  e.preventDefault();
@@ -65957,15 +65987,11 @@ register(lib);
65957
65987
  if (r1 || r2) throw new Error('不支持带旋转的世界文件');
65958
65988
  return [cx - dx / 2, cy + (h - 0.5) * dy, cx + (w - 0.5) * dx, cy - dy / 2];
65959
65989
  },
65960
- // 根据范围推断并提示 UTM/GK 分区对应的 EPSG。
65961
- updateZoneHint(extent) {
65962
- this.form.zoneHint = {
65963
- message: '',
65964
- options: []
65965
- };
65966
- if (!extent) return;
65990
+ // 根据范围数值模式猜测中国高斯投影带号 EPSG(3度带/6度带):命中返回候选码,未命中返回 null。
65991
+ guessCrsFromExtent(extent) {
65992
+ if (!extent) return null;
65967
65993
  const x = extent[2];
65968
- if (x <= 1e6 || x >= 7e7) return;
65994
+ if (x <= 1e6 || x >= 7e7) return null;
65969
65995
  const zone = Math.round(x / 1e6);
65970
65996
  const options = [];
65971
65997
  if (zone >= 25 && zone <= 45) {
@@ -65980,13 +66006,22 @@ register(lib);
65980
66006
  label: `6度带 ${zone},中央经线 ${zone * 6 - 3}°E(EPSG:${4491 + zone - 13},含带号)`
65981
66007
  });
65982
66008
  }
65983
- if (options.length) {
65984
- this.form.dataCrs = options[0].code;
65985
- this.form.zoneHint = {
65986
- message: '检测到高斯带号坐标,可能是:',
65987
- options
65988
- };
65989
- }
66009
+ if (!options.length) return null;
66010
+ this.form.zoneHint = {
66011
+ message: '检测到高斯带号坐标,可能是:',
66012
+ options
66013
+ };
66014
+ return options[0].code;
66015
+ },
66016
+ // 根据范围推断并提示 UTM/GK 分区对应的 EPSG。
66017
+ updateZoneHint(extent) {
66018
+ this.form.zoneHint = {
66019
+ message: '',
66020
+ options: []
66021
+ };
66022
+ if (!extent) return;
66023
+ const code = this.guessCrsFromExtent(extent);
66024
+ if (code) this.form.dataCrs = code;
65990
66025
  },
65991
66026
  // 将已选世界文件应用到范围输入框。
65992
66027
  applyWorldFileToExtent() {
@@ -66060,95 +66095,109 @@ register(lib);
66060
66095
  },
66061
66096
  // 加载 PNG/JPG 图片地图(配世界文件或手动填写范围)。
66062
66097
  async loadImageLayer() {
66063
- if (!this.pickedImage) {
66064
- this.toast('请先选择图片文件', 'warn');
66065
- return;
66066
- }
66067
- const proj = this.ensureProjection(this.form.dataCrs);
66068
- if (!proj) {
66069
- this.toast('请先选择或注册有效的坐标系', 'warn');
66070
- return;
66071
- }
66072
- let {
66073
- width,
66074
- height
66075
- } = this.pickedImage;
66076
- if (!width || !height) {
66077
- try {
66078
- const d = await this.getImageSize(this.pickedImage.objectUrl);
66079
- width = this.pickedImage.width = d.width;
66080
- height = this.pickedImage.height = d.height;
66081
- } catch {
66082
- this.toast('无法读取图片尺寸', 'err');
66098
+ this.pushLoading();
66099
+ try {
66100
+ if (!this.pickedImage) {
66101
+ this.toast('请先选择图片文件', 'warn');
66083
66102
  return;
66084
66103
  }
66085
- }
66086
- const er = this.readExtentInputs();
66087
- if (!er.ok) return;
66088
- const extent = er.extent;
66089
- const layerUrl = URL.createObjectURL(this.pickedImage.file);
66090
- const layer = new layer_Image({
66091
- source: new ImageStatic({
66092
- url: layerUrl,
66093
- imageExtent: extent,
66094
- projection: proj
66095
- })
66096
- });
66097
- this.addMapEntry({
66098
- name: this.pickedImage.name,
66099
- kind: 'image',
66100
- layer,
66101
- extent,
66102
- projection: proj,
66103
- cleanup() {
66104
- URL.revokeObjectURL(layerUrl);
66104
+ const proj = this.ensureProjection(this.form.dataCrs);
66105
+ if (!proj) {
66106
+ this.toast('请先选择或注册有效的坐标系', 'warn');
66107
+ return;
66105
66108
  }
66106
- });
66109
+ let {
66110
+ width,
66111
+ height
66112
+ } = this.pickedImage;
66113
+ if (!width || !height) {
66114
+ try {
66115
+ const d = await this.getImageSize(this.pickedImage.objectUrl);
66116
+ width = this.pickedImage.width = d.width;
66117
+ height = this.pickedImage.height = d.height;
66118
+ } catch {
66119
+ this.toast('无法读取图片尺寸', 'err');
66120
+ return;
66121
+ }
66122
+ }
66123
+ const er = this.readExtentInputs();
66124
+ if (!er.ok) return;
66125
+ const extent = er.extent;
66126
+ const layerUrl = URL.createObjectURL(this.pickedImage.file);
66127
+ const layer = new layer_Image({
66128
+ source: new ImageStatic({
66129
+ url: layerUrl,
66130
+ imageExtent: extent,
66131
+ projection: proj
66132
+ })
66133
+ });
66134
+ this.addMapEntry({
66135
+ name: this.pickedImage.name,
66136
+ kind: 'image',
66137
+ layer,
66138
+ extent,
66139
+ projection: proj,
66140
+ cleanup() {
66141
+ URL.revokeObjectURL(layerUrl);
66142
+ }
66143
+ });
66144
+ } finally {
66145
+ this.popLoading();
66146
+ }
66107
66147
  },
66108
66148
  // 从 URL(含可选世界文件或范围参数)创建并加载图片地图。
66109
66149
  async addImageFromUrl({
66110
66150
  url,
66111
66151
  name,
66112
66152
  worldText,
66113
- extent
66153
+ extent,
66154
+ crs = null
66114
66155
  }) {
66115
- let meta;
66156
+ this.pushLoading();
66116
66157
  try {
66117
- meta = await this.getImageSize(url);
66118
- } catch {
66119
- this.toast(`无法加载图片:${url}`, 'err');
66120
- return;
66121
- }
66122
- if (worldText) {
66158
+ let meta;
66123
66159
  try {
66124
- const wf = this.parseWorldFile(worldText);
66125
- extent = this.extentFromWorldFile(meta.width, meta.height, wf);
66126
- } catch (err) {
66127
- this.toast(`世界文件解析失败:${err.message}`, 'err');
66160
+ meta = await this.getImageSize(url);
66161
+ } catch {
66162
+ this.toast(`无法加载图片:${url}`, 'err');
66163
+ return;
66128
66164
  }
66165
+ if (worldText) {
66166
+ try {
66167
+ const wf = this.parseWorldFile(worldText);
66168
+ extent = this.extentFromWorldFile(meta.width, meta.height, wf);
66169
+ } catch (err) {
66170
+ this.toast(`世界文件解析失败:${err.message}`, 'err');
66171
+ }
66172
+ }
66173
+ extent = extent || [0, -meta.height, meta.width, 0];
66174
+ // 坐标系决策:显式传入的 crs 优先;未传入时按范围数值模式猜测高斯带号,未命中默认 EPSG:3857。
66175
+ this.updateZoneHint(extent);
66176
+ const crsCode = crs || this.guessCrsFromExtent(extent) || 'EPSG:3857';
66177
+ this.form.dataCrs = crsCode;
66178
+ const proj = this.ensureProjection(crsCode) || this.PIXEL;
66179
+ const layer = new layer_Image({
66180
+ source: new ImageStatic({
66181
+ url,
66182
+ imageExtent: extent,
66183
+ projection: proj
66184
+ })
66185
+ });
66186
+ this.$refs.extMinx.value = extent[0];
66187
+ this.$refs.extMiny.value = extent[1];
66188
+ this.$refs.extMaxx.value = extent[2];
66189
+ this.$refs.extMaxy.value = extent[3];
66190
+ this.addMapEntry({
66191
+ name,
66192
+ kind: 'image',
66193
+ layer,
66194
+ extent,
66195
+ projection: proj,
66196
+ cleanup() {}
66197
+ });
66198
+ } finally {
66199
+ this.popLoading();
66129
66200
  }
66130
- extent = extent || [0, -meta.height, meta.width, 0];
66131
- const proj = this.ensureProjection(this.form.dataCrs) || this.PIXEL;
66132
- const layer = new layer_Image({
66133
- source: new ImageStatic({
66134
- url,
66135
- imageExtent: extent,
66136
- projection: proj
66137
- })
66138
- });
66139
- this.$refs.extMinx.value = extent[0];
66140
- this.$refs.extMiny.value = extent[1];
66141
- this.$refs.extMaxx.value = extent[2];
66142
- this.$refs.extMaxy.value = extent[3];
66143
- this.updateZoneHint(extent);
66144
- this.addMapEntry({
66145
- name,
66146
- kind: 'image',
66147
- layer,
66148
- extent,
66149
- projection: proj,
66150
- cleanup() {}
66151
- });
66152
66201
  },
66153
66202
  // 处理用户选择的本地切片目录。
66154
66203
  onTilesDirChosen(fileList) {
@@ -66176,57 +66225,62 @@ register(lib);
66176
66225
  },
66177
66226
  // 加载本地 XYZ 切片目录为瓦片图层。
66178
66227
  loadTilesLayer() {
66179
- if (!this.pickedTiles || !this.pickedTiles.files.length) {
66180
- this.toast('请先选择本地切片XYZ目录', 'warn');
66181
- return;
66182
- }
66183
- const proj = this.ensureProjection(this.form.dataCrs);
66184
- if (!proj) {
66185
- this.toast('请先选择或注册有效的坐标系', 'warn');
66186
- return;
66187
- }
66188
- const tmap = this.scanTileFiles(this.pickedTiles.files, this.form.tilesTms);
66189
- if (!tmap.size) {
66190
- this.toast('所选目录中未找到 z/x/y 结构的切片文件', 'err');
66191
- return;
66192
- }
66193
- const stats = this.tileStats(tmap);
66194
- // 先按全球网格计算覆盖范围,再据此构建受限网格:
66195
- // 1) 层级钳制在 [minZ, maxZ],过度缩放时使用最近可用层级(拉伸显示而非空白)
66196
- // 2) 网格范围限制在实际覆盖区(配合 wrapX:false),缩小时只枚举数据范围内的瓦片,避免卡顿
66197
- const worldGrid = this.buildTileGrid(proj, stats.maxZ, stats.minZ);
66198
- const covered = this.tilesExtent(worldGrid, stats);
66199
- const grid = this.buildTileGrid(proj, stats.maxZ, stats.minZ, covered);
66200
- const urlCache = new Map();
66201
- const source = new source_XYZ({
66202
- projection: proj,
66203
- tileGrid: grid,
66204
- wrapX: false,
66205
- minZoom: stats.minZ,
66206
- maxZoom: stats.maxZ,
66207
- tileUrlFunction: ([z, x, y]) => {
66208
- const f = tmap.get(`${z}/${x}/${y}`);
66209
- if (!f) return undefined;
66210
- let u = urlCache.get(f);
66211
- if (!u) {
66212
- u = URL.createObjectURL(f);
66213
- urlCache.set(f, u);
66214
- }
66215
- return u;
66228
+ this.pushLoading();
66229
+ try {
66230
+ if (!this.pickedTiles || !this.pickedTiles.files.length) {
66231
+ this.toast('请先选择本地切片XYZ目录', 'warn');
66232
+ return;
66216
66233
  }
66217
- });
66218
- this.addMapEntry({
66219
- name: this.pickedTiles.name,
66220
- kind: 'tiles',
66221
- layer: new layer_Tile({
66222
- source
66223
- }),
66224
- extent: covered,
66225
- projection: proj,
66226
- cleanup() {
66227
- urlCache.forEach(u => URL.revokeObjectURL(u));
66234
+ const proj = this.ensureProjection(this.form.dataCrs);
66235
+ if (!proj) {
66236
+ this.toast('请先选择或注册有效的坐标系', 'warn');
66237
+ return;
66228
66238
  }
66229
- });
66239
+ const tmap = this.scanTileFiles(this.pickedTiles.files, this.form.tilesTms);
66240
+ if (!tmap.size) {
66241
+ this.toast('所选目录中未找到 z/x/y 结构的切片文件', 'err');
66242
+ return;
66243
+ }
66244
+ const stats = this.tileStats(tmap);
66245
+ // 先按全球网格计算覆盖范围,再据此构建受限网格:
66246
+ // 1) 层级钳制在 [minZ, maxZ],过度缩放时使用最近可用层级(拉伸显示而非空白)
66247
+ // 2) 网格范围限制在实际覆盖区(配合 wrapX:false),缩小时只枚举数据范围内的瓦片,避免卡顿
66248
+ const worldGrid = this.buildTileGrid(proj, stats.maxZ, stats.minZ);
66249
+ const covered = this.tilesExtent(worldGrid, stats);
66250
+ const grid = this.buildTileGrid(proj, stats.maxZ, stats.minZ, covered);
66251
+ const urlCache = new Map();
66252
+ const source = new source_XYZ({
66253
+ projection: proj,
66254
+ tileGrid: grid,
66255
+ wrapX: false,
66256
+ minZoom: stats.minZ,
66257
+ maxZoom: stats.maxZ,
66258
+ tileUrlFunction: ([z, x, y]) => {
66259
+ const f = tmap.get(`${z}/${x}/${y}`);
66260
+ if (!f) return undefined;
66261
+ let u = urlCache.get(f);
66262
+ if (!u) {
66263
+ u = URL.createObjectURL(f);
66264
+ urlCache.set(f, u);
66265
+ }
66266
+ return u;
66267
+ }
66268
+ });
66269
+ this.addMapEntry({
66270
+ name: this.pickedTiles.name,
66271
+ kind: 'tiles',
66272
+ layer: new layer_Tile({
66273
+ source
66274
+ }),
66275
+ extent: covered,
66276
+ projection: proj,
66277
+ cleanup() {
66278
+ urlCache.forEach(u => URL.revokeObjectURL(u));
66279
+ }
66280
+ });
66281
+ } finally {
66282
+ this.popLoading();
66283
+ }
66230
66284
  },
66231
66285
  // 更新在线 XYZ URL 模板提示。
66232
66286
  updateUrlTemplateHint() {
@@ -66311,41 +66365,46 @@ register(lib);
66311
66365
  },
66312
66366
  // 根据表单中的在线 XYZ 参数加载图层。
66313
66367
  async loadUrlLayer() {
66314
- const template = String(this.form.urlTemplate || '').trim();
66315
- if (!template) {
66316
- this.toast('请填写在线切片XYZ URL 模板', 'warn');
66317
- return;
66318
- }
66319
- if (String(this.form.urlMinzoom || '').trim() === '') {
66320
- this.toast('请填写最小层级', 'warn');
66321
- return;
66322
- }
66323
- if (String(this.form.urlMaxzoom || '').trim() === '') {
66324
- this.toast('请填写最大层级', 'warn');
66325
- return;
66326
- }
66327
- // 显示范围必填:坐标系可与切片坐标系不同,加载时自动换算
66328
- const extVals = [this.$refs.urlExtMinx.value, this.$refs.urlExtMiny.value, this.$refs.urlExtMaxx.value, this.$refs.urlExtMaxy.value];
66329
- if (extVals.some(v => v.trim() === '')) {
66330
- this.toast('请填写完整的范围:X/Y 的最小值和最大值', 'warn');
66331
- return;
66332
- }
66333
- const nums = extVals.map(Number);
66334
- if (!nums.every(Number.isFinite)) {
66335
- this.toast('范围数值无效', 'warn');
66336
- return;
66337
- }
66338
- const extentOpt = {
66339
- extent: nums,
66340
- crs: this.form.urlExtentCrs
66341
- };
66342
- const r = await this.createXyzEntry(template, this.form.dataCrs, null, null, extentOpt);
66343
- if (!r.entry) {
66344
- this.toast(r.error || '加载失败', 'warn');
66345
- return;
66368
+ this.pushLoading();
66369
+ try {
66370
+ const template = String(this.form.urlTemplate || '').trim();
66371
+ if (!template) {
66372
+ this.toast('请填写在线切片XYZ URL 模板', 'warn');
66373
+ return;
66374
+ }
66375
+ if (String(this.form.urlMinzoom || '').trim() === '') {
66376
+ this.toast('请填写最小层级', 'warn');
66377
+ return;
66378
+ }
66379
+ if (String(this.form.urlMaxzoom || '').trim() === '') {
66380
+ this.toast('请填写最大层级', 'warn');
66381
+ return;
66382
+ }
66383
+ // 显示范围必填:坐标系可与切片坐标系不同,加载时自动换算
66384
+ const extVals = [this.$refs.urlExtMinx.value, this.$refs.urlExtMiny.value, this.$refs.urlExtMaxx.value, this.$refs.urlExtMaxy.value];
66385
+ if (extVals.some(v => v.trim() === '')) {
66386
+ this.toast('请填写完整的范围:X/Y 的最小值和最大值', 'warn');
66387
+ return;
66388
+ }
66389
+ const nums = extVals.map(Number);
66390
+ if (!nums.every(Number.isFinite)) {
66391
+ this.toast('范围数值无效', 'warn');
66392
+ return;
66393
+ }
66394
+ const extentOpt = {
66395
+ extent: nums,
66396
+ crs: this.form.urlExtentCrs
66397
+ };
66398
+ const r = await this.createXyzEntry(template, this.form.dataCrs, null, null, extentOpt);
66399
+ if (!r.entry) {
66400
+ this.toast(r.error || '加载失败', 'warn');
66401
+ return;
66402
+ }
66403
+ this.form.urlInfo = '使用指定范围加载,已自动定位。';
66404
+ this.addMapEntry(r.entry);
66405
+ } finally {
66406
+ this.popLoading();
66346
66407
  }
66347
- this.form.urlInfo = '使用指定范围加载,已自动定位。';
66348
- this.addMapEntry(r.entry);
66349
66408
  },
66350
66409
  // 由图层名生成稳定的矢量默认颜色。
66351
66410
  vectorColor(name) {
@@ -66781,120 +66840,125 @@ register(lib);
66781
66840
  },
66782
66841
  // 解析配对文件、套用 SLD 样式并加载为矢量图层组。
66783
66842
  async loadGeoJsonLayers() {
66784
- if (!this.pickedGeoJson || !this.pickedGeoJson.pairs.length) {
66785
- this.toast('请先选择包含 GeoJSON 文件的目录', 'warn');
66786
- return;
66787
- }
66788
- const format = new format_GeoJSON();
66789
- const allFeatures = [];
66790
- let firstProjection = null;
66791
- let firstCode = null;
66792
- let sldCount = 0;
66793
- let orderedPairs = this.pickedGeoJson.pairs;
66794
- const preparedPairs = [];
66843
+ this.pushLoading();
66795
66844
  try {
66796
- if (this.pickedGeoJson.metadataFile) {
66797
- const ordered = await this.orderGeoJsonPairsByMetadata(this.pickedGeoJson.pairs, this.pickedGeoJson.metadataFile);
66798
- orderedPairs = ordered.pairs;
66799
- this.pickedGeoJson.metadataName = ordered.metadata && ordered.metadata.name ? ordered.metadata.name : null;
66845
+ if (!this.pickedGeoJson || !this.pickedGeoJson.pairs.length) {
66846
+ this.toast('请先选择包含 GeoJSON 文件的目录', 'warn');
66847
+ return;
66800
66848
  }
66801
- for (const pair of orderedPairs) {
66802
- const text = await pair.file.text();
66803
- const object = JSON.parse(text);
66804
- // 优先使用每个文件自身声明的 CRS,避免不同文件坐标系不一致时被统一误读
66805
- const code = this.geoJsonCrsCode(object) || this.form.dataCrs;
66806
- const proj = this.ensureProjection(code);
66807
- if (!proj) throw new Error('无效坐标系:' + code);
66808
- let features = format.readFeatures(object, {
66809
- dataProjection: code,
66810
- featureProjection: code
66811
- });
66812
- if (firstProjection) {
66813
- if (proj.getCode() !== firstCode) {
66814
- for (const feature of features) {
66815
- const geometry = feature.getGeometry();
66816
- if (geometry) geometry.transform(proj, firstProjection);
66849
+ const format = new format_GeoJSON();
66850
+ const allFeatures = [];
66851
+ let firstProjection = null;
66852
+ let firstCode = null;
66853
+ let sldCount = 0;
66854
+ let orderedPairs = this.pickedGeoJson.pairs;
66855
+ const preparedPairs = [];
66856
+ try {
66857
+ if (this.pickedGeoJson.metadataFile) {
66858
+ const ordered = await this.orderGeoJsonPairsByMetadata(this.pickedGeoJson.pairs, this.pickedGeoJson.metadataFile);
66859
+ orderedPairs = ordered.pairs;
66860
+ this.pickedGeoJson.metadataName = ordered.metadata && ordered.metadata.name ? ordered.metadata.name : null;
66861
+ }
66862
+ for (const pair of orderedPairs) {
66863
+ const text = await pair.file.text();
66864
+ const object = JSON.parse(text);
66865
+ // 优先使用每个文件自身声明的 CRS,避免不同文件坐标系不一致时被统一误读
66866
+ const code = this.geoJsonCrsCode(object) || this.form.dataCrs;
66867
+ const proj = this.ensureProjection(code);
66868
+ if (!proj) throw new Error('无效坐标系:' + code);
66869
+ let features = format.readFeatures(object, {
66870
+ dataProjection: code,
66871
+ featureProjection: code
66872
+ });
66873
+ if (firstProjection) {
66874
+ if (proj.getCode() !== firstCode) {
66875
+ for (const feature of features) {
66876
+ const geometry = feature.getGeometry();
66877
+ if (geometry) geometry.transform(proj, firstProjection);
66878
+ }
66817
66879
  }
66880
+ } else {
66881
+ firstProjection = proj;
66882
+ firstCode = proj.getCode();
66818
66883
  }
66819
- } else {
66820
- firstProjection = proj;
66821
- firstCode = proj.getCode();
66822
- }
66823
- if (pair.sld) {
66824
- const sldText = await pair.sld.text();
66825
- const styles = this.parseSldStyles(sldText);
66826
- if (styles) {
66827
- const labelProperty = this.sldLabelProperty(sldText);
66828
- const styleFunction = this.makeSldStyleFunction(styles, labelProperty);
66829
- for (const feature of features) {
66830
- feature.set('_sldStyle', styleFunction, true);
66831
- if (pair.layerTitle) feature.set('_layerName', pair.layerTitle, true);
66884
+ if (pair.sld) {
66885
+ const sldText = await pair.sld.text();
66886
+ const styles = this.parseSldStyles(sldText);
66887
+ if (styles) {
66888
+ const labelProperty = this.sldLabelProperty(sldText);
66889
+ const styleFunction = this.makeSldStyleFunction(styles, labelProperty);
66890
+ for (const feature of features) {
66891
+ feature.set('_sldStyle', styleFunction, true);
66892
+ if (pair.layerTitle) feature.set('_layerName', pair.layerTitle, true);
66893
+ }
66894
+ sldCount++;
66832
66895
  }
66833
- sldCount++;
66834
66896
  }
66897
+ preparedPairs.push({
66898
+ pair,
66899
+ features
66900
+ });
66901
+ allFeatures.push(...features);
66835
66902
  }
66836
- preparedPairs.push({
66837
- pair,
66838
- features
66903
+ } catch (err) {
66904
+ this.toast('GeoJSON 加载失败:' + err.message, 'err');
66905
+ return;
66906
+ }
66907
+ if (!allFeatures.length) {
66908
+ this.toast('所选文件中没有有效要素', 'warn');
66909
+ return;
66910
+ }
66911
+ // 每个 GeoJSON 独立建层,便于左侧图层列表单独显示/隐藏
66912
+ const layerRecords = preparedPairs.map(({
66913
+ pair,
66914
+ features
66915
+ }) => {
66916
+ const source = this.createReprojectingVectorSource(features, firstProjection);
66917
+ const layer = new Vector({
66918
+ source,
66919
+ style: (feature, resolution) => this.defaultVectorStyle(feature, resolution)
66839
66920
  });
66840
- allFeatures.push(...features);
66921
+ return {
66922
+ name: pair.layerTitle || pair.file.name,
66923
+ kind: 'geojson',
66924
+ layer,
66925
+ featureCount: features.length,
66926
+ visible: true,
66927
+ group: this.inferGeoJsonGroup(features),
66928
+ groupVisible: true,
66929
+ source
66930
+ };
66931
+ });
66932
+ // 缺少 layer 属性的图层归入“0”组;只有确实存在“0”组时才归组,否则保持顶层普通图层
66933
+ if (layerRecords.some(record => record.group === '0')) {
66934
+ for (const record of layerRecords) {
66935
+ if (!record.group) record.group = '0';
66936
+ }
66841
66937
  }
66842
- } catch (err) {
66843
- this.toast('GeoJSON 加载失败:' + err.message, 'err');
66844
- return;
66845
- }
66846
- if (!allFeatures.length) {
66847
- this.toast('所选文件中没有有效要素', 'warn');
66848
- return;
66849
- }
66850
- // 每个 GeoJSON 独立建层,便于左侧图层列表单独显示/隐藏
66851
- const layerRecords = preparedPairs.map(({
66852
- pair,
66853
- features
66854
- }) => {
66855
- const source = this.createReprojectingVectorSource(features, firstProjection);
66856
- const layer = new Vector({
66857
- source,
66858
- style: (feature, resolution) => this.defaultVectorStyle(feature, resolution)
66938
+ const layer = new Group({
66939
+ layers: layerRecords.map(record => record.layer)
66859
66940
  });
66860
- return {
66861
- name: pair.layerTitle || pair.file.name,
66941
+ this.addMapEntry({
66942
+ name: this.pickedGeoJson.metadataName || this.pickedGeoJson.name,
66862
66943
  kind: 'geojson',
66863
66944
  layer,
66864
- featureCount: features.length,
66865
- visible: true,
66866
- group: this.inferGeoJsonGroup(features),
66867
- groupVisible: true,
66868
- source
66869
- };
66870
- });
66871
- // 缺少 layer 属性的图层归入“0”组;只有确实存在“0”组时才归组,否则保持顶层普通图层
66872
- if (layerRecords.some(record => record.group === '0')) {
66873
- for (const record of layerRecords) {
66874
- if (!record.group) record.group = '0';
66875
- }
66945
+ sublayers: layerRecords.map(record => {
66946
+ const rest = {
66947
+ ...record
66948
+ };
66949
+ delete rest.source;
66950
+ return rest;
66951
+ }),
66952
+ extent: this.featuresExtent(allFeatures),
66953
+ projection: firstProjection,
66954
+ sourceCrs: firstCode,
66955
+ sources: layerRecords.map(record => record.source),
66956
+ cleanup() {}
66957
+ });
66958
+ this.form.geojsonInfo = '已按 metadata.json 顺序加载 ' + orderedPairs.length + ' 个图层、' + allFeatures.length + ' 个要素,应用 ' + sldCount + ' 个 SLD 样式,数据坐标系 ' + firstCode + '。';
66959
+ } finally {
66960
+ this.popLoading();
66876
66961
  }
66877
- const layer = new Group({
66878
- layers: layerRecords.map(record => record.layer)
66879
- });
66880
- this.addMapEntry({
66881
- name: this.pickedGeoJson.metadataName || this.pickedGeoJson.name,
66882
- kind: 'geojson',
66883
- layer,
66884
- sublayers: layerRecords.map(record => {
66885
- const rest = {
66886
- ...record
66887
- };
66888
- delete rest.source;
66889
- return rest;
66890
- }),
66891
- extent: this.featuresExtent(allFeatures),
66892
- projection: firstProjection,
66893
- sourceCrs: firstCode,
66894
- sources: layerRecords.map(record => record.source),
66895
- cleanup() {}
66896
- });
66897
- this.form.geojsonInfo = '已按 metadata.json 顺序加载 ' + orderedPairs.length + ' 个图层、' + allFeatures.length + ' 个要素,应用 ' + sldCount + ' 个 SLD 样式,数据坐标系 ' + firstCode + '。';
66898
66962
  },
66899
66963
  // 切换视图坐标系并保持视野位置与比例。
66900
66964
  switchViewProjection() {
@@ -67082,17 +67146,17 @@ register(lib);
67082
67146
  this.addMapEntry(r.entry);
67083
67147
  return r.entry;
67084
67148
  },
67085
- // 从 URL 加载图片地图;可配世界文件(URL 或文本)与范围。
67149
+ // 从 URL 加载图片地图;可配世界文件(URL 或文本)与范围。crs 未传时按范围自动猜测高斯带号,未命中默认 EPSG:3857。
67086
67150
  async loadImageFromUrl({
67087
67151
  url,
67088
67152
  name = null,
67089
- crs = 'EPSG:3857',
67153
+ crs = null,
67090
67154
  worldFileUrl = null,
67091
67155
  worldText = null,
67092
67156
  extent = null
67093
67157
  } = {}) {
67094
67158
  if (!this.viewerReady) await this.waitViewerReady();
67095
- this.form.dataCrs = crs;
67159
+ if (crs) this.form.dataCrs = crs;
67096
67160
  if (!worldText && worldFileUrl) {
67097
67161
  try {
67098
67162
  const r = await fetch(worldFileUrl);
@@ -67103,7 +67167,8 @@ register(lib);
67103
67167
  url: url,
67104
67168
  name: name || decodeURIComponent(String(url).split('/').pop() || '图片'),
67105
67169
  worldText: worldText,
67106
- extent: extent
67170
+ extent: extent,
67171
+ crs: crs
67107
67172
  });
67108
67173
  return this.activeEntry;
67109
67174
  },
@@ -67250,22 +67315,27 @@ register(lib);
67250
67315
  },
67251
67316
  // 按 autoLoad prop 自动加载数据源:type 为 manifest 加载矢量清单,为 image 加载 PNG 栅格地图。
67252
67317
  async processAutoLoad() {
67253
- const item = this.autoLoad;
67254
- if (!item || !item.url) return;
67255
- if (item.type === 'manifest') {
67256
- const r = await fetch(item.url);
67257
- if (!r.ok) throw new Error('获取失败:' + item.url + '(' + r.status + ')');
67258
- const manifest = await r.json();
67259
- await this.loadGeoJsonFromManifest(manifest, item.baseUrl || '');
67260
- } else if (item.type === 'image') {
67261
- await this.loadImageFromUrl({
67262
- url: item.url,
67263
- name: item.name || null,
67264
- crs: item.crs || 'EPSG:3857',
67265
- worldFileUrl: item.worldFile || null,
67266
- worldText: item.worldText || null,
67267
- extent: item.extent || null
67268
- });
67318
+ this.pushLoading();
67319
+ try {
67320
+ const item = this.autoLoad;
67321
+ if (!item || !item.url) return;
67322
+ if (item.type === 'manifest') {
67323
+ const r = await fetch(item.url);
67324
+ if (!r.ok) throw new Error('获取失败:' + item.url + '(' + r.status + ')');
67325
+ const manifest = await r.json();
67326
+ await this.loadGeoJsonFromManifest(manifest, item.baseUrl || '');
67327
+ } else if (item.type === 'image') {
67328
+ await this.loadImageFromUrl({
67329
+ url: item.url,
67330
+ name: item.name || null,
67331
+ crs: item.crs || null,
67332
+ worldFileUrl: item.worldFile || null,
67333
+ worldText: item.worldText || null,
67334
+ extent: item.extent || null
67335
+ });
67336
+ }
67337
+ } finally {
67338
+ this.popLoading();
67269
67339
  }
67270
67340
  },
67271
67341
  // 应用明暗主题(初始值来自 theme prop,切换时发出 theme-changed 事件)。