Skip to content
Snippets Groups Projects
index.js 16.5 KiB
Newer Older
import { of } from 'rxjs'
import { ajax } from 'rxjs/ajax'
esikkala's avatar
esikkala committed
import axios from 'axios'
import {
  mergeMap,
  switchMap,
  map,
  withLatestFrom,
  debounceTime,
  catchError
} from 'rxjs/operators'
import { combineEpics, ofType } from 'redux-observable'
import intl from 'react-intl-universal'
import { stateToUrl, pickSelectedDatasets } from '../helpers/helpers'
esikkala's avatar
esikkala committed
import querystring from 'querystring'
Esko Ikkala's avatar
Esko Ikkala committed
import {
esikkala's avatar
esikkala committed
  FETCH_RESULT_COUNT,
esikkala's avatar
esikkala committed
  FETCH_RESULT_COUNT_FAILED,
  FETCH_PAGINATED_RESULTS,
esikkala's avatar
esikkala committed
  FETCH_PAGINATED_RESULTS_FAILED,
  FETCH_RESULTS,
  FETCH_INSTANCE_ANALYSIS,
  FETCH_FULL_TEXT_RESULTS,
esikkala's avatar
esikkala committed
  FETCH_RESULTS_FAILED,
  FETCH_BY_URI,
esikkala's avatar
esikkala committed
  FETCH_BY_URI_FAILED,
  FETCH_FACET,
  FETCH_FACET_CONSTRAIN_SELF,
  FETCH_SIMILAR_DOCUMENTS_BY_ID,
  FETCH_SIMILAR_DOCUMENTS_BY_ID_FAILED,
esikkala's avatar
esikkala committed
  FETCH_FACET_FAILED,
esikkala's avatar
esikkala committed
  FETCH_GEOJSON_LAYERS,
  FETCH_GEOJSON_LAYERS_BACKEND,
  FETCH_KNOWLEDGE_GRAPH_METADATA,
  FETCH_KNOWLEDGE_GRAPH_METADATA_FAILED,
  CLIENT_FS_FETCH_RESULTS,
  CLIENT_FS_FETCH_RESULTS_FAILED,
esikkala's avatar
esikkala committed
  updateResultCount,
esikkala's avatar
esikkala committed
  updatePaginatedResults,
esikkala's avatar
esikkala committed
  updateResults,
  clientFSUpdateResults,
esikkala's avatar
esikkala committed
  updateInstanceTable,
  updateInstanceTableExternal,
  updateInstanceAnalysisData,
  updateFacetValues,
  updateFacetValuesConstrainSelf,
esikkala's avatar
esikkala committed
  updateLocale,
  updateGeoJSONLayers,
  updateKnowledgeGraphMetadata,
  fetchGeoJSONLayersFailed
} from '../actions'
import portalConfig from '../../configs/portalConfig.json'
const { portalID, localeConfig, documentFinderConfig } = portalConfig
const { documentFinderAPIUrl } = documentFinderConfig
esikkala's avatar
esikkala committed
export const availableLocales = {}
for (const locale of localeConfig.availableLocales) {
  let localeObj
  if (locale.format && locale.format === 'js') {
    const localeModule = await import(`../translations/${portalID}/${locale.filename}`)
    localeObj = localeModule.default
  } else {
    localeObj = await import(`../translations/${portalID}/${locale.filename}`)
  }
  availableLocales[locale.id] = localeObj
esikkala's avatar
esikkala committed
}
esikkala's avatar
esikkala committed

/*
* Note that all code inside the 'client' folder runs on the browser, so there is no 'process' object as in Node.js.
* Instead, the variable 'process.env.API_URL' is defined in 'webpack.client.common.js'.
*/
const apiUrl = process.env.API_URL
let backendErrorText = null

const fetchPaginatedResultsEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_PAGINATED_RESULTS),
  withLatestFrom(state$),
  mergeMap(([action, state]) => {
    const { resultClass, facetClass, sortBy } = action
    const { page, pagesize, sortDirection } = state[resultClass]
    const params = stateToUrl({
      facets: state[`${facetClass}Facets`].facets,
      facetClass: null,
      page,
      pagesize,
      sortBy,
      sortDirection
    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/paginated`
    // https://rxjs-dev.firebaseapp.com/api/ajax/ajax
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse =>
        updatePaginatedResults({
esikkala's avatar
esikkala committed
          resultClass,
          data: ajaxResponse.response.data,
          sparqlQuery: ajaxResponse.response.sparqlQuery
        })),
      // https://redux-observable.js.org/docs/recipes/ErrorHandling.html
esikkala's avatar
esikkala committed
      catchError(error => of({
        type: FETCH_PAGINATED_RESULTS_FAILED,
esikkala's avatar
esikkala committed
        resultClass,
esikkala's avatar
esikkala committed
        error: error,
esikkala's avatar
esikkala committed
        message: {
esikkala's avatar
esikkala committed
          text: backendErrorText,
          title: 'Error'
esikkala's avatar
esikkala committed
        }
      }))
const fetchResultsEpic = (action$, state$) => action$.pipe(
esikkala's avatar
esikkala committed
  ofType(FETCH_RESULTS),
esikkala's avatar
esikkala committed
  mergeMap(([action, state]) => {
    const { perspectiveID, resultClass, facetClass, limit, optimize } = action
    const params = stateToUrl({
      perspectiveID,
      facets: facetClass ? state[`${facetClass}Facets`].facets : null,
esikkala's avatar
esikkala committed
      facetClass,
      uri: action.uri ? action.uri : null,
esikkala's avatar
esikkala committed
      limit,
      optimize
    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/all`
    // https://rxjs-dev.firebaseapp.com/api/ajax/ajax
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse => updateResults({
esikkala's avatar
esikkala committed
        resultClass,
        data: ajaxResponse.response.data,
        sparqlQuery: ajaxResponse.response.sparqlQuery
esikkala's avatar
esikkala committed
      })),
esikkala's avatar
esikkala committed
      catchError(error => of({
        type: FETCH_RESULTS_FAILED,
esikkala's avatar
esikkala committed
        resultClass,
esikkala's avatar
esikkala committed
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
Esko Ikkala's avatar
Esko Ikkala committed
  })
const fetchInstanceAnalysisEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_INSTANCE_ANALYSIS),
  withLatestFrom(state$),
  mergeMap(([action, state]) => {
    const { resultClass, facetClass, fromID, toID, period, province } = action
    const params = stateToUrl({
      facets: facetClass ? state[`${facetClass}Facets`].facets : null,
      facetClass,
      uri: action.uri ? action.uri : null,
      fromID,
    })
    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/all`
    // https://rxjs-dev.firebaseapp.com/api/ajax/ajax
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse => updateInstanceAnalysisData({
        resultClass,
        data: ajaxResponse.response.data,
        sparqlQuery: ajaxResponse.response.sparqlQuery
      })),
      catchError(error => of({
        type: FETCH_RESULTS_FAILED,
        resultClass,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
    )
  })
)

esikkala's avatar
esikkala committed
const fetchResultCountEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_RESULT_COUNT),
  withLatestFrom(state$),
  mergeMap(([action, state]) => {
    const { resultClass, facetClass } = action
esikkala's avatar
esikkala committed
    const params = stateToUrl({
      facets: state[`${facetClass}Facets`].facets
    const requestUrl = `${apiUrl}/faceted-search/${resultClass}/count`
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse => updateResultCount({
esikkala's avatar
esikkala committed
        resultClass,
        data: ajaxResponse.response.data,
        sparqlQuery: ajaxResponse.response.sparqlQuery
esikkala's avatar
esikkala committed
      })),
esikkala's avatar
esikkala committed
      catchError(error => of({
esikkala's avatar
esikkala committed
        type: FETCH_RESULT_COUNT_FAILED,
esikkala's avatar
esikkala committed
        resultClass,
esikkala's avatar
esikkala committed
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
esikkala's avatar
esikkala committed
  })
const fullTextSearchEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_FULL_TEXT_RESULTS),
  withLatestFrom(state$),
  debounceTime(500),
  switchMap(([action, state]) => {
    const requestUrl = `${apiUrl}/full-text-search?q=${action.query}`
    return ajax.getJSON(requestUrl).pipe(
esikkala's avatar
esikkala committed
      map(response => updateResults({
        resultClass: 'fullTextSearch',
esikkala's avatar
esikkala committed
        data: response.data,
        sparqlQuery: response.sparqlQuery,
esikkala's avatar
esikkala committed
        query: action.query,
esikkala's avatar
esikkala committed
        jenaIndex: action.jenaIndex
esikkala's avatar
esikkala committed
      })),
      catchError(error => of({
        type: FETCH_RESULTS_FAILED,
        resultClass: 'fullTextSearch',
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
esikkala's avatar
esikkala committed
const fetchByURIEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_BY_URI),
esikkala's avatar
esikkala committed
  withLatestFrom(state$),
  mergeMap(([action, state]) => {
    const { perspectiveID, resultClass, facetClass, uri } = action
    const params = stateToUrl({
      perspectiveID,
esikkala's avatar
esikkala committed
      facets: facetClass == null ? null : state[`${facetClass}Facets`].facets,
    const requestUrl = `${apiUrl}/${resultClass}/page/${encodeURIComponent(uri)}`
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
esikkala's avatar
esikkala committed
      map(ajaxResponse => updateInstanceTable({
esikkala's avatar
esikkala committed
        resultClass,
        data: ajaxResponse.response.data,
        sparqlQuery: ajaxResponse.response.sparqlQuery
esikkala's avatar
esikkala committed
      })),
esikkala's avatar
esikkala committed
      catchError(error => of({
        type: FETCH_BY_URI_FAILED,
        resultClass: resultClass,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
const fetchFacetEpic = (action$, state$) => action$.pipe(
Esko Ikkala's avatar
Esko Ikkala committed
  ofType(FETCH_FACET),
esikkala's avatar
esikkala committed
  withLatestFrom(state$),
esikkala's avatar
esikkala committed
  mergeMap(([action, state]) => {
esikkala's avatar
esikkala committed
    const { facetClass, facetID, constrainSelf } = action
    const facets = state[`${facetClass}Facets`].facets
    const facet = facets[facetID]
    const { sortBy = null, sortDirection = null } = facet
    const params = stateToUrl({
esikkala's avatar
esikkala committed
      facets,
      sortBy,
      sortDirection,
      constrainSelf
    const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}`
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse => updateFacetValues({
esikkala's avatar
esikkala committed
        facetClass,
        id: ajaxResponse.response.id,
        data: ajaxResponse.response.data || [],
        flatData: ajaxResponse.response.flatData || [],
        sparqlQuery: ajaxResponse.response.sparqlQuery
esikkala's avatar
esikkala committed
      })),
      catchError(error => of({
        type: FETCH_FACET_FAILED,
esikkala's avatar
esikkala committed
        facetClass,
        facetID,
esikkala's avatar
esikkala committed
        error: error,
esikkala's avatar
esikkala committed
        message: {
esikkala's avatar
esikkala committed
          text: backendErrorText,
          title: 'Error'
esikkala's avatar
esikkala committed
        }
esikkala's avatar
esikkala committed
      }))
Esko Ikkala's avatar
Esko Ikkala committed
  })
const fetchFacetConstrainSelfEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_FACET_CONSTRAIN_SELF),
  withLatestFrom(state$),
  mergeMap(([action, state]) => {
    const { facetClass, facetID } = action
    const facets = state[`${facetClass}Facets`].facets
    const facet = facets[facetID]
    const { sortBy, sortDirection } = facet
    const params = stateToUrl({
      facets: facets,
      sortBy: sortBy,
      sortDirection: sortDirection,
      constrainSelf: true
    const requestUrl = `${apiUrl}/faceted-search/${action.facetClass}/facet/${facetID}`
    return ajax({
      url: requestUrl,
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: params
    }).pipe(
      map(ajaxResponse => updateFacetValuesConstrainSelf({
esikkala's avatar
esikkala committed
        facetClass,
        id: facetID,
        data: ajaxResponse.response.data || [],
        flatData: ajaxResponse.response.flatData || [],
        sparqlQuery: ajaxResponse.response.sparqlQuery
      })),
      catchError(error => of({
        type: FETCH_FACET_FAILED,
esikkala's avatar
esikkala committed
        resultClass: facetClass,
        id: action.id,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
const clientFSFetchResultsEpic = (action$, state$) => action$.pipe(
  ofType(CLIENT_FS_FETCH_RESULTS),
  withLatestFrom(state$),
  debounceTime(500),
  switchMap(([action, state]) => {
    const { perspectiveID, jenaIndex } = action
    const federatedSearchState = state[perspectiveID]
    const selectedDatasets = pickSelectedDatasets(federatedSearchState.datasets)
    const dsParams = selectedDatasets.map(ds => `dataset=${ds}`).join('&')
    let requestUrl
    if (action.jenaIndex === 'text') {
      requestUrl = `${apiUrl}/federated-search?q=${action.query}&${dsParams}&perspectiveID=${perspectiveID}`
    } else if (action.jenaIndex === 'spatial') {
esikkala's avatar
esikkala committed
      const { latMin, longMin, latMax, longMax } = federatedSearchState.maps.boundingboxSearch
      requestUrl = `${apiUrl}/federated-search?latMin=${latMin}&longMin=${longMin}&latMax=${latMax}&longMax=${longMax}&${dsParams}&perspectiveID=${perspectiveID}`
    }
    return ajax.getJSON(requestUrl).pipe(
      map(response => clientFSUpdateResults({
        results: response,
        jenaIndex
      })),
      catchError(error => of({
        type: CLIENT_FS_FETCH_RESULTS_FAILED,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
    )
  })
)

const loadLocalesEpic = action$ => action$.pipe(
  ofType(LOAD_LOCALES),
  // https://thecodebarbarian.com/a-beginners-guide-to-redux-observable
  mergeMap(async action => {
    await intl.init({
      currentLocale: action.currentLanguage,
      locales: availableLocales,
      warningHandler: () => null
    backendErrorText = intl.get('backendErrorText')
    return updateLocale({ language: action.currentLanguage })
const fetchSimilarDocumentsEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_SIMILAR_DOCUMENTS_BY_ID),
  withLatestFrom(state$),
  mergeMap(([action]) => {
    const { resultClass, id, modelName, resultSize } = action
    const requestUrl = `${documentFinderAPIUrl}/top-similar/${modelName}/${id}?n=${resultSize}`
    return ajax.getJSON(requestUrl).pipe(
esikkala's avatar
esikkala committed
      map(res => updateInstanceTableExternal({
        resultClass,
        data: res.documents || null
      })),
      catchError(error => of({
        type: FETCH_SIMILAR_DOCUMENTS_BY_ID_FAILED,
        resultClass: action.resultClass,
        id: action.id,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
    )
  })
)

const fetchGeoJSONLayersBackendEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_GEOJSON_LAYERS_BACKEND),
  withLatestFrom(state$),
  mergeMap(([action]) => {
    const { layerIDs /* bounds */ } = action
    // const { latMin, longMin, latMax, longMax } = boundsToValues(bounds)
    const params = {
      layerID: layerIDs
      // latMin,
      // longMin,
      // latMax,
      // longMax
    const requestUrl = `${apiUrl}/wfs?${querystring.stringify(params)}`
    return ajax.getJSON(requestUrl).pipe(
      map(res => updateGeoJSONLayers({
        payload: res
      })),
      catchError(error => of({
        type: SHOW_ERROR,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
    )
  })
)

const fetchGeoJSONLayersEpic = action$ => action$.pipe(
esikkala's avatar
esikkala committed
  ofType(FETCH_GEOJSON_LAYERS),
  mergeMap(async action => {
esikkala's avatar
esikkala committed
    const { layerIDs, bounds } = action
    try {
      const data = await Promise.all(layerIDs.map(layerID => fetchGeoJSONLayer(layerID, bounds)))
      return updateGeoJSONLayers({ payload: data })
    } catch (error) {
      return fetchGeoJSONLayersFailed({
        error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      })
    }
esikkala's avatar
esikkala committed

esikkala's avatar
esikkala committed
const fetchGeoJSONLayer = async (layerID, bounds) => {
esikkala's avatar
esikkala committed
  const baseUrl = 'https://kartta.nba.fi/arcgis/services/WFS/MV_Kulttuuriymparisto/MapServer/WFSServer'
  // const baseUrl = 'https://kartta.nba.fi/arcgis/services/WFS/MV_KulttuuriymparistoSuojellut/MapServer/WFSServer'
  // const baseUrl = 'http://avaa.tdata.fi/geoserver/kotus/ows'
  // const baseUrl = 'http://avaa.tdata.fi/geoserver/paituli/wfs'
  const boundsStr =
    `${bounds._southWest.lng},${bounds._southWest.lat},${bounds._northEast.lng},${bounds._northEast.lat}`
esikkala's avatar
esikkala committed
  const mapServerParams = {
    request: 'GetFeature',
    service: 'WFS',
    version: '2.0.0',
    typeName: layerID,
    srsName: 'EPSG:4326',
    outputFormat: 'geojson',
    bbox: boundsStr
    // outputFormat: 'application/json' for kotus layers
esikkala's avatar
esikkala committed
  }
  const url = `${baseUrl}?${querystring.stringify(mapServerParams)}`
  const response = await axios.get(url)
  return {
    layerID: layerID,
    geoJSON: response.data
esikkala's avatar
esikkala committed

const fetchKnowledgeGraphMetadataEpic = (action$, state$) => action$.pipe(
  ofType(FETCH_KNOWLEDGE_GRAPH_METADATA),
  withLatestFrom(state$),
  mergeMap(([action]) => {
esikkala's avatar
esikkala committed
    const requestUrl = `${apiUrl}/void/${action.perspectiveID}/${action.resultClass}`
    return ajax({
      url: requestUrl,
      method: 'GET'
    }).pipe(
      map(ajaxResponse => updateKnowledgeGraphMetadata({
        resultClass: action.resultClass,
        data: ajaxResponse.response.data[0],
        sparqlQuery: ajaxResponse.response.sparqlQuery
      })),
      catchError(error => of({
        type: FETCH_KNOWLEDGE_GRAPH_METADATA_FAILED,
        perspectiveID: action.resultClass,
        error: error,
        message: {
          text: backendErrorText,
          title: 'Error'
        }
      }))
    )
  })
)

const rootEpic = combineEpics(
  fetchPaginatedResultsEpic,
  fetchResultsEpic,
  fetchInstanceAnalysisEpic,
esikkala's avatar
esikkala committed
  fetchResultCountEpic,
  fetchByURIEpic,
  fetchFacetEpic,
  fetchFacetConstrainSelfEpic,
  fullTextSearchEpic,
  clientFSFetchResultsEpic,
  loadLocalesEpic,
esikkala's avatar
esikkala committed
  fetchSimilarDocumentsEpic,
  fetchGeoJSONLayersEpic,
  fetchGeoJSONLayersBackendEpic,
  fetchKnowledgeGraphMetadataEpic
Erkki Heino's avatar
Erkki Heino committed

export default rootEpic