@geogirafe/lib-geoportal 1.1.0-dev.2602404359 → 1.1.0-dev.2610902439

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.
@@ -11,7 +11,7 @@ class AboutComponent extends GirafeHTMLElement {
11
11
  .panel{color:var(--text-color)}.panel>div{padding:1rem}img.logo{filter:var(--svg-filter);opacity:.8;height:4rem}.list{flex-direction:column;margin-top:1rem;display:flex}
12
12
  </style>
13
13
  <style>${this.customStyle}</style>
14
- <div class="panel"><div><img class="logo" alt="geogirafe-logo" src="images/logo/horizontal_black.svg"><div class="list"><div class="elem"><span i18n="Version">Version</span> : ${this.version}</div><div class="elem"><span i18n="Build">Build</span> : ${this.build}</div><div class="elem"><span i18n="Date">Date</span> : ${this.date}</div></div><div class="list"><div class="elem"><span i18n="Source">Source</span>: <a href="https://gitlab.com/geogirafe/gg-viewer" i18n="GitLab" rel="noopener" target="_blank">GitLab</a></div><div class="elem"><span i18n="Documentation">Documentation</span>: <a href="https://doc.geomapfish.dev" rel="noopener" target="_blank">https://doc.geomapfish.dev</a></div></div></div></div>
14
+ <div class="panel"><div><img class="logo" alt="geogirafe-logo" src="images/logo/horizontal_black.svg"><div class="list"><div class="elem"><span i18n="Version">Version</span> : ${this.version}</div><div class="elem"><span i18n="Build">Build</span> : ${this.build}</div><div class="elem"><span i18n="Date">Date</span> : ${this.date}</div></div><div class="list"><div class="elem"><span i18n="Source">Source</span>: <a href="https://gitlab.com/geogirafe/gg-viewer" i18n="GitLab" rel="noopener" target="_blank">GitLab</a></div><div class="elem"><span i18n="Documentation">Documentation</span>: <a href="https://doc.geogirafe.org" rel="noopener" target="_blank">https://doc.geogirafe.org</a></div></div></div></div>
15
15
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
16
16
  };
17
17
  isPanelVisible = false;
@@ -1,4 +1,5 @@
1
1
  import { html as uHtml } from 'uhtml';
2
+ // SPDX-License-Identifier: Apache-2.0
2
3
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
3
4
  import { toLonLat } from 'ol/proj.js';
4
5
  import { getCenter } from 'ol/extent.js';
@@ -11,7 +11,7 @@ class PrototypeBannerComponent extends GirafeHTMLElement {
11
11
  a{color:#fff;text-transform:uppercase;text-decoration:none}a:visited{color:#fff}
12
12
  </style>
13
13
  <style>${this.customStyle}</style>
14
- <a href="https://doc.geomapfish.dev/" i18n="Prototype" target="_blank" rel="noopener">PROTOTYPE</a>
14
+ <a href="https://doc.geogirafe.org/" i18n="Prototype" target="_blank" rel="noopener">PROTOTYPE</a>
15
15
  ${this.htmlUnsafe(this.feedbackTemplateHtml ?? '')}`;
16
16
  };
17
17
  constructor() {
@@ -91,4 +91,4 @@ img:-moz-loading {
91
91
  .no-data span {
92
92
  display: block;
93
93
  flex-grow: 1;
94
- }
94
+ }
package/package.json CHANGED
@@ -3,9 +3,9 @@
3
3
  "description": "GeoGirafe is a flexible application to build online geoportals.",
4
4
  "author": {
5
5
  "name": "GeoGirafe PSC",
6
- "url": "https://doc.geomapfish.dev"
6
+ "url": "https://doc.geogirafe.org"
7
7
  },
8
- "version": "1.1.0-dev.2602404359",
8
+ "version": "1.1.0-dev.2610902439",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -31,7 +31,7 @@
31
31
  "websig"
32
32
  ],
33
33
  "license": "Apache-2.0",
34
- "homepage": "https://doc.geomapfish.dev",
34
+ "homepage": "https://doc.geogirafe.org",
35
35
  "dependencies": {
36
36
  "buffer": "6.0.3",
37
37
  "d3": "7.9.0",
@@ -83,6 +83,7 @@
83
83
  "cross-env": "10.1.0",
84
84
  "eslint": "10.2.0",
85
85
  "eslint-plugin-es-x": "9.6.0",
86
+ "fake-indexeddb": "6.2.5",
86
87
  "fast-glob": "3.3.3",
87
88
  "fs-extra": "11.3.4",
88
89
  "globals": "17.4.0",
@@ -152,7 +152,7 @@
152
152
  <span i18n="Information without public faith">Information without public faith</span>
153
153
  <span>
154
154
  ©
155
- <a href="https://doc.geomapfish.dev" target="_blank">GeoGirafe</a>,
155
+ <a href="https://doc.geogirafe.org" target="_blank">GeoGirafe</a>,
156
156
  <a href="https://www.swisstopo.admin.ch/en/legal-basis" target="_blank">swisstopo</a>,
157
157
  <a href="http://www.openstreetmap.org/copyright" target="_blank">OSM</a>
158
158
  </span>
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2602404359", "build":"2602404359", "date":"15/06/2026"}
1
+ {"version":"1.1.0-dev.2610902439", "build":"2610902439", "date":"18/06/2026"}
@@ -27,5 +27,5 @@
27
27
  "types": ["node"]
28
28
  },
29
29
  "include": ["src/**/*.ts"],
30
- "exclude": ["node_modules", "src/service-worker.ts", "src/service-worker.js"]
30
+ "exclude": ["node_modules", "src/tools/sw"]
31
31
  }
@@ -120,7 +120,7 @@ export default defineConfig(({ command, mode }) => {
120
120
  { src: `${cesiumSource}/Workers`, dest: cesiumBaseUrl },
121
121
  { src: `${cesiumSource}/Assets`, dest: cesiumBaseUrl },
122
122
  { src: `${cesiumSource}/Widgets`, dest: cesiumBaseUrl },
123
- { src: `${geogirafeSource}/service-worker.js`, dest: '' },
123
+ { src: `${geogirafeSource}/tools/sw/*.js`, dest: '' },
124
124
  { src: `${geogirafeSource}/styles/*.css`, dest: 'styles/' },
125
125
  { src: `${geogirafeSource}/assets/*`, dest: '' },
126
126
  { src: `${geogirafeSource}/tools/auth/silentlogincallback.html`, dest: '' },
@@ -50,6 +50,7 @@ import SelectionToolComponent from '../../components/selectiontool/component.js'
50
50
  import AdvancedFilterComponent from '../../components/advancedfilter/component.js';
51
51
  import FixedDimensionComponent from '../../components/drawing/fixed-dimension/component.js';
52
52
  import GetDirectionsArtifact from '../../components/getdirections/component.js';
53
+ import ServiceWorkerHelper from '../utils/swhelper.js';
53
54
  import FeedbackButtonComponent from '../../components/feedbackbutton/component.js';
54
55
  import AddBookmarkComponent from '../../components/addbookmark/component.js';
55
56
  import DrawingToolbarComponent from '../../components/drawing-toolbar/component.js';
@@ -168,21 +169,29 @@ export default class GeoGirafeApp {
168
169
  catch (err) {
169
170
  throw new Error(`Service worker registration failed. ${err}`, { cause: err });
170
171
  }
171
- // Communicate logging configuration to service-worker
172
172
  const config = this.context.configManager.Config;
173
- sw.postMessage({ logLevel: config.general.logLevel });
174
- // Communicate oauth configuration to service-worker
173
+ const serviceWorkerHelper = new ServiceWorkerHelper(sw);
174
+ // Communicate configuration to service-worker
175
+ // NOTE REG : Important : we have to wait that the config has been correctly sent to the SW
176
+ // Otherwise the first requests can be sent without the correct auhentication.
175
177
  const issuerConfig = config.oauth?.issuer ?? config.gmfauth;
176
178
  const gmfConfig = config.oauth?.geomapfish ?? config.gmfauth;
177
179
  if (issuerConfig && gmfConfig) {
178
180
  const issuerHostname = new URL(issuerConfig.url).hostname;
179
181
  const audience = [...issuerConfig.audience, issuerHostname];
180
- sw.postMessage({
182
+ const swConfig = {
183
+ logLevel: config.general.logLevel,
181
184
  audience: audience,
182
185
  audienceExcludedPaths: issuerConfig.audienceExcludedPaths,
183
186
  authMode: gmfConfig.authMode,
187
+ alwaysSendCookies: issuerConfig.alwaysSendCookies,
184
188
  refererPolicy: gmfConfig.refererPolicy
185
- });
189
+ };
190
+ await serviceWorkerHelper.sendMessageToServiceWorker(swConfig);
191
+ }
192
+ else {
193
+ // If no auth, we just update the log level, but we do not have to wait
194
+ sw.postMessage({ logLevel: config.general.logLevel });
186
195
  }
187
196
  await this.context.offlineManager.setServiceWorker(sw, storeVersion, dbCacheName);
188
197
  await this.context.authManager.initialize(sw);
@@ -194,7 +203,7 @@ export default class GeoGirafeApp {
194
203
  }
195
204
  }
196
205
  async waitForServiceWorkerActivation() {
197
- const registration = await navigator.serviceWorker.register('service-worker.js');
206
+ const registration = await navigator.serviceWorker.register('service-worker.js', { type: 'module' });
198
207
  if (registration.active) {
199
208
  return registration.active;
200
209
  }
@@ -191,6 +191,7 @@ declare class GirafeConfig {
191
191
  checkSessionOnLoad: boolean;
192
192
  audience: string[];
193
193
  authMode: 'cookie';
194
+ alwaysSendCookies: boolean;
194
195
  refererPolicy: ReferrerPolicy;
195
196
  audienceExcludedPaths: string[];
196
197
  };
@@ -205,6 +206,7 @@ declare class GirafeConfig {
205
206
  checkSessionOnLoad: boolean;
206
207
  audience: string[];
207
208
  audienceExcludedPaths: string[];
209
+ alwaysSendCookies: boolean;
208
210
  };
209
211
  geomapfish: {
210
212
  userInfoUrl: string;
@@ -49,7 +49,7 @@ class GirafeConfig {
49
49
  * @param config the configuration
50
50
  */
51
51
  constructor(config) {
52
- // Default values are documented here : https://doc.geomapfish.dev/docs/configuration
52
+ // Default values are documented here : https://doc.geogirafe.org/docs/configuration
53
53
  // NOTE: Please adapt the documentation if necessary when doing changes here.
54
54
  this.general = this.initConfigGeneral(config);
55
55
  this.languages = this.initConfigLanguages(config);
@@ -127,7 +127,7 @@ class GirafeConfig {
127
127
  }
128
128
  initConfigProjections(config) {
129
129
  if (!config.projections) {
130
- throw new Error(`Configuration for projections is required. See https://doc.geomapfish.dev/docs/configuration`);
130
+ throw new Error(`Configuration for projections is required. See https://doc.geogirafe.org/docs/configuration`);
131
131
  }
132
132
  return config.projections;
133
133
  }
@@ -137,16 +137,16 @@ class GirafeConfig {
137
137
  initConfigMap(config) {
138
138
  // Map
139
139
  if (!config.map?.srid) {
140
- throw new Error(`Configuration for projections is required. See https://doc.geomapfish.dev/docs/configuration`);
140
+ throw new Error(`Configuration for projections is required. See https://doc.geogirafe.org/docs/configuration`);
141
141
  }
142
142
  if (!config.map?.scales) {
143
- throw new Error(`Configuration for projections is required. See https://doc.geomapfish.dev/docs/configuration`);
143
+ throw new Error(`Configuration for projections is required. See https://doc.geogirafe.org/docs/configuration`);
144
144
  }
145
145
  if (!config.map?.startPosition) {
146
- throw new Error(`Configuration for projections is required. See https://doc.geomapfish.dev/docs/configuration`);
146
+ throw new Error(`Configuration for projections is required. See https://doc.geogirafe.org/docs/configuration`);
147
147
  }
148
148
  if (!config.map?.startZoom) {
149
- throw new Error(`Configuration for projections is required. See https://doc.geomapfish.dev/docs/configuration`);
149
+ throw new Error(`Configuration for projections is required. See https://doc.geogirafe.org/docs/configuration`);
150
150
  }
151
151
  return {
152
152
  srid: config.map.srid,
@@ -179,10 +179,10 @@ class GirafeConfig {
179
179
  }
180
180
  initConfigShare(config) {
181
181
  if (!config.share?.createUrl) {
182
- throw new Error(`Configuration for share.createUrl is required. See https://doc.geomapfish.dev/docs/configuration`);
182
+ throw new Error(`Configuration for share.createUrl is required. See https://doc.geogirafe.org/docs/configuration`);
183
183
  }
184
184
  if (config.share?.service === 'geogirafe' && !config.share?.getUrl) {
185
- throw new Error(`Configuration for share.getUrl is required if service type is 'geogirafe'. See https://doc.geomapfish.dev/docs/configuration`);
185
+ throw new Error(`Configuration for share.getUrl is required if service type is 'geogirafe'. See https://doc.geogirafe.org/docs/configuration`);
186
186
  }
187
187
  return {
188
188
  service: config.share?.service ?? 'gmf',
@@ -204,7 +204,7 @@ class GirafeConfig {
204
204
  }
205
205
  initConfigPrint(config) {
206
206
  if (!config.print?.url) {
207
- throw new Error(`Configuration for print.url is required. See https://doc.geomapfish.dev/docs/configuration`);
207
+ throw new Error(`Configuration for print.url is required. See https://doc.geogirafe.org/docs/configuration`);
208
208
  }
209
209
  config.print.attributeNames ??= ['title', 'comments', 'legend'];
210
210
  config.print.formats ??= ['pdf', 'png'];
@@ -212,10 +212,10 @@ class GirafeConfig {
212
212
  }
213
213
  initConfigSearch(config) {
214
214
  if (!config.search?.url) {
215
- throw new Error(`Configuration for search.url is required. See https://doc.geomapfish.dev/docs/configuration`);
215
+ throw new Error(`Configuration for search.url is required. See https://doc.geogirafe.org/docs/configuration`);
216
216
  }
217
217
  if (!config.search.url.includes('###SEARCHTERM###')) {
218
- throw new Error(`search.url is missing the expected pattern. See https://doc.geomapfish.dev/docs/configuration`);
218
+ throw new Error(`search.url is missing the expected pattern. See https://doc.geogirafe.org/docs/configuration`);
219
219
  }
220
220
  return {
221
221
  url: config.search.url,
@@ -301,7 +301,7 @@ class GirafeConfig {
301
301
  }
302
302
  initConfigThemes(config) {
303
303
  if (!config.themes?.url) {
304
- throw new Error(`Configuration for themes.url is required. See https://doc.geomapfish.dev/docs/configuration.`);
304
+ throw new Error(`Configuration for themes.url is required. See https://doc.geogirafe.org/docs/configuration.`);
305
305
  }
306
306
  return {
307
307
  url: config.themes.url,
@@ -313,7 +313,7 @@ class GirafeConfig {
313
313
  }
314
314
  initConfigLanguages(config) {
315
315
  if (!config.languages) {
316
- throw new Error(`Configuration for languages is required. See https://doc.geomapfish.dev/docs/configuration.`);
316
+ throw new Error(`Configuration for languages is required. See https://doc.geogirafe.org/docs/configuration.`);
317
317
  }
318
318
  return config.languages;
319
319
  }
@@ -343,14 +343,14 @@ class GirafeConfig {
343
343
  return undefined;
344
344
  }
345
345
  if (!config.gmfauth.url) {
346
- throw new Error(`Configuration for gmfauth.url is required. See https://doc.geomapfish.dev/docs/configuration.`);
346
+ throw new Error(`Configuration for gmfauth.url is required. See https://doc.geogirafe.org/docs/configuration.`);
347
347
  }
348
348
  let gmfauthUrl = config.gmfauth.url;
349
349
  if (!config.gmfauth.url.endsWith('/')) {
350
350
  gmfauthUrl = `${config.gmfauth.url}/`;
351
351
  }
352
352
  if (!config.gmfauth.audience) {
353
- throw new Error(`Configuration for gmfauth.audience is required. See https://doc.geomapfish.dev/docs/configuration.`);
353
+ throw new Error(`Configuration for gmfauth.audience is required. See https://doc.geogirafe.org/docs/configuration.`);
354
354
  }
355
355
  return {
356
356
  url: gmfauthUrl,
@@ -358,6 +358,7 @@ class GirafeConfig {
358
358
  loginRequired: config.gmfauth.loginRequired ?? false,
359
359
  checkSessionOnLoad: config.gmfauth.checkSessionOnLoad ?? true,
360
360
  authMode: 'cookie',
361
+ alwaysSendCookies: config.gmfauth.alwaysSendCookies ?? false,
361
362
  refererPolicy: config.gmfauth.refererPolicy ?? 'strict-origin-when-cross-origin',
362
363
  audienceExcludedPaths: config.gmfauth.audienceExcludedPaths ?? []
363
364
  };
@@ -368,16 +369,16 @@ class GirafeConfig {
368
369
  return undefined;
369
370
  }
370
371
  if (!config.oauth.issuer.url) {
371
- throw new Error(`Configuration for oauth.issuer.url is required. See https://doc.geomapfish.dev/docs/configuration.`);
372
+ throw new Error(`Configuration for oauth.issuer.url is required. See https://doc.geogirafe.org/docs/configuration.`);
372
373
  }
373
374
  if (!config.oauth.issuer.clientId) {
374
- throw new Error(`Configuration for oauth.issuer.clientId is required. See https://doc.geomapfish.dev/docs/configuration.`);
375
+ throw new Error(`Configuration for oauth.issuer.clientId is required. See https://doc.geogirafe.org/docs/configuration.`);
375
376
  }
376
377
  if (!config.oauth.issuer.audience) {
377
- throw new Error(`Configuration for oauth.issuer.audience is required. See https://doc.geomapfish.dev/docs/configuration.`);
378
+ throw new Error(`Configuration for oauth.issuer.audience is required. See https://doc.geogirafe.org/docs/configuration.`);
378
379
  }
379
380
  if (!config.oauth.geomapfish.userInfoUrl) {
380
- throw new Error(`Configuration for oauth.geomapfish.userInfoUrl is required. See https://doc.geomapfish.dev/docs/configuration.`);
381
+ throw new Error(`Configuration for oauth.geomapfish.userInfoUrl is required. See https://doc.geogirafe.org/docs/configuration.`);
381
382
  }
382
383
  const issuerConfig = {
383
384
  url: config.oauth.issuer.url,
@@ -388,7 +389,8 @@ class GirafeConfig {
388
389
  loginRequired: config.oauth.issuer.loginRequired ?? false,
389
390
  checkSessionOnLoad: config.oauth.issuer.checkSessionOnLoad ?? false,
390
391
  audience: config.oauth.issuer.audience,
391
- audienceExcludedPaths: config.oauth.issuer.audienceExcludedPaths ?? []
392
+ audienceExcludedPaths: config.oauth.issuer.audienceExcludedPaths ?? [],
393
+ alwaysSendCookies: config.oauth.issuer.alwaysSendCookies ?? false
392
394
  };
393
395
  const geomapfishConfig = {
394
396
  userInfoUrl: config.oauth.geomapfish.userInfoUrl,
@@ -445,7 +447,7 @@ class GirafeConfig {
445
447
  if (config.filtering?.gmfLayerMetadataUrl &&
446
448
  !(config.filtering.gmfLayerMetadataUrl.includes('###LAYERNAME###') &&
447
449
  config.filtering.gmfLayerMetadataUrl.includes('###ATTRIBUTENAME###'))) {
448
- throw new Error(`Configuration for filtering.gmfLayerMetadataUrl is missing the expected pattern. See https://doc.geomapfish.dev/docs/configuration`);
450
+ throw new Error(`Configuration for filtering.gmfLayerMetadataUrl is missing the expected pattern. See https://doc.geogirafe.org/docs/configuration`);
449
451
  }
450
452
  return config.filtering ?? undefined;
451
453
  }
@@ -42,7 +42,7 @@ class I18nManager extends GirafeSingleton {
42
42
  this.loadingLanguagePromise = this.context.configManager.loadConfig().then(async () => {
43
43
  if (this.context.configManager.Config?.languages.translations &&
44
44
  language in this.context.configManager.Config.languages.translations) {
45
- let mergedTranslations = {};
45
+ let mergedTranslations = this.translations[language] ?? {};
46
46
  // Translations are loaded in the order defined in the list of files
47
47
  // If an element is present in both results, the last value overwrite all the others
48
48
  for (const url of this.context.configManager.Config.languages.translations[language]) {
package/tools/main.d.ts CHANGED
@@ -104,6 +104,8 @@ export type { GgUserInteractionListener } from './state/userInteractionManager.j
104
104
  export { default as UserInteractionManager } from './state/userInteractionManager.js';
105
105
  export type { GgUserInteractionEvent } from './state/userinteractionevent.js';
106
106
  export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
107
+ export type { SwState } from './sw/service-worker.tools';
108
+ export { swLog, SwHelper, IndexedDbHelper, CacheHelper } from './sw/service-worker.tools';
107
109
  export { default as CustomThemesManager } from './themes/customthemesmanager.js';
108
110
  export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
109
111
  export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
package/tools/main.js CHANGED
@@ -77,6 +77,7 @@ export { default as StateToggleManager } from './state/stateToggleManager.js';
77
77
  export { default as StateManager } from './state/statemanager.js';
78
78
  export { default as UserInteractionManager } from './state/userInteractionManager.js';
79
79
  export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
80
+ export { swLog, SwHelper, IndexedDbHelper, CacheHelper } from './sw/service-worker.tools';
80
81
  export { default as CustomThemesManager } from './themes/customthemesmanager.js';
81
82
  export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
82
83
  export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
@@ -3,14 +3,15 @@ import LayerWmts from '../../models/layers/layerwmts.js';
3
3
  import { Extent } from 'ol/extent.js';
4
4
  declare class OfflineManager extends GirafeSingleton {
5
5
  private serviceWorker;
6
- private database?;
6
+ private readonly indexedDbHelper;
7
7
  private get map();
8
8
  private get state();
9
+ private database;
9
10
  private totalLength;
10
11
  private counter;
11
12
  private progressCallback?;
12
- private storeVersion?;
13
- private dbCacheName?;
13
+ private storeVersion;
14
+ private dbCacheName;
14
15
  private readonly tilesStoreName;
15
16
  private readonly bboxStoreName;
16
17
  private readonly vectorLayer;
@@ -19,7 +20,7 @@ declare class OfflineManager extends GirafeSingleton {
19
20
  private registerEvents;
20
21
  /** Exports all the WMTS tiles for the layers in parameter and store them to local cache */
21
22
  exportWMTSTiles(bbox: Extent, wmtsLayers: LayerWmts[], progressCallback?: CallableFunction): Promise<void>;
22
- private openIndexedDB;
23
+ upgradeIndexedDb(database: IDBDatabase): void;
23
24
  /** The offline manager works with a ServiceWorker in charge of intercepting
24
25
  * the fetch requests and read the data from the local cache if the application
25
26
  * is offline. This method defines the servicework object to use.
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import GirafeSingleton from '../../base/GirafeSingleton.js';
3
+ import { IndexedDbHelper } from '../sw/service-worker.tools';
3
4
  import { Feature } from 'ol';
4
5
  import VectorLayer from 'ol/layer/Vector.js';
5
6
  import VectorSource from 'ol/source/Vector.js';
@@ -8,13 +9,17 @@ import Style from 'ol/style/Style.js';
8
9
  import { Stroke } from 'ol/style.js';
9
10
  class OfflineManager extends GirafeSingleton {
10
11
  serviceWorker = null;
11
- database;
12
+ indexedDbHelper = new IndexedDbHelper();
12
13
  get map() {
13
14
  return this.context.mapManager.getMap();
14
15
  }
15
16
  get state() {
16
17
  return this.context.stateManager.state;
17
18
  }
19
+ async database() {
20
+ const database = await this.indexedDbHelper.openDb(this.dbCacheName, this.storeVersion, this.upgradeIndexedDb);
21
+ return database;
22
+ }
18
23
  totalLength = 0;
19
24
  counter = 0;
20
25
  progressCallback;
@@ -38,10 +43,10 @@ class OfflineManager extends GirafeSingleton {
38
43
  this.context.stateManager.state.isOffline = isOffline;
39
44
  }
40
45
  registerEvents() {
41
- window.addEventListener('offline', () => {
46
+ globalThis.addEventListener('offline', () => {
42
47
  this.state.isOffline = true;
43
48
  });
44
- window.addEventListener('online', () => {
49
+ globalThis.addEventListener('online', () => {
45
50
  this.state.isOffline = false;
46
51
  });
47
52
  this.context.stateManager.subscribe('isOffline', () => {
@@ -55,45 +60,16 @@ class OfflineManager extends GirafeSingleton {
55
60
  const tileUrls = this.getAllTileUrls(bbox, wmtsLayers);
56
61
  await this.fetchAndSaveTiles(tileUrls);
57
62
  }
58
- async openIndexedDB() {
59
- return new Promise((resolve, reject) => {
60
- console.debug('Opening IndexedDB');
61
- const request = indexedDB.open(this.dbCacheName, this.storeVersion);
62
- let timedOut = false;
63
- const timeoutId = setTimeout(() => {
64
- timedOut = true;
65
- console.debug('Timeout while opening IndexedDB');
66
- reject(new Error('Timeout while opening IndexedDB'));
67
- }, 3000);
68
- request.onerror = (event) => {
69
- if (!timedOut) {
70
- clearTimeout(timeoutId);
71
- const message = event.target.error?.message;
72
- console.debug(`IndexedDB could not be opened : ${message}`);
73
- reject(new Error(message));
74
- }
75
- };
76
- request.onsuccess = (event) => {
77
- if (!timedOut) {
78
- clearTimeout(timeoutId);
79
- console.debug('IndexedDB is open');
80
- resolve(event.target.result);
81
- }
82
- };
83
- request.onupgradeneeded = (event) => {
84
- // Version migration is necessary.
85
- // open the indexedDB and create the new structure
86
- console.debug('Upgrading IndexedDB');
87
- const database = event.target.result;
88
- // First : a store for tiles
89
- const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
90
- tilesStore.createIndex('url', 'url', { unique: true });
91
- // Second : A store for offline available bbox
92
- database.createObjectStore('bbox', { autoIncrement: true });
93
- console.debug('IndexedDB upgraded.');
94
- resolve(database);
95
- };
96
- });
63
+ upgradeIndexedDb(database) {
64
+ // Version migration is necessary.
65
+ // open the indexedDB and create the new structure
66
+ console.debug('Upgrading IndexedDB');
67
+ // First : a store for tiles
68
+ const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
69
+ tilesStore.createIndex('url', 'url', { unique: true });
70
+ // Second : A store for offline available bbox
71
+ database.createObjectStore('bbox', { autoIncrement: true });
72
+ console.debug('IndexedDB upgraded.');
97
73
  }
98
74
  /** The offline manager works with a ServiceWorker in charge of intercepting
99
75
  * the fetch requests and read the data from the local cache if the application
@@ -108,7 +84,6 @@ class OfflineManager extends GirafeSingleton {
108
84
  this.serviceWorker = sw;
109
85
  this.storeVersion = storeVersion;
110
86
  this.dbCacheName = dbCacheName;
111
- this.database = await this.openIndexedDB();
112
87
  this.serviceWorker.postMessage({
113
88
  storeVersion: this.storeVersion,
114
89
  dbCacheName: this.dbCacheName,
@@ -165,9 +140,7 @@ class OfflineManager extends GirafeSingleton {
165
140
  const iterator = tileUrls.values();
166
141
  // Use 4 parallel workers to download tiles
167
142
  this.counter = 0;
168
- const workers = Array(4)
169
- .fill(iterator)
170
- .map((iterator) => this.doWork(iterator));
143
+ const workers = new Array(4).fill(iterator).map((iterator) => this.doWork(iterator));
171
144
  Promise.allSettled(workers).then(() => {
172
145
  console.debug('Everything downloaded.');
173
146
  if (this.progressCallback) {
@@ -180,41 +153,41 @@ class OfflineManager extends GirafeSingleton {
180
153
  for (const url of iterator) {
181
154
  const response = await fetch(url);
182
155
  if (response.ok) {
183
- response.blob().then((blob) => {
184
- const transaction = this.database.transaction([this.tilesStoreName], 'readwrite');
185
- const store = transaction.objectStore(this.tilesStoreName);
186
- const index = store.index('url');
187
- const dbRequest = index.getKey(url);
188
- dbRequest.onsuccess = () => {
189
- let request;
190
- if (dbRequest.result) {
191
- const key = dbRequest.result;
192
- request = store.put({ url: url, data: blob }, key);
193
- }
194
- else {
195
- request = store.put({ url: url, data: blob });
156
+ const blob = await response.blob();
157
+ const transaction = (await this.database()).transaction([this.tilesStoreName], 'readwrite');
158
+ const store = transaction.objectStore(this.tilesStoreName);
159
+ const index = store.index('url');
160
+ const dbRequest = index.getKey(url);
161
+ dbRequest.onsuccess = () => {
162
+ let request;
163
+ if (dbRequest.result) {
164
+ const key = dbRequest.result;
165
+ request = store.put({ url: url, data: blob }, key);
166
+ }
167
+ else {
168
+ request = store.put({ url: url, data: blob });
169
+ }
170
+ request.onsuccess = () => {
171
+ if (this.progressCallback) {
172
+ this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
196
173
  }
197
- request.onsuccess = () => {
198
- if (this.progressCallback) {
199
- this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
200
- }
201
- console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
202
- };
203
- request.onerror = () => {
204
- console.error(`Error while saving tile ${url}`);
205
- };
174
+ console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
206
175
  };
207
- });
176
+ request.onerror = () => {
177
+ console.error(`Error while saving tile ${url}`);
178
+ };
179
+ };
208
180
  }
209
181
  }
210
182
  }
211
183
  async getTotalSizeMB() {
184
+ const database = await this.database();
212
185
  return new Promise((resolve, reject) => {
213
- if (!this.database) {
186
+ if (!database) {
214
187
  reject(new Error('Database is not initialized.'));
215
188
  return;
216
189
  }
217
- const transaction = this.database.transaction([this.tilesStoreName], 'readonly');
190
+ const transaction = database.transaction([this.tilesStoreName], 'readonly');
218
191
  const store = transaction.objectStore(this.tilesStoreName);
219
192
  const request = store.openCursor();
220
193
  let totalBytes = 0;
@@ -239,12 +212,13 @@ class OfflineManager extends GirafeSingleton {
239
212
  });
240
213
  }
241
214
  async clearStore(storeName) {
215
+ const database = await this.database();
242
216
  return new Promise((resolve, reject) => {
243
217
  if (!this.database) {
244
218
  reject(new Error('Database is not initialized.'));
245
219
  return;
246
220
  }
247
- const transaction = this.database.transaction([storeName], 'readwrite');
221
+ const transaction = database.transaction([storeName], 'readwrite');
248
222
  const store = transaction.objectStore(storeName);
249
223
  const request = store.clear();
250
224
  request.onsuccess = () => {
@@ -264,11 +238,9 @@ class OfflineManager extends GirafeSingleton {
264
238
  this.clearStore(this.bboxStoreName);
265
239
  }
266
240
  async saveBoundingBox(bbox) {
267
- if (this.database === undefined) {
268
- this.database = await this.openIndexedDB();
269
- }
241
+ const database = await this.database();
270
242
  // Save bbox to the store
271
- const transaction = this.database.transaction([this.bboxStoreName], 'readwrite');
243
+ const transaction = database.transaction([this.bboxStoreName], 'readwrite');
272
244
  const store = transaction.objectStore(this.bboxStoreName);
273
245
  const request = store.put(bbox);
274
246
  request.onsuccess = () => {
@@ -279,11 +251,9 @@ class OfflineManager extends GirafeSingleton {
279
251
  };
280
252
  }
281
253
  async displayBoundBoxes() {
282
- if (this.database === undefined) {
283
- this.database = await this.openIndexedDB();
284
- }
254
+ const database = await this.database();
285
255
  // Read bbox for offline tiles
286
- const transaction = this.database.transaction([this.bboxStoreName], 'readonly');
256
+ const transaction = database.transaction([this.bboxStoreName], 'readonly');
287
257
  const store = transaction.objectStore(this.bboxStoreName);
288
258
  const request = store.getAll();
289
259
  request.onsuccess = () => {
@@ -0,0 +1 @@
1
+ export {};