Skip to content

Conversation

@steff-o
Copy link
Contributor

@steff-o steff-o commented Apr 4, 2025

Fixes #1901 and fixes #1903

This is a draft PR for offline layers. It is draft as no PR accepted is given but we would like to have others to look at it. This PR contains two offline layer types: WMSOFFLINE and WFSOFFLINE. It also contains a Control that adds a new toolbar with tools to control offline functionality. This PR does not contain code for making an Origo application able to start up offline. That is given as a code example further down in this text. Most of the added functionality is added through the layer classes and the control, but some hooks are added in the editor for handling offline edits. This PR also removes the last remaining files from the old defunct offline implementation.

To test the control and layers, the application itself does not have to be offline capable, just add the control and a couple of offline layers. To be fair, the terminology should probably be "local layers", but since "offline" is well rooted, we'll stick to that. None of the layer types are sometimes online, sometimes offline, they always only use the local storage as source and have to preload some data before anything can be shown. This is because we think it will confuse the users if the layer behaves differently when switching between offline and online modes.

As it is not likely for anyone to preload an entire tiled map, e.g. orthophoto for en entire municipality as background map we recommend using a generalized vector map as background map. It can either be distributed as a bundled asset geojson or a WFSOFFLINE layer with the offlineUseLayerExtent option set to true. This will ignore the drawn rectangle for those layers, which makes it always download the entire extent, which is good for keeping the background map always up to date, but will slow each download down. Using a service-worker cached geojson as background will only be updated when the application "manifest" is updated, but will not slow down downloads. Obviously there is room for improvement here, layers could have a special background flag and be treated differently or the user could be able to select only which layers to download and sync.

WMSOFFLINE layers

Has the capability to download tiles from a WMS service, pretty much like a tiled WMS layer. The selection of area to download is done by using the download rectangle tool in the offline toolbar. Be aware that tiles are very bandwith consuming. To reduce bandwidth requirements, the zoom levels can be limited. Also be aware that it may put stress on the WMS server as it burst loads everything.

The layer uses a WmsOfflineSource class which is a specialized class of the abstract class ImageTileOfflineSource, which implements all offline handling. In this way it is easy to implement new tiled image layers by just implementing the code for how to fetch tiles.

WFSOFFLINE layers

Is the WFS equivalent of WMSOFFLINE. In addition it supports offline editing. Offline edits are handled just like ordinary edits, but are persisted in local storage when saved using the ordinary save button or autosave. By synchronizing edits using the offline toolbar's sync tool the locally stored edits are uploaded to the server. After the upload, all offline vector layers are re-read from the server to reflect the current state.

The WMSOFFLINE layer type is built on a specialized source class and an abstract base class that performs most of the functionality. To create new editable layer types only the code for fetching features and pushing edits have to be added and a layer factory function that handles the configuration.

The Offline control

The tool opens the offline toolbar which has five tools

Preload tool: The preload tool allows the user to draw an extent to download in the map. One or more extents can be downloaded.
each downloaded extent is fetched from the server and stored in the browsers persistent storage (indexdDb). When the tool is
active all previously downloaded extents are visible in the map. Before any extent is downloaded the offline layers will contain
no data.

Clear tool: The Clear tool removes all locally stored data for all layers and information about downloaded extents.

View ongoing downloads: As tiles can take some time to download that is done in the background. This tool displays progress
in a dialog. For the time being, it only displays progress for tiled layers as vector layers are usually much faster.

Go offline tool: The go offline tool toggles between offline and online operation of the application. It does not in fact affect
the connectivity in any way, it merely hides all layers that are not offline capable to avoid the map to be slow. If there is a backgroundlayer
tagged as offline it will activated. When toggled to online all visibility of all layers are restored to before offline mode was selected.
Which offline mode is active is persisted between sessions to support offline mode at startup.

Sync edits tool: The sync edits tool will upload locally saved edits to the server for all offline editable layers. Afterwards
all downloaded extents are re-read from the server for editable layers, even if there were no edits. This results
in a sync mechanism that treats all inserts to succeed, all edits win over server side edits regardless of which edit was first,
and all deletes to succeed. If an edited feature was already deleted on the server it is not resurrected, remaining deleted.

Property Description
none The control currently has no options

WFSOFFLINE Configuration

A WFSOFFLINE layer is a vector layer that uses a local storage in the client browser as source and does not require a network
connection.
The features are preloaded into the local storage from a WFS service. One or more disjoint or overlapping extents can preloaded.
Most options are the same as for WFS, but some additional
options have to be set.

WFSOFFLINE layers can be edited and the edits are saved to persistant local storage offline and can be synchronized with the WFS server
when network is available.

Preloading features and synchronization of edits can be performed using the Offline control or api functions on the layer's
source.

Property Description
abstract short description of the layer shown in the layer info. Optional.
attachments An attachment object containing configuration for editing and displaying attachments
allowedEditOperations List of available edit tools. Possible values are: updateAttributes, updateGeometry, create, delete. Only applies if layer is editable. Defaults to all. Optional.
attributes definition of attributes and how they should be presented in featureinfo. If not provided all available attributes will be shown with a standard template.
attribution attribution for the layer shown in the footer. Used for copyright text or any other information. Optional.
clusterOptions options for clustering. See the settings page for details.
clusterStyle the style to be used for clustered features. Is required if layerType cluster is used.
css Used for adding CSS properties to layer canvas element. Formatted as key/value pairs.
editable if the layer should be editable or not. Requires the editor control. Defaults to false. Optional.
exportable Adds a Export layer option to the layer info menu if set to true. To ensure all features in a layer is exported, strategy should be set to all. Optional.
exportFormat String or array of formats for file export if exportable is true. Can be set to geojson, gpx or kml. Defaults to geojson.
extent extent of the layer. Map extent is default.
featureinfoTitle attribute to be used instead of the title property as the title for the popup/sidebar. Optional.
filter filter provided as cql. Optional.
geometryName geometry attribute name. Default is geom.
geometryType geometry type for the layer.
id the id or ids used to identify the layers in the map server. White spaces and special characters should be avoided.
isTable Bool that indicates if the geometry should be ignored. Implies visible. Only useful when layer is a child in related layers. Optional. defaults to false
legend if the layer should be included in the map legend. Default is false.
group group the layer belong to. If group is not provided it will not be included in legend. Optional.
maxScale the maximum scale the layer is visible. Optional.
minScale the minmum scale the layer is visible. Optional.
name the unique name of the layer used internally and the name of the layer in the wfs service. White spaces and special characters should be avoided. To be able to reuse layers add after the layer name a double underscore plus a suffix to tell them apart.
offlineStoreName The name of the layer in local storage. Can be shared bewteen applications from the same domain using the same name. Name does not have to be pre-configured in local storage. Required.
offlineUseLayerExtent Ignores the preload extent and uses the layer's extent, or map extent if not set. Results in this layer only has one active extent. Boolean defaults to false. Optional.
opacity opacity of the layer. Value between 0 and 1. Default is 1.
opacityControl Adds an opacity slider in the legends extended layer info. Optional, defaults to true.
projection set projection (e g to "EPSG:4326") to request features in another reference system and Origo will handle the transformation. The proj4Defs has to be configured in index.json unless it's EPSG:4326 or 3857.
queryable if featureinfo should be enabled for the layer. Default is true.
relatedLayers Array of relatedLayers objects defining child layers. Optional
removable Adds a Remove layer option to the layer info menu if set to true. Optional.
requestMethod request method for this layer. Can be set to 'post', otherwise it will be 'get'. Default is 'get'.
searchable used with includeSearchableLayers in search control. Can be set to 'always', true (when visible) or false.
source named source of the layer. The source must be defined.
style the name of the referenced style to be used for the layer.
stylePicker Adds a dropup with alternative styles in the layer info. An array of styles defined with title, style and clusterStyle. See stylePicker. Optional.
thematicStyling Setting thematicStyling to true will add buttons to the different thematic styles to be able to turn them on or off. Optional, defaults to false.
title title for the layer visible to the user.
type type of source for the layer. For WFSOFFLINE source the type is WFSOFFLINE. Required.
visible if the layer should be visible. Default is false.
zoomToExtent Adds a Zoom To option to the layer info menu if set to true. Optional.

Source options

The following options are available for the source configuration for WFS.

Name Type Required Description
url string Yes Url to the wfs endpoint
requestMethod string No Request method for this source. Can be set to 'post', otherwise it will be 'get'. If set on layer level this option will be omitted. Default is 'get'.
clusterOptions string No Options for clustering. See the settings page for details.
workspace string Only when editing Name of the Wfs feature type namespace. Should match the configuration of the server.
prefix string Only when editing Prefix to add to Wfs transaction. Should match the configuration of the server.

WMSOFFLINE Configuration

A WMSOFFLINE layer is a tiled layer that uses a local storage in the client browser as source and does not require a network
connection.
The tiles are preloaded into the local storage from a WMS service. One or more disjoint or overlapping extents can preloaded.
Most options are the same as for WMS, but due to the offline nature
some options are not available. Preloading tiles can be performed using the Offline control or api functions on the layer's
source.

Beware that preloading tiles can be very bandwidth extensive even at small extents as all tiles on all zoom levels are
preloaded!

Property Description
abstract short description of the layer shown in the layer info. Optional.
attachments An attachment object containing configuration for displaying attachments
attributes definition of attributes and how they should be presented in featureinfo. If not provided all available attributes will be shown with a standard template.
attribution attribution for the layer shown in the footer. Used for copyright text or any other information. Optional.
clip Clip tiles against the preload extent to avoid tiles spill outside the preload extent as it may look confusing to have tiles outside the extent behaving differently at different zoom levels. Only works on GeoServer. Boolean Optional defaults to false.
compressionFactor Estimated compression factor for each tile. Used to estimate progress. Defaults to 0.1.
css Used for adding CSS properties to layer canvas element. Formatted as key/value pairs.
extent extent of the layer. Map extent is default.
format the image format used for the layer. Default is 'image/png' unless format is set for the source.
group group the layer belong to. If group is not provided it will not be included in legend. Optional.
gutter gutter setting for the layer. Default is 0.
hasThemeLegend Whether extendedLegend or not. See WMS autolegend. Optional, defaults to false. Has no effect if a style is also defined.
legend if the layer should be included in the map legend. Default is false.
legendParams A getLegendGraphic parameters object, see WMS autolegend. Optional, has no effect if a style is also defined.
maxScale the maximum scale the layer is visible. Optional.
minScale the minmum scale the layer is visible. Optional.
name the unique name of the layer used internally and the name of the layer in the WMS service. White spaces and special characters should be avoided. To be able to reuse layers add after the layer name a double underscore plus a suffix to tell them apart.
offlineMaxZoom Maximum zoom level to preload (int). Avoid preloading the most zoomed in levels. Optional.
offlineMinZoom Minimum zoom level to preload (int). Avoid preloading the most zoomed out levels. Optional.
offlineStoreName The name of the layer in local storage. Can be shared bewteen applications from the same domain using the same name. Name does not have to be pre-configured in local storage. Required.
opacity opacity of the layer. Value between 0 and 1. Default is 1.
opacityControl Adds an opacity slider in the legends extended layer info. Optional, defaults to true.
removable Adds a Remove layer option to the layer info menu if set to true. Optional.
queryable if featureinfo should be enabled for the layer. Default is true.
source named source of the layer. The source must be defined with the layers source options.
sourceParams A object with any additional params that can be added to the source and sent to the WMS server. For example CQL_FILTER can be provided as cql and there by filter on which objects should be included on a Geoserver layer. For a QGIS Server the param FILTER can be used in a similar maner, the syntax should be in OGC filter format. Other server specific params can also be set as DPI, BGCOLOR oc OPACITIES. Optional.
style the name of the referenced style to be used for the layer.
tileGrid custom tileGrid for the WMS layer. extent, alignBottomLeft, resolutions and tileSize can be set.
title title for the layer visible to the user.
type type of source for the layer. For WMSOFFLINE the type is WMSOFFLINE.
visible if the layer should be visible. Default is false.
zoomToExtent Adds a Zoom To option to the layer info menu if set to true. Optional.
Source options Description
format the image format used for the layer unless format is set on layer-level. Default is 'image/png'.
url url to the wms endpoint
version the OGC WMS version. Default is 1.1.1.
tileGrid custom tileGrid for the WMS source. extent, alignBottomLeft, resolutions and tileSize can be set.
type vendor of the WMS server. Used for functionality that requires different handling depending on the server type. Currently the options are 'Geoserver', 'ArcGIS', 'QGIS'. Optional.

Offline application

In order for the application itself to be able to start without network connection it must be stored locally in the browser at some point. This easiest accomplished by using a service worker. The code for bootstrapping an offline application is in two steps: first the service worker must be loaded and the the service worker must store the application and all its assets i a local cache. The first part could be bundled in the origo bundle, but we have chosen not to do it right now. The service worker itself must be a separate file and can thus not be bundled, but it could possibly be added as an static file. Problem is that it contains a hardcoded list of assets with versioning so that the assets can be loaded when updated. It can probably be parameterized, but we chose not to try that for now.

index.html

Code neccessary for offline in index.html:

<script src="js/origo.js"></script>
<script type="module" src="init-offline.js"></script>

init-offline.js

The init-offline.js file. This is not production ready code, but should work for most applications:

/**
 * When updating origo or any other service worker controlled resource, you need to update the service-worker.js version to make sure we trigger a update event
 */

/**
 *
 * @returns
 */

function createNotificationElement() {
  const notificationElement = document.createElement('div');
  notificationElement.innerHTML = `
        <div id="sw-notification" class="o-ui" style="position: absolute; top: 1rem; right: 5rem; width: 100%; max-width: 20rem; padding: 1rem; color: #6b7280; background-color: #ffffff; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);" role="alert">
                <div style="display: flex;">
                        <div style="display: flex; align-items: center; justify-content: center; flex-shrink: 0; width: 2rem; height: 2rem; color: #3b82f6; background-color: #dbeafe; border-radius: 0.5rem;">
                                <svg style="width: 1rem; height: 1rem;" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
                                        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97"/>
                                </svg>
                        </div>
                        <div style="margin-left: 0.75rem; line-height: 1.25rem;">
                                <span style="font-weight: 600; color: #111827;">Ny version tillgänglig</span>
                                <div style="margin-bottom: 0.5rem;">En ny version finns tillgänglig att ladda ner.</div> 
                                <div style="display: flex; justify-content: end;">
                                    <a href="#" style="padding: 0.375rem 0.5rem; text-align: center; font-size: 0.75rem; color: #ffffff; background-color: #3b82f6; border-radius: 0.5rem; text-decoration: none; cursor: pointer;">Uppdatera</a>    
                                </div>
                        </div>
                        <button type="button" style="border: 0 solid #e5e7eb; height: 2rem; width: 2rem; margin-left: auto; margin-right: -0.375rem; margin-top: -0.375rem; background-color: #ffffff; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; color: #9ca3af; border-radius: 0.5rem; padding: 0.375rem; cursor: pointer;" aria-label="Close">
                                <span style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0;">Close</span>
                                <svg style="width: 0.75rem; height: 0.75rem;" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
                                        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
                                </svg>
                        </button>
                </div>
        </div>
    `;
  document.body.appendChild(notificationElement);
  return notificationElement;
}

function invokeServiceWorkerUpdateFlow(registration) {
  // TODO: Replace with Origo floatingPanel.
  const notification = document.getElementById('sw-notification') ?? createNotificationElement();

  if (!notification) {
    return;
  }
  notification.style.display = 'block';
  // notification.show('New version of the app is available. Refresh now?');
  notification.addEventListener('click', (evt) => {
    evt.preventDefault();
    if (registration.waiting) {
      // let waiting Service Worker know it should became active
      registration.waiting.postMessage('SKIP_WAITING');
      notification.style.display = 'none';
    } else {
      // just reload the page if there is no waiting Service Worker
      window.location.reload();
    }
  });
}

const registerServiceWorker = async () => {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', async () => {
      try {
        const registration = await navigator.serviceWorker.register('service-worker.js');

        if (registration.installing) {
          console.log('Service Worker installing for the first time');
        } else if (registration.waiting) {
          console.log('We got a new Service Worker waiting to be activated');
          invokeServiceWorkerUpdateFlow(registration);
        } else if (registration.active) {
          console.log('The current Service worker is active');
        }

        registration.addEventListener('updatefound', () => {
          // TODO: Send up a toaster (Origo.logger) that a new version is downloading.
          // If user knows a new version is dowloading, they might wait and install it.
          // Normally the download time is short, but with bundled background maps and slow
          // connection this event may fire long before the update is available for install.
          console.log('We found a new Service Worker to update');
          if (registration.installing) {
            console.log('The new Service Worker is installing');
          }
          registration.installing.addEventListener('statechange', () => {
            console.log('[UPDATE] Service worker installing state change', registration);
            // if (registration.installing.state === 'installed') {
            //   console.log('[UPDATE] Service worker installed');
            //   //   invokeServiceWorkerUpdateFlow(registration);
            // } else
            if (registration.installing) {
              console.log('The new Service Worker is installing');
            } else if (registration.waiting) {
              console.log('The new Service Worker is waiting to be activated');
              // our new instance is now waiting for activation (its state is 'installed')
              // we now may invoke our update UX safely
              invokeServiceWorkerUpdateFlow(registration);
            } else {
              // apparently installation must have failed (SW state is 'redundant')
              // it makes no sense to think about this update any more
            }
          });
        });

        let refreshing = false;
        // The controllerchange event of the ServiceWorkerContainer interface fires when the Service Worker acquires a new active worker.
        // Force page reload to reload content
        navigator.serviceWorker.addEventListener('controllerchange', () => {
          console.log('[CHANGE] Active Service Worker have changed');

          if (!refreshing) {
            console.log('[CHANGE] Service worker refreshing');

            window.location.reload();
            refreshing = true;
          }
        });
      } catch (e) {
        console.error('Service Worker registration failed: ', e);
      }
    });
  }
};

registerServiceWorker();

service-worker.js

The service worker itself (service-worker.js). Also, this is not production ready code. At the bottom are all assets that must be available offline. This is taken from our demo application, which uses some geojson layers as offline background map, so they are cached by the service worker instead of making the user preload them manually. When an asset is updated the version number in this file should be bumped to make the service worker download the new version of the asset as it uses a cache-first approach.

/* eslint-disable no-restricted-globals */
const VERSION = 'v1.0.9';

/**
 * Add all provided resources to the cache
 * 
 * @param {string[]} resources list of resources to add to the cache. Provided with url string.
 */
const addResourcesToCache = async (resources) => {
  const cache = await caches.open(VERSION);
  console.log('Service Worker added all reasources to cache');

  // We fetch the resource using a unique URL to ensure that we get a newer version of the resource
  for (const url of resources) {
    const cacheUrl = new URL(url, location.href);
	  const urlToCache = new URL(url, location.href);
    cacheUrl.searchParams.set('version', VERSION);
    const response = await fetch(cacheUrl.href);
    console.log('Caching url', urlToCache);
    
    await cache.put(urlToCache.href, response);
  }
};

/**
 * Store the response in the cache
 * 
 * @param {*} request 
 * @param {*} response 
 */
const putInCache = async (request, response) => {
  const cache = await caches.open(VERSION);
  // TODO: Should only store the request if it is a request we want to store.

  console.log('putInCache', request.url);
  await cache.put(request, response);
};

/**
 * Enable navigation preload if it is available
 */
const enableNavigationPreload = async () => {
  if (self.registration.navigationPreload) {
    await self.registration.navigationPreload.enable();
  }
};

/**
 * Fetch the data from the cache first, then try to fetch from the network
 * 
 * @param {*} param0 
 * @returns 
 */
const cacheFirst = async ({ request, preloadResponsePromise, fallbackUrl }) => {
  // First try to get the resource from the cache
  const responseFromCache = await caches.match(request);
  if (responseFromCache) {
    console.log('Found match in cache', request.url);
    
    return responseFromCache;
  }

  // Next try to use (and cache) the preloaded response, if it's there
  const preloadResponse = await preloadResponsePromise;
  if (preloadResponse) {
    // putInCache(request, preloadResponse.clone());
    return preloadResponse;
  }

  // Next try to get the resource from the network
  try {
    const responseFromNetwork = await fetch(request);
    // putInCache(request, responseFromNetwork.clone());
    return responseFromNetwork;
  } catch (error) {
    const fallbackResponse = await caches.match(fallbackUrl);
    if (fallbackResponse) {
      return fallbackResponse;
    }
    // when even the fallback response is not available,
    // there is nothing we can do, but we must always
    // return a Response object
    return new Response('Network error happened', { status: 408, headers: { 'Content-Type': 'text/plain' } });
  }
};

/**
 * Delete key from cache
 * 
 * @param {*} key 
 */
const deleteCache = async (key) => {
  await caches.delete(key);
};

/**
 * Delete all caches except the current version
 */
const deleteOldCaches = async () => {
  const cacheKeepList = [VERSION];
  const keyList = await caches.keys();
  const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key));
  await Promise.all(cachesToDelete.map(deleteCache));
};
// Add event listners for the service worker
self.addEventListener('activate', (event) => {
  console.log('Service worker activating...', event);
  event.waitUntil(deleteOldCaches());
  event.waitUntil(enableNavigationPreload());
});

self.addEventListener('install', (event) => {
  console.log('Service worker installing...', event);

  // Files needed to cache to be able to run the app offline
  event.waitUntil(addResourcesToCache([
    './',
    'index.html',
    'css/style.css',
    'js/origo.js',
    'init-offline.js',
    'css/svg/fa-icons.svg',
    'css/svg/material-icons.svg',
    'css/svg/miscellaneous.svg',
    'css/svg/origo-icons.svg',
    'css/svg/custom.svg',
    'index.json',
    'img/png/logo.png',
    'img/png/osm.png', // Icon for OpenStreetMap in ledgend
    'img/png/drop_blue.png',
    'data/s250-hydrolinje.json',
    'data/s250-mark.json',
    'data/s250-textpunkt.json',
    'data/s250-vaglinje.json'
  ]));
});

self.addEventListener('fetch', (event) => {
  event.respondWith(cacheFirst({ request: event.request, preloadResponsePromise: event.preloadResponse, fallbackUrl: '/img/png/red.png' }));
});

self.addEventListener('message', (event) => {
  if (event.data === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

@steff-o steff-o marked this pull request as draft April 4, 2025 09:28
@steff-o
Copy link
Contributor Author

steff-o commented May 9, 2025

Finally ready for review! The offline layers and control is best tested without the mechanisms to start the application offline as described above. The changes in this PR does not depend on caching the application and its assets, as WMSOFFLINE and WFSOFFLINE depends solely on indexedDb. Adding offline support to the application only adds complexity in testing.

Most of the changes does not affect exitsting code as this PR mainly adds two new layertypes and a Control, but there are some changes to the old editstore and transactionhandler that potentially can affect normal edit operation (WFS).

To test this PR a bunch of new configuration is needed as described above in the first comment on this PR. The control needs to be added and a couple of WMSOFFLINE and WFSOFFLINE layers.

Documentation will likely be the layerconfig tables from comment above.

@steff-o steff-o marked this pull request as ready for review May 9, 2025 07:40
@steff-o
Copy link
Contributor Author

steff-o commented Aug 27, 2025

Resolved merge conflicts. Could someone please test this before it degenerates into a pile of non-mergeable garbage. It is only necessary to test that it doesn't break any previous functionality and that the new functionality doesn't introduce any unwanted design patterns. The actual functionality does not have to be tested completely as there are no existing applications that are depending on it.

@Grammostola
Copy link
Contributor

Grammostola commented Aug 29, 2025

I don't think we want to lose out on this bit of functionality.
I'll see if I can get it started, I may return with questions on that : )

Did note however that the loc string keys are in snake case.

Edit. An example config would have been appreciated but as I take it there are two mandatory (extra) props of a WFSOFFLINE layer, offlineStoreName and the type being WFSOFFLINE. And the example service-worker and init-offline are appreciated.

I tried the editor with the offline control loaded but without any WFSOFFLINE layers. Creating new features and editing existing worked as expected.
I tried turning said layer into an WFSOFFLINE layer with the proper type and offlineStoreName.

For some reason I had more success in Firefox, Chrome didn't see any layers to download when the Download rectangle action is taken from the Offline toolbar. But FF did and taking things offline, going offline via the toggle, then editing, then switching to online again and syncing worked well.

The message then was "Synchronized
Alla lokala redigeringar har synkroniserats" which seemed a bit mixed (this with the Swedish locale).

It's neat that already downloaded areas are shown when the action to download new areas is taken.

As for unwanted design patterns, well if someone else wants to take a stab then go for it. I didn't see anything immediately horrible in the edited files : )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edit features when offline Offline tile cache

3 participants