Documentation

5 Functionalities

5.1 Compatibility and support

5.1.1 Browser support

Oskari is compatible with the following browsers:

  • Chrome
  • Firefox
  • Microsoft Edge
  • Safari

5.1.2 Multi-screen support

The Oskari user interface supports touch screens and is accessible on mobile devices, including smartphones and tablets. However, there is currently no dedicated mobile or tablet-optimized version of Oskari (such as a mobile app). The geoportal is gradually being developed to work even better with smaller screens of mobile devices.

Oskari provides a Oskari.util.isMobile() helper function for developers to detect small screen or mobile device being used and bundles should use media queries or similar to make a reasonable effort into supporting smaller screens.

5.1.3 Multilingual support

Oskari supports localisation for the UI and currently includes the translations in English, Finnish and Swedish. Additional translations can be added quite easily if required.

In addition to the languages mentioned above, Oskari also includes partial translations for the following languages:

  • Dutch
  • Estonian
  • French
  • German
  • Icelandic
  • Italian
  • Norsk bokmål
  • Nynorsk
  • Slovakian
  • Slovenian
  • Spanish

Localization on server

Most of the localizations are in the frontend bundles, but there are some localizations on the server side as well. The server localization can be found under oskari-server messages.properties and the can be overridden with messages-ext.properties in applications. These localizations can be used in JSP files with the JSTL tags:

<spring:message code="logout" text="Logout" />

Localization in frontend

Localization files are provided by bundles in resources/locale folder under the bundle implementation (relative to the bundles index.js file). Using this convention allows the oskari-bundle and oskari-lazy-bundle Webpack loaders to optimize packaging the translations for a specific application to a single localization file per language and the files don't need to be referenced manually since the loaders search that folder automatically.

Note that older bundle.js based bundles can reference the localization files in bundle definitions (bundle.js - locales array).

Each languages localization is in its own file with structure like this:

Oskari.registerLocalization({
    // localization language
    "lang" : "en",
    // Bundle id or some other identifier which is used when retrieving this localization data
    "key" : "{{MyBundlesLocalizationKey}}",
    "value" : {
        // Actual localization data in custom structure, bundle using this data is responsible for interpreting the structure
        "title" : "Bundle title",
        "steps" : {
            "step1" : {
                "tab" : "Step 1",
                "title" : "Step 1 title",
                "content" : "{name}'s layer was created on {created, date}"
            },
            "step2" : {
                "tab" : "Step 2",
                "title" : "Step 2 title",
                "content" : "Remove {count, plural, one {# map layer} other {# map layers}}"
            },
            "step3" : {
                "tab" : "Step 3",
                "title" : "Step 3 title",
                "content" : "No interpolation here"
            }
        }
    }
});

The message strings are in ICU message syntax. Value names to be included into the string are in curly braces. Type (number, date or time) of the value can be given after the comma (default is string). The number/date/time will be formatted according to the locale rules (see steps.step1.content above). If the language has different forms for singular/plural, all forms must be defined (see steps.step2.content above). Note: some languages have multiple plural forms. It also possible to define different messages depending on value.

The language that will be returned is the one set with Oskari.setLang() function (usually done automatically before application start):

Oskari.setLang('en');
Formatting

To get a localized string in the current language:

Oskari.getMsg('<MyBundlesLocalizationKey>', '<path.to.message>', { key1: value1, key2: value2 });

Where keys are the ones used inside curly braces in the ICU format. From the example above:

Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step1.content', { name: 'Oscar', created: new Date() });
Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step2.content', { count: 4 });
Oskari.getMsg('<MyBundlesLocalizationKey>', 'steps.step3.content');

To avoid typing the bundles's localization key in every localization call, you can bind the first argument and create an instance variable for it:

Oskari.clazz.define(
    'Oskari.my.bundle.clazz',
    function () {
        this.loc = Oskari.getMsg.bind(null, '<MyBundlesLocalizationKey>');
    }, {
        someMethod: function() {
            this.loc('steps.step1.content', {name: 'Oscar', created: new Date()});
        }
    },
    ...

Overriding localization in application

Existing localization can be overridden in applications so they make more sense in the application context. An example of this could be overriding the info text for what the end-user can expect the search functionality to give as results. Usual way of doing this is making an application specific bundle with an id like lang-overrides (the id can be anything really) that includes localization files with overriding values for existing translations and import it in the application.

Oskari.registerLocalization({
    // localization language
    "lang" : "en",
    // An existing bundle localization key you want to override
    "key" : "{{MyBundlesLocalizationKey}}",
    "value" : {
        // Keys that you want to override, this can be a subset and only the provided ones are overridden
        "steps" : {
            "step1" : {
                "tab" : "Step 1 (Customized)",
                "content" : "The glorious map layer of user '{name}' was created on {created, date}"
            }
        }
    }
}, true);

The notable part is the second parameter for Oskari.registerLocalization() with true as the value will merge the object that is provided as the first parameter with an existing localization with the same localization key, overriding any localization values that are provided and using the existing ones where missing. If the parameter is missing or false (usually missing for regular localization files) the localization object is also merged to an existing one, but not overriding existing values. So it doesn't matter if the lang overrides are added before bundles that have the original texts or after them, as long as the second parameter for the overrides is true.

Using localization string in React component

Localizations can be provided with React context by importing Oskari LocaleProvider and having the React component tree as it's child elements like this:

import { LocaleProvider } from 'oskari-ui/util';
<LocaleProvider value={{ bundleKey: 'MyBundlesLocalizationKey' }}>
    ...
    <Message messageKey={'steps.step1.tab'}/>
    <Message messageKey={'steps.step1.content'} messageArgs={{ name: 'Oscar', created: new Date() }}/>
    ...
</LocaleProvider>
Deprecated old way

In some even older bundles the whole localization data tree is retrieved on runtime with a call:

var localization = Oskari.getLocalization('<MyBundlesLocalizationKey>');
this.showMessage(localization.steps.step1.tab);

This way does not support interpolation, pluralization or number/time formatting and therefore should not be used in new bundles.

5.2 Map window functionalities

The map window offers various functionalities, some of which are controlled with mouse, others as separate tools. Some of the functions can also be configured programatically.

The following are mouse-click functionalities for the end-user:

  • Panning map view
  • Zoom in by double clicking on the map
  • GetFeatureInfo (GFI) action with a single click on all map layers with GFI enabled, including WFS layers

The map window has separate tools to perform the following:

  • Restore the initial map state
  • A scale bar that updates with the map scale
  • Coordinate display that adapts according to configured coordinate system

Map window has functions that can configured programatically:

  • Coordinate system
  • Zoom bar (appearance and number of zoom levels)
  • Tile size and image format parameters

5.3 GetFeatureInfo

The GetFeatureInfo functionality is always active and starts by a single click / tap on the map. All GFI enabled map layers are queried on the single click. The response is shown in a popup dialog and can be styled using XSLT transformation (requires backend). Multiple response types are supported.

5.4 Map layers

Oskari supports various APIs for map layers: WMS, WMTS, WFS, ArcGIS Cache layers and ArcGIS REST feature layers. The map layers can be added by the admin - the instructions for this can be found from the Admin FAQ.

5.5 Map layer selector

The map layers can be accessed via map layer selector. The Map layer selector contains a list of all available map layers in the Oskari instance, grouped either by theme or by data provider. The map layer selector can be used to show map layers, filter them and to access their metadata. To view metadata there is link to the metadata display module, which accesses CSW interface.

Supported layer types are:

  • map layer
  • map layer stack
  • background map layer
  • WFS layer
  • thematic map layer
  • user's own data layer
  • time-enabled WMS layer (WMS-T)

If needed, new map layer types can be also added programmatically.

5.6 Selected map layers window

Oskari UI has a separate window for the selected map layers. Here are listed all the layers displayed in the map view.

The functionalities of Selected map layers window are:

  • Layers have localized titles (and optionally subtitles)
  • Layers can be temporarily hidden and shown
  • Layers can be organized in the layer stack by dragging and dropping
  • Layer opacity can be controlled using a slidebar or by entering opacity percentage
  • Layer metadata can be viewed (fetched for display using CSW interface)
  • WFS layers provide a link to feature data (referred to as the Feature property data in the admin panel)
  • WFS layer style can be changed on a temporary basis
  • GeoServer backed WMS layers can be visualized as heatmaps
  • For WMS layers the available styles can be viewed and selected for use

5.7 Map Legends (Flyouts)

The map legend for all the selected layers can be opened from the menu and it opens as a flyout. The flyouts can be moved around the map window and closed by the user or programmatically. The flyout size adapts to screen size and some flyouts can be resized by the user.

5.8 WFS layers and attribute data table functionalities (requires backend)

  • Complex schema WFS layers can be displayed
  • Attribute data from WFS layers can be displayed in an attribute table
  • Table view is synchronized with the map view: attribute rows are shown only for the features visible on the map
  • Columns to be shown can be selected
  • Data can be sorted ascending/descending
  • Features can be highlighted by clicking either on the map or in the table rows
  • Shift / Ctrl can be used to select multiple rows

The whole Oskari UI can be customized. This includes the menu on the side of the map window. The menu can be, for example, hidden and the individual menu entries can be configured individually.

Toolbar is anchored to the menu bar and contains various tools. Any functionality can add buttons to the toolbar and one can have an application-specific functionality that has a button on the toolbar.

The toolbar can be hidden and different buttons can be disabled and enabled individually. Tools can also be made to appear when a specific map layer is added to selected layers list.

The toolbar buttons include (by default/out-of-the-box):

  • Drag & pan button (enabled by default)
  • Rubber band zoom button
  • Restore initial map state button
  • Map view history manager: navigation with back and forward buttons
  • Measurement tools: measure distance and area
  • Link map view: creates an URL which opens the current map view
  • Marker tool: markers with associated text can be created on the map and sent as link
  • Print map view: starts the print mode
  • Save map view: map view configurations can be saved by logged-in user
  • Find nearest place tool: find the nearest place name by clicking on the map
    • The sample app includes OpenStreetMap-based Nominatim reverse geocoding search backend. This can changed to any backend that supports reverse geocoding.

Oskari has search functionality that can be configured to use custom search services as a backends to power the search. An implementation for OpenStreetMap nominatim service is included as an example adapter.

The location search is a one-field search which can return search results from multiple sources, such as placename, address, cadastral parcel or similar services. The service access parameters are configurable in the backend. If one needs to search for cadastral parcels or any other type of search result better suited to their application, a search backend that supports this can be configured.

5.10.1 Search channel

Describe the search channel concept.

5.10.2 WFS Search channels

There is an user interface for admin users that allows configuring WFS-services as backend for the search. This can be used to provide for example cadastral parcels as search results.

The search functionality in Oskari can be extended, customized and configured in a number of ways. Different datasources can be exposed as search channels in Oskari. Search channels can provide textual search, reverse geocoding or both and can have other parameters that are passed to the datasource. Search channel access can be restricted by permissions and the queried channels (datasources) can be specified when making a search. By default all the available search channels are used for making searches, but this can be configured and customized.

5.11.1 Configuring default search channels

Default channels are used for searching when no channel has been specified for the search. Search channels can specify if they should be included to be used in such queries. For channels extending and not overriding the SearchChannel.isDefaultChannel() method this can be configured with oskari-ext.properties (CHANNEL_ID is the annotated name of the channel like MyChannel):

search.channel.CHANNEL_ID.isDefault=false

There's also action route specific configurations for GetSearchResult (textual search) and GetReverseGeocodingResult (reverse geocoding) for default channels to use:

actionhandler.GetSearchResult.channels=[comma-separated list of channel ids]
actionhandler.GetReverseGeocodingResult.channels=[comma-separated list of channel ids]

These will setup searches made using these action routes to use the configured channels and as channels are specified any default channel settings are not used.

5.11.2 Whitelisting enabled search channels

If you want to whitelist search channels and use only relevant ones for your application you can define a list of channel id's in oskari-ext.properties:

search.channels=OPENSTREETMAP_CHANNEL,MyChannel

Channels generated by ChannelProvider components are not restricted by this setting. Provider channels are always added. Read more about ChannelProviders.

5.11.3 Things to improve (TODO)

  • Parallel search
  • Maybe add result SRS and have a common transformation so channels don't need to care about it
  • Logic for result ordering
  • Describe result items in more detail and make them more generic to support more properties

5.12 Creating a custom location search channel

All search channels need to implement the SearchableChannel interface from the file service-search/src/main/java/fi/nls/oskari/search/channel/SearchableChannel.java. The easiest way to add a custom search channel is to extend the class service-search/src/main/java/fi/nls/oskari/search/channel/SearchChannel.java and annotate the class with @Oskari("channelID"). This example offers a basic textual search implementation:

package fi.nls.oskari.search;

import fi.mml.portti.service.search.ChannelSearchResult;
import fi.mml.portti.service.search.SearchCriteria;
import fi.mml.portti.service.search.SearchResultItem;
import fi.nls.oskari.annotation.Oskari;
import fi.nls.oskari.search.channel.SearchChannel;
import fi.nls.oskari.util.IOHelper;

import java.io.IOException;

@Oskari("MyChannel")
public class CustomChannel extends SearchChannel {

    public ChannelSearchResult doSearch(SearchCriteria criteria) {
        ChannelSearchResult result = new ChannelSearchResult();
        try {
            // TODO: do the actual search
            final String responseData = IOHelper.getURL("https://www.google.fi/?q="
                    + criteria.getSearchString());
            // parse responseData and populate result with SearchResultItems
            SearchResultItem item = new SearchResultItem();
            item.setTitle("MySearchResult");
            result.addItem(item);
        }
        catch (IOException ex) {
            throw new RuntimeException("Error searching", ex);
        }
        return result;
    }
}

Once the class is in the servers classpath the search channel is available in searches as channel with id MyChannel. Note! The results location (coordinates) should be in the same projection as specified in the criteria!

5.12.1 SearchChannel methods to override

void init();

Any initialization should be performed here. Properties setup for example.

boolean hasPermission(User user)

Default implementation returns true for all users. Override if the datasource should only be available for some users.

Default search channels

Default channels are used for searching when no channel has been specified for the search. Search channels can specify if they should be included to be used in such queries. This can be done by returning a boolean value from the SearchChannel.isDefaultChannel() method. The default is true and can be configured with channel specific properties:

public boolean isDefaultChannel() {
    return PropertyUtil.getOptional("search.channel." + getName() + ".isDefault", true);
}

SearchableChannel.Capabilities getCapabilities();

Capabilities is an enum with values COORD, TEXT and BOTH. Defaults to TEXT on SearchChannel baseclass.

  • TEXT means the channel can be used to search with text.
  • COORD means the channel can be used to search with coordinates/reverse geocode.
  • BOTH means the channel implements both text and coordinate based searches.

boolean isValidSearchTerm(SearchCriteria criteria);

This method can be overridden to check whether the criteria makes sense in the context of the channel. Like is the searchtext in correct syntax for a cadastral parcel id etc.

ChannelSearchResult reverseGeocode(SearchCriteria criteria) throws IllegalSearchCriteriaException;

Implement this method if you want to use reverse geocoding for the channel. You will also need to override getCapabilities to return COORD or BOTH.

5.12.2 More examples

There is an example search channel for OpenStreetMap available in the file service-search-opendata/src/main/java/fi/nls/oskari/search/OpenStreetMapSearchService.java and several more in service-search-nls.

5.13 ChannelProvider customizations

Search channels can sometimes be generated based on some configuration and need to be created dynamically. One example of this in Oskari is configuring registered WFS-services as datasources for search (see WFSChannelProvider below).

Custom ChannelProvider

Creating a channel provider needs to extend fi.nls.oskari.search.channel.ChannelProvider and implement the getChannels() method. The method should return a set of SearchChannel objects.

package fi.nls.oskari.search.channel;

import fi.nls.oskari.annotation.Oskari;

import java.util.HashSet;
import java.util.Set;

@Oskari
public class WFSChannelProvider extends ChannelProvider {

    public Set getChannels() {
        Set channels = new HashSet<>();
        // TODO: populate set with SearchChannels
        return channels;
    }
}

The provider also implements a simple listener interface for observing channel changes. The search service registers as a listener to adjust usable search channels based on currently available ones as they can change at runtime. The provider class needs to be annoted with @Oskari for search service to find it in the classpath.

The search functionality should be notified of any runtime changes to channels the provider creates by calling the channelAdded() and channelRemoved() methods on the provider.

WFSChannelProvider

There is an admin bundle available (tampere/admin-wfs-search-channel) providing user interface for selecting a registered wfs-service and attribute(s) in that service to query against for results. The backend that is managing the WFS channels is implemented as a ChannelProvider. Check fi.nls.oskari.search.channel.WFSChannelProvider in oskari-server/service-search-wfs for the implementation. This reads the database for configurations based on registered WFS-services and creates a set of WFSSearchChannel objects to be used as datasources. These can be further customized by creating WFSChannelHandlers. WFSChannelHandler can be selected with database table oskari_wfs_search_channels column config with JSON value like this:

{
    "handler" : "ExampleHandlerID"
}
WFSChannelHandler

The WFSChannelHandlers can be used to modify the WFS-filter that is used when calling the service by overriding the createFilter() method.

package fi.nls.oskari.search.channel;

import fi.mml.portti.service.search.SearchCriteria;
import fi.nls.oskari.annotation.Oskari;
import fi.nls.oskari.wfs.WFSSearchChannelsConfiguration;

import java.util.List;

@Oskari("ExampleHandlerID")
public class ExampleHandler extends WFSChannelHandler {
    private Logger log = LogFactory.getLogger(this.getClass());

    public String createFilter(SearchCriteria sc, WFSSearchChannelsConfiguration config) {
        // custom filter handling
        String searchStr = sc.getSearchString();
        StringBuffer filter = new StringBuffer("{{Filter}}");
        // TODO: create filter contents
        filter.append("{{/Filter}}");
        return filter.toString().trim();
    }
}

5.14 WFS search functionality deployment

For the oskari-server make sure you have the dependency providing the functionality included in the maven pom.xml:


    fi.nls.oskari.service
    oskari-search-wfs
    ${oskari.version}

The frontend code for getting the administrative user interface is provided by admin-wfs-search-channel in the tampere namespace. You can add configure it as an admin bundles in oskari-ext.properties by adding it to the dynamic bundles list:

## bundles that are added on runtime to view if user has one of configured role
actionhandler.GetAppSetup.dynamic.bundles = ... any other bundles..., admin-wfs-search-channel

## Linking dynamic bundles based on user roles
## Properties are named 'actionhandler.GetAppSetup.dynamic.[BUNDLE ID].roles'
##   with value as comma-separated list of role names that should be served the bundle
actionhandler.GetAppSetup.dynamic.bundle.admin-wfs-search-channel.roles = Admin

This way the bundle will start for any users that have the role named Admin.

5.14.1 Customized search user interface

There is a more specific user interface for end-user searching that goes well with this functionality: search-from-channels in the tampere namespace. It provides an advanced options panel for the end-user to select which channels to use in a search. You can test it out by adding it to the dynamic bundles setup as with the admin above and if you like it you can add it to all the default/user views for the system in the database. You can use the Flyway-script below in your application specific flyway-module (replace V1_0_0 with the next available version):

package flyway.sample;

import fi.nls.oskari.util.FlywayHelper;
import org.flywaydb.core.api.migration.jdbc.JdbcMigration;

import java.sql.Connection;
import java.util.List;

/**
 * Adds search-from-channels bundle to default and user views.
 */
public class V1_0_0__add_search-from-channels_to_default_views implements JdbcMigration {
    private static final String BUNDLE_ID = "search-from-channels";

    public void migrate(Connection connection) throws Exception {
        final List views = FlywayHelper.getUserAndDefaultViewIds(connection);
        for(Long viewId : views){
            if (FlywayHelper.viewContainsBundle(connection, BUNDLE_ID, viewId)) {
                continue;
            }
            FlywayHelper.addBundleWithDefaults(connection, viewId, BUNDLE_ID);
        }
    }
}

More information about Flyway can be found in here. Or you can add it to a single view by manually running an SQL like this:

INSERT INTO oskari_appsetup_bundles (appsetup_id, seqno, bundle_id, bundleinstance, config, state)
   VALUES ([appsetup_id],
    (SELECT (max(seqno) + 1) FROM oskari_appsetup_bundles WHERE appsetup_id = [appsetup_id]),
    (SELECT id FROM oskari_bundle WHERE name = 'search-from-channels'),
    (SELECT name FROM oskari_bundle WHERE name = 'search-from-channels'),
    (SELECT config FROM oskari_bundle WHERE name = 'search-from-channels'),
    (SELECT state FROM oskari_bundle WHERE name = 'search-from-channels'));

By replacing [appsetup_id] with the id of the appsetup you want to use it in.

5.15 Metadata catalogue

Oskari can integrate with a metadata catalogue through a CSW-interface for both searching and viewing metadata for datasets.

The metadata search retrieves metadata from a CSW backend and offers advanced search options. Users can search for keywords within specific resource types, metadata languages, or layers published by selected organizations. After a successful search, the resulting layers can be viewed as a list or displayed on the map window.

5.16 Embedded maps (requires backend)

Describe publisher functionality etc

Create map mode is a tool for creating embedded map windows. The embeddable map window is shown in WYSIWYG mode. The embedded map has same basic functions as the main map.

Many of the tools available in the main map window are also available in the embedded map: scale bar, index map, zoom bar and search field. There are also map functions which can be enabled or disabled: panning by dragging, GetFeatureInfo. The size of the embedded map can also be configured. The created embedded maps are saved and can be later edited.

The embedded map parameters are name, website and language. The status of an embedded map can be either published or unpublished.

Embedded map features

  • Zooming and panning
  • Measure tools
  • GetFeatureInfo
  • WFS tabular data display
  • Map layer menu
  • Supported layer types
  • WMS
  • WMTS
  • WFS
  • My Data
  • Imported data
  • Thematic Maps and Tables
  • ArcGIS rest Feature Layer
  • Address, placename and real estate search
  • Customizable layout (colours and tool positioning)
  • Customizable size (preset size, fill space available)
  • Find my location
  • RPC API for interaction with the web site where the map is embedded
  • RPC API features are listed separately here: http://oskari.org/examples/rpc-api/

5.17 How to enable thematic maps

Oskari can display statistical data and link it to map layers creating thematic maps. Currently choropleth maps are supported for areal data. The statistical data are combined with the map layer data with an ID field that must match between the data. Supported data sources for statistical data are PX-Web, SDMX REST and Sotkanet REST, more support can be added via adapters written in Java. When the requested statistical data is multi-dimensional the user has to slice each dimension to reach a tabular form of data.

See requirements for using the thematic maps functionality in Oskari application.

See instructions for configuring the thematic maps functionality.

5.17.1 Requirements for thematic maps

Note! This functionality uses Redis heavily for caching so it needs to be available for the server to work properly.

Frontend

These are actions that enable the functionality to work on your apps.

Vector features support

The VectorLayerPlugin provides support for the adding vector features to the map and is required for thematic maps to work properly. The map visualization is done by adding regions for a region set as vector features to the map. The plugin needs to be started with mapfull bundles config referencing it in the plugins array:

{ "id" : "Oskari.mapframework.mapmodule.VectorLayerPlugin" }

The code for VectorLayerPlugin is included in the mapmodule and the sample application has it in the configuration so you shouldn't really need to do anything about it but if you don't see any regions on the map this can be the cause.

If you for some reason don't have you should do an application specific Flyway-migration to add the plugin to the appsetups that you need it in. See server section for details.

User-interface for thematic maps

For bundling in the user interface you will need to include the statsgrid bundle in your applications main.js.

If you always show it for users you should use this to include it on the application:

import 'oskari-loader!oskari-frontend/bundles/statistics/statsgrid/bundle.js';

OR if you want to show it to only some users you can reduce the amount of code all users need to download by using the "lazy loader". For example on the embedded map application you want to use this since most of the embedded maps don't usually include thematic maps:

import 'oskari-lazy-loader?statsgrid!oskari-frontend/bundles/statistics/statsgrid/bundle.js';

After modifying the main.js you will need to run npm run build to update the build artifacts in the dist/[version] folder.

After building a new version with the statsgrid bundle included you can test it out in the browser by running this on the developer console:

Oskari.app.playBundle({ bundlename : 'statsgrid' });

You should see the user-interface for the functionality show up on the geoportal page.

Server

Adding the functionality to geoportal user interface

After bundling in the functionality on the frontend build you will need to tell the server to start the functionality in the appsetups you want to use it with. Usually this is all appsetups of type USER and DEFAULT.

The actual thematic maps user interface is provided by another bundle: statsgrid that needs to be added to the geoportal views.

You can use a Flyway-migration to add it in your own server-extension:

package flyway.[your module];

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import org.oskari.helpers.AppSetupHelper;

/**
 * Adds statsgrid info bundle to all default/user appsetups
 */
public class V1_0_0__add_statsgrid_bundle extends BaseJavaMigration {

    public void migrate(Context context) throws Exception {
        AppSetupHelper.addBundleToApps(context.getConnection(), "statsgrid");
    }
}

Note that you need to change the package to match your applications module (in the sample-server-extension the module is called example and it's located here: https://github.com/oskariorg/sample-server-extension/tree/master/app-resources/src/main/java/flyway/example). You will also need to change the version number (in the class name) to match your application. Picking the next number from the current version is ok. If this is your first migration for the app you will also need to define the module to be activated in oskari-ext.properties:

db.additional.modules=myplaces, userlayer, [your module]

If you want to test is out before doing the Flyway-migration on the server side you can also add it to a single appsetup by running the below SQL. We recommend using Flyway-migration as you will most likely want to add it to any appsetups the user might have saved in addition to the default appsetup. Replace [appsetup_id] in the SQL below with the id of your default appsetup:

INSERT INTO oskari_appsetup_bundles (appsetup_id, seqno, bundle_id, config, state)
       VALUES ([appsetup_id],
        (SELECT (max(seqno) + 1) FROM oskari_appsetup_bundles WHERE appsetup_id = [appsetup_id]),
        (SELECT id FROM oskari_bundle WHERE name = 'statsgrid'),
        (SELECT config FROM oskari_bundle WHERE name = 'statsgrid'),
        (SELECT state FROM oskari_bundle WHERE name = 'statsgrid'));

Note! You don't need to add the bundle to publish-template or any embedded maps. If the user decides to publish a thematic map, the bundle is automatically added to the embedded maps bundles.

oskari-server/Maven dependencies

The server-side code for thematic maps are included by default on the sample application, but if you have an oskari-server-extension (like you propably should) make sure you have the dependencies included in the webapp-map/pom.xml:

<dependency>
    <groupId>org.oskari</groupId>
    <artifactId>control-statistics</artifactId>
    <version>${oskari.version}</version>
</dependency>
<!-- Statistics plugins -->
<dependency>
    <groupId>org.oskari</groupId>
    <artifactId>service-statistics-pxweb</artifactId>
    <version>${oskari.version}</version>
</dependency>
<dependency>
    <groupId>org.oskari</groupId>
    <artifactId>service-statistics-unsd</artifactId>
    <version>${oskari.version}</version>
</dependency>
<!-- /Statistics plugins -->

Take a look at the sample webapp-map for details: https://github.com/oskariorg/sample-server-extension/blob/1.4.1/webapp-map/pom.xml#L56-L69. There might be additional adapters available also. Take a look at the folders under oskari-server starting with 'service-statistics-'.

5.17.2 Configuring thematic maps functionality

See requirements for enabling the code that powers the thematic maps functionality.

Adding regionsets as maplayers

Regionsets used for thematic maps are configured in pretty much the same way you might register a WFS-layer for Oskari database:

INSERT INTO oskari_maplayer(type, url,
                    name, dataprovider_id,
                    locale,
                    attributes, internal, srs_name)
VALUES(
    'statslayer', 'http://mydomain.com/geoserver/wfs',
    'mylayer', (SELECT MAX(id) FROM oskari_dataprovider),
    '{ "en" : {
        "name":"Municipalities"
    }}',
    '{
        "statistics" : {
            "regionIdTag":"id",
            "nameIdTag":"name"
        }
    }', true, 'EPSG:4326');

Where

  • type of the layer is 'statslayer'

  • the url and name should match a layer in a WFS-service

  • locale is a JSON object with language code at the first level and UI-name of the layer/regionset as the name value. It supports multiple languages as any other maplayers in Oskari.

  • dataprovider_id is a link to the data provider/organization that provides the layer

  • additional configuration for the attributes column are:

    • regionIdTag: feature attribute that is the unique id for that region (like municipality/postal/zip code). This is used to map statistics data to a region
    • nameIdTag: feature attribute that has the name for the region (this is shown to the end-user as the region name)
    • featuresUrl: (deprecated) URL for corresponding WFS-service. It's used to read all the features to create a list of regions in the region set (this URL can be used to override the actual url-field. It was useful when we used both wms and wfs layers, but now it's considered deprecated and only the main url should be used.)

Note! Add the view permissions for the layer so users can see it.

Regionset as JSON resource

As of 1.46.0 Oskari version regionsets don't need to come from a WFS-service and having them as resource files under the webserver works as well:

1. Make your GeoJSON resource file available for the webapp container.

Store the file to the root resource directory for your web application (for example $JETTY_HOME/resources). When adding a resource file as a regionset layer configure featuresUrl in layer attributes as follows:

"resources://${path}"

Where ${path} is relative to the root resource directory in your web application. For example having the file in $JETTY_HOME/resources/regionsets/myfile.json would mean featuresUrl value of "resources://regionsets/myfile.json"

Note! The featuresUrl must start with "resources://" for the system to recognize that this layer is resource based and the file-extension MUST be '.json'.

2. The features describing the Regions in your GeoJSON resource need to have atleast two properties.

  • One describing the id of the Region (the property name is configured as regionIdTag like in WFS-based config)
  • Another describing the name of the Region (the property name is configured as nameIdTag like in WFS-based config)

Note! It's expected that the geometry in GeoJSON files is in the projection referenced in the srs_name column on oskari_maplayer for the regionset.

Adding a datasource

A datasource can be registered with a simple SQL:

INSERT INTO oskari_statistical_datasource(locale, config, plugin)
VALUES('{
    "en" : {
        "name":"Health and Welfare"
    }}',
    '{
        "url" : "http://www.sotkanet.fi/rest"
    }', 'SotkaNET');

Where:

  • locale is a JSON with language code at the first level and UI-name of the datasource as the name value. It supports multiple languages like maplayers in Oskari.
  • config is an adapter specific configuration that is used to give the adapter code hints how to process the datasource
  • plugin is the ID for the adapter code to use for this datasource

Config can also include additional info about datasource and hints for sorting indicator data dimension values to be shown to user:

    {
      "info" : {
        "url" : "https://moreinfo.here"
      },
      "hints" : {
        "dimensions" : [ {
          "id" : "year",
          "sort" : "DESC"
        }, {
          "id" : "gender",
          "default" : "total"
        }]
      }
    }

Where id value will match the id of a data dimension item in indicator datamodel. Other keys affect the order of allowed values for that dimension. Sort (if present) will be done first with either DESC or ASC value. If default is present the matching allowed value will be moved as the first value in allowed values. You can use both, one or none.

The info-block is sent to the frontend code as is and the url (if provided) is shown as part of the attribution data for selected indicators.

Plugins/adapters

Plugins or adapters are used to interpret the statistics data API to a common format recognized by the Oskari frontend and map the statistics data to regions. There are a few API adapters available in Oskari and the oskari-server can be easily extended with additional adapters.

Eurostat

Eurostat is the statistical office of the European Union situated in Luxembourg. Its mission is to provide high quality statistics for Europe. They offer an API that uses SDMX and JSON-stat as dataformats.

Code: https://github.com/oskariorg/oskari-server/blob/develop/service-statistics-eurostat/src/main/java/fi/nls/oskari/statistics/eurostat/EurostatStatisticalDatasourceFactory.java

Datasource config:

{
    "url" : "http://ec.europa.eu/eurostat"
}
PxWeb

PxWeb is a widely used statistics software that offers an API for accessing the data.

Code: https://github.com/oskariorg/oskari-server/blob/develop/service-statistics-pxweb/src/main/java/fi/nls/oskari/control/statistics/plugins/pxweb/PxwebStatisticalDatasourceFactory.java

Datasource config:

{
    "url" : "http://some.pxweb.com/statdb",
    "regionKey" : "name of the attribute for the region id in stats data",
    "ignoredVariables": ["optional config", "any", "attributes", "that", "should", "be", "ignored"],
    "timeVariable": "Optional config for id of the variable that describes time like 'year'. This is used for time-series functionality.",
    "metadataFile": "/file/in/classpath.json (optional)"
}

The metadataFile configuration allows linking more metadata for indicators in the datasource like source for data, name/descriptions overrides for values from the API and configuring the type of data for selecting if it will be visualized as choropleth or point symbols by default etc. The value should point to a file in the server classpath. The format of the JSON is an array with objects like:

[{
    "code": "M408",
    "desc": {
        "fi": "Taajama-aste tarkoittaa taajamissa asuvien osuutta väestöstä, jonka sijainti tunnetaan. Taajamaksi määritellään kaikki vähintään 200 asukkaan rakennusryhmät, joissa rakennusten välinen etäisyys ei yleensä ole 200 metriä suurempi"
    },
    "source": {
        "fi": "Väestörakenne"
    },
    "isRatio": true,
    "base": 100,
    "min": -100,
    "max": 500,
    "decimalCount": 1,
    "timerange": {
        "start": "1987",
        "end": "2015"
    },
    "updated": "1.4.2016",
    "nextUpdate": "29.3.2017"
}, ...]
  • code value is used to map the indicator between the metadata JSON and data from the PxWeb API and id should match the indicator id. This is used to map the metadata to the indicator. Everything else is optional.
  • timerange can be used to configure indicator specific timeranges if they are all in the same .px file.
  • updated and nextUpdate are shown in the UI with the indicator description.
  • isRatio (optional boolean): true for choropleth, false for points
  • base (optional number): with number value the classification distribution is set to Diverging. Missing or non-numeric value distribution defaults to Quantitative
  • min and max describe the data but is not used by the frontend currently
SotkaNET

The Sotkanet Indicator Bank is an API provided by Finnish National Institute for Health and Welfare (THL). It is also used by National Land Survey of Finland to share statistics.

Code: https://github.com/oskariorg/oskari-server/blob/develop/service-statistics-sotka/src/main/java/fi/nls/oskari/control/statistics/plugins/sotka/SotkaStatisticalDatasourceFactory.java

Datasource config:

{
    "url" : "http://www.sotkanet.fi/rest",
    "timeVariable": "Optional config for id of the variable that describes time like 'year'. This is used for time-series functionality. Defaults to 'year' for SotkaNET if not configured."
}

Linking datasources and regionsets

Not all datasources have data for all of the regionsets so as the last step you need to link layers/regionsets that can be used with a given datasource.

INSERT INTO
    oskari_statistical_datasource_regionsets(datasource_id, layer_id, config)
VALUES(
    (SELECT id FROM oskari_statistical_datasource
        WHERE locale like '%Health and Welfare%'),
    (SELECT id FROM oskari_maplayer WHERE type='statslayer' AND name = 'mylayer'),
    '{}');

The config is an adapter specific configuration that can be used to pass information datasource specific information for the layer.

The current layer configuration options are listed below.

Eurostat

Doesn't use layer config, but could be used to detect which layer is used for which type of areas (NUTS 1,2,3).

PxWeb

Doesn't use layer config, but could be used to detect which layer is used for which type of areas in the statistics data or provide a fully qualified class name for doing the layer/statistics data mapping for more involved functionality.

SotkaNET

Uses config:

{
    "regionType" : "kunta"
}

The value of "regionType" should match the "category" value (like "kunta") in Sotkanet regions response (https://sotkanet.fi/rest/1.1/regions). The regionType value is case-insensitive. It's used to filter out indicators that the service has, but which don't have a regionset in the Oskari instance and as such can't be visualized in Oskari. Sotkanet data responses include data for all the regionsets and the same config is used to filter the data before it's passed to the frontend in Oskari.

5.17.3 Known issues

  • performance is not as good as it should before data is cached.

  • database configuration is cached on server startup so any changes to database for the statistics functionality will require a server restart. This includes for example regions in regionsets and datasources.

  • There is no user interface to configure datasources so they need to be administrated with SQL

The users of Oskari can print out what they have in the map window.

The print mode functionalities include:

  • Automatic Print preview window
  • Paper size and layout selection
  • Printout format selection: PNG or PDF, also PDF/A
  • Title, scalebar, date and a logo can be added, also text and markers of user's choice

5.19 User management

Some Oskari modules utilize role-based user management, allowing users with different roles to access specific functionalities. For example, only logged-in users can view certain layers or spesific user groups can add or edit map layers within the service. The spring security framework that Oskari uses can be customized to different use cases.

5.19.1 How to use user authentication

Users in Oskari are described with a few attributes listed below:

User table Role table

The datasource for users can be configured to read and manage users using JSON, SAML etc, but default to the core database for Oskari. The Java-interface for managing users is fi.nls.oskari.service.UserService under the service-base Maven module with fi.nls.oskari.user.DatabaseUserService under service-users Maven module as the reference implementation.

Permissions for resources are mapped using roles and user-specific content uses the UUID to link the content with the user.

TODO: description/use cases for UserService implementations.

5.20 Role-based authorization

Access to different functionalities and data can be granted with user roles.

5.21 Map layer permissions in the database

The maven module under service-permissions provides a generic permission handling database. Two db tables (oskari_resource and oskari_resource_permission) are used to mostly control access permissions to map layers, but the implementation is generic and could be used to control bundle access etc.

5.21.1 Example: grant view permission for maplayer

We want to grant access for guest users to see a maplayer:

  • assume guests have role with id 10110 (configurable in UserService implementation)

  • assume layer id is 2

The maplayer needs to be registered as a resource for permissions db (since the permission implementation is generic).

oskari_resource

id resource_type resource_mapping
1 'maplayer' '2'
  • resource_type is a string reference to another table for example in this case oskari_maplayer.
  • resource_mapping is a table specific reference to the resource in question. For maplayers it's the layer id as a string.

After the layer has been added as a resouce we can use it to set the actual permissions for that resource.

oskari_resource_permission

id resource_id permission external_type external_id
1 1 'VIEW_LAYER' 'ROLE' '10110'
  • resource_id is reference to a row in oskari_resource table

  • permission is a constant value of the type of permission in question

  • external_type is a reference link for the external_id. Usually permissions are mapped for user roles so you the value is then ROLE which means external_id is a role id. Another possible external_type is USER which means external_id is a user id, but this isn't widely used or might not be supported in all cases. Recommented use for now is to only use ROLE mappings.

Permission types

  • VIEW_LAYER: Permission to view a layer on map and geoportal listing
  • PUBLISH: Permission to use the maplayer as a layer when creating an embedded map
  • VIEW_PUBLISHED: Permission to view a layer in a published/embedded maps/links, but it is not listed on geoportal
  • DOWNLOAD: Permission to download a vector layer properties table as a CSV/Excel file from the geoportal
  • EDIT_LAYER: Permission to modify a layer content (not enabled by default, but content-editor functionality uses this)

Example: grant permission to add layers (for non-admin user role)

Adding layers is currently a generic permission (not mapped to data producer or similar). We need to add a resource for "generic-functionality".

oskari_resource
id resource_type resource_mapping
2 Bundle generic-functionality
oskari_resource_permission
id oskari_resource_id permission external_type external_id
3 2 ADD_MAPLAYER ROLE 10110

Admin user interface for layer permissions

There is an admin-permissions bundle with which admins can set layers' permissions. Check adding bundles for how to add the bundle. With that bundle, admin users can set permissions based on user roles for any layers through the user interface.

5.22 User self registration

The oskari-server repository has module control-users for allowing end-user self registration.

User registration

Oskari includes a simple user registration functionality that needs to be activated by configuration (see details below). This results in a "register" link being shown under the login form on the default geoportal-page and also enables the registration backend to accept requests.

The registration pages can be customized like any other JSP-file in Oskari and the emails sent by Oskari can also be customized.

Registration form

The first step of registration is just asking for the users email. This will always respond by saying an email has been sent to the address. The only error it will show if the email is not valid or if the server can detect that the mail was not sent.

Registration form

The mail the user receives has two options:

  • if the email is already registered to an existing user - a message with password reminder link is sent.
  • if the email is new the user receives a message with a link that can be used to access a form for user details.
Completing the user profile

Once the user clicks on the email link the token included in the link is validated. If the link token has expired or is unknown the first step of registration is shown with a message about expired link. The user can start the registration from the beginning.

On most cases the user is presented with a form to complete the registration:

Complete user profile

As usernames are used to link the user password to user details it needs to be unique. This is checked while the user is filling the form. The password strength requirements are based on the server configuration. They can be customized by configuration.

After the user has completed the profile the user can log in to the geoportal.

Password reset

Users can request a link for setting a new password for the account:

Request reset

For logged in users the email is pre-filled. For guest users the mail the user receives has two options:

  • if the email is new the user receives a message saying that there is no account for the email and a link to the registration page.
  • if the email is already registered to an existing user - a link to set the password is sent:

Password change

Modify user details

Currently only the name can be changed as changing the email would require a mail to be sent to check the email and usernames are problematic since they are used to map passwords to users.

Modify user

Configuration options

User registration for Oskari can be enabled by modifying oskari-ext.properties:

allow.registration=true
oskari.email.sender=<sender@domain.com>
oskari.email.host=<smtp.domain.com>

The password requirement can be configured with:

# min length for user password
user.passwd.length=8
# Require lower and UPPER chars
user.passwd.case=true
# Number of days that registration/passwd recover links are valid
oskari.email.link.expirytime=2

You will also need to include the control-users Maven module as a dependency on your application:

    <dependency>
        <groupId>org.oskari</groupId>
        <artifactId>control-users</artifactId>
    </dependency>

The functionality is mostly contained under oskari-server/control-users with JSPs that can be overridden in sample-server-extension using the same filename as the original. There are also localizations for the user registration that can be overridden with locale/messages-ext.properties files in the server classpath (like {tomcat.base}/lib/locale/messages-ext.properties).

To customize email-templates configure oskari-ext.properties (add files in classpath for example under {tomcat.base}/lib/templates):

# defaults
# on registration init
oskari.email.registration.tpl=/templates/registration_email.html
# on registration init if there's already a user account with the email
oskari.email.exists.tpl=/templates/registration_email_exists.html
# on "forgot my password"
oskari.email.passwordrecovery.tpl=/templates/user_passwordreset_email.html
# on "forgot my password" when there's no user account associated with the email
oskari.email.passwordrecovery.noaccount.tpl=/templates/user_passwordreset_email_new_user.html

# you can specify localized versions by adding the language code at the end of the property key
oskari.email.registration.tpl.fi=/templates/registration_email_finnish_version.html 

The default templates are stored in control-users/src/main/resources/fi/nls/oskari/control/users/service. The templates receive variables for:

  • URL to continue the process (link_to_continue)
  • number of days before the token expires (days_to_expire)

Tokens with expiration dates are tracked in the oskari_users_pending database table. If an email has an existing token it will be replaced with a new one with new expiration date so one email can only have one token at a time. This means that for example registration links will automatically expire if the user uses the password reminder. Emails and usernames are checked in case-insensitive fashion.

Note! User registration has been tested/implemented only for the case where users are in the Oskari database. Not for SAML logins etc.

Known issues

If you have problems having the mail sent first try to send a mail fron the server with telnet:

telnet [mail server] [port]
MAIL FROM: noreply@mysite.org
RCPT TO: someuser@somedomain.com

From terminal it might look like this:

[user@server ~]$ telnet [mail server] 25
Trying [mail server]...
Connected to [mail server].
Escape character is '^]'.
220 [mail server] ESMTP Postfix (Ubuntu)
MAIL FROM: noreply@mysite.org
250 2.1.0 Ok
RCPT TO: someuser@somedomain.com
454 4.7.1 <someuser@somedomain.com>: Relay access denied

In this case the mail server doesn't allow mails to be sent to the recipient and there's nothing you can do but ask the hosting party to loosen the restrictions. The good news is that the connection to the server was successful so there's no firewall blocking the connection.

If everything is good to go with the mail server and you still have problems sending mails you should check the Oskari logs for more information about the issue.

On one instance adding a javax.mail implementation manually was required, but it should be included in the Oskari download packaging. If this is the case you can add the jar-file to {tomcat.base}/lib/ folder (http://mvnrepository.com/artifact/com.sun.mail/javax.mail/1.5.4).

5.23 Create data using My Data (requires backend)

The end-user of Oskari instance can create their own spatial data in the main map window. For this purpose, Oskari supports vector data, which means that points, lines and areas can be created and saved. Multigeometries and creation of holes is also supported.

The users can:

  • Create multiple layers
  • Configure the symbology for each layer separately
  • Edit the description of map view and add links to external resources

5.24 Import data (requires backend)

Users can import their own datasets to Oskari as zipped files. Supported formats are:

  • Shapefile
  • Mapinfo MID/MIF
  • GPX trace
  • KMX (zipped KML)

5.24.1 User removal

User content (myplaces, saved views, embedded maps, userlayers, indicators) is removed from the database with the user. The content removal is done programmatically by searching for instances of UserContentServices with @Oskari annotation. You can search the oskari-server codebase for examples of this if you need to add additional cleanup for user removal.

Note! Removing a user from the database directly will not remove all content related to the user!