diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java
index e9356c024589c94bb81d3eff8960729a425bbb1c..3014c19e165b2656a46494c2afea9f1289dfba82 100755
--- a/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java
+++ b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java
@@ -38,14 +38,12 @@ import no.nibio.vips.util.DateUtil;
import no.nibio.vips.util.DateUtilException;
import no.nibio.vips.util.ExceptionUtil;
import no.nibio.vips.util.ServletUtil;
-import no.nibio.web.forms.FormUtil;
-import no.nibio.web.forms.FormValidation;
-import no.nibio.web.forms.FormValidationException;
-import no.nibio.web.forms.FormValidator;
+import no.nibio.web.forms.*;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileItem;
-import org.apache.commons.fileupload2.core.FileUploadException;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.ParseException;
@@ -58,6 +56,8 @@ import java.util.stream.Collectors;
* @copyright 2014-2022 <a href="http://www.nibio.no/">NIBIO</a>
*/
public class ObservationController extends HttpServlet {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ObservationController.class);
+
@PersistenceContext(unitName = "VIPSLogic-PU")
EntityManager em;
@@ -84,6 +84,8 @@ public class ObservationController extends HttpServlet {
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
String action = request.getParameter("action");
+ String returnTo = request.getParameter("returnTo");
+ boolean returnToTimeSeries = returnTo != null && returnTo.equals("timeseries");
VipsLogicUser user = (VipsLogicUser) request.getSession().getAttribute("user");
// Get the organization groups for the current user
List<OrganizationGroup> organizationGroups = null;
@@ -246,8 +248,8 @@ public class ObservationController extends HttpServlet {
if (userBean.authorizeUser(user, VipsLogicRole.OBSERVER, VipsLogicRole.OBSERVATION_AUTHORITY,
VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) {
try {
-
Observation observation = new Observation();
+ observation.setTimeOfObservation(new Date());
// See if there are presets on the URL string
if (request.getParameter("cropOrganismId") != null) {
observation.setCropOrganism(
@@ -264,14 +266,31 @@ public class ObservationController extends HttpServlet {
allCrops = em.createNamedQuery("Organism.findAllCrops").getResultList();
}
+ request.setAttribute("observation", observation);
+
// Get the polygonServices
List<PolygonService> polygonServices =
observationBean.getPolygonServicesForOrganization(user.getOrganization_id());
-
- request.setAttribute("observation", observation);
request.setAttribute("polygonServices", polygonServices);
- request.setAttribute("locationVisibilityFormValue",
- ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PUBLIC);
+
+ if (request.getParameter("observationTimeSeriesId") != null) {
+ ObservationTimeSeries observationTimeSeries = em.find(ObservationTimeSeries.class,
+ Integer.valueOf(request.getParameter("observationTimeSeriesId")));
+ observation.setObservationTimeSeries(observationTimeSeries);
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.YEAR, observationTimeSeries.getYear());
+ cal.set(Calendar.MONTH, Calendar.getInstance().get(Calendar.MONTH));
+ cal.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
+ observation.setTimeOfObservation(cal.getTime());
+ request.setAttribute("minTimeOfObservation", getMinDate(observationTimeSeries.getYear()));
+ request.setAttribute("maxTimeOfObservation", getMaxDate(observationTimeSeries.getYear()));
+ request.setAttribute("locationVisibilityFormValue", getLocationVisibilityFormValue(
+ observationTimeSeries.getPolygonService(), observationTimeSeries.getLocationIsPrivate()));
+ } else {
+ request.setAttribute("locationVisibilityFormValue",
+ ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PUBLIC);
+ }
+
request.setAttribute("shortcut", request.getParameter("observationFormShortcutId") != null ?
em.find(ObservationFormShortcut.class,
Integer.valueOf(request.getParameter("observationFormShortcutId")))
@@ -299,6 +318,8 @@ public class ObservationController extends HttpServlet {
request.setAttribute("observationMethods",
em.createNamedQuery("ObservationMethod.findAll", ObservationMethod.class).getResultList());
request.setAttribute("organizationGroups", organizationGroups);
+ request.setAttribute("returnTo", returnTo);
+ request.setAttribute("returnPath", returnToTimeSeries ? "/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=" + observation.getObservationTimeSeriesId() : "/observation");
request.setAttribute("editAccess", "W"); // User always has edit access to new observation
if (userBean.authorizeUser(user, VipsLogicRole.OBSERVATION_AUTHORITY,
VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) {
@@ -325,8 +346,15 @@ public class ObservationController extends HttpServlet {
List<PolygonService> polygonServices =
observationBean.getPolygonServicesForOrganization(user.getOrganization_id());
request.setAttribute("locationVisibilityFormValue",
- this.getLocationVisibilityFormValue(observation));
+ this.getLocationVisibilityFormValue(observation.getPolygonService(), observation.getLocationIsPrivate()));
request.setAttribute("observation", observation);
+
+ ObservationTimeSeries observationTimeSeries = observation.getObservationTimeSeries();
+ if(observationTimeSeries != null) {
+ request.setAttribute("minTimeOfObservation", getMinDate(observationTimeSeries.getYear()));
+ request.setAttribute("maxTimeOfObservation", getMaxDate(observationTimeSeries.getYear()));
+ }
+
request.setAttribute("polygonServices", polygonServices);
request.setAttribute("noBroadcast", request.getParameter("noBroadcast") != null);
request.setAttribute("mapLayers", userBean.getMapLayerJSONForUser(user));
@@ -352,6 +380,8 @@ public class ObservationController extends HttpServlet {
request.setAttribute("statusTypeIds",
em.createNamedQuery("ObservationStatusType.findAll").getResultList());
}
+ request.setAttribute("returnTo", returnTo);
+ request.setAttribute("returnPath", returnToTimeSeries ? "/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=" + observation.getObservationTimeSeriesId() : "/observation");
if (request.getParameter("messageKey") != null) {
request.setAttribute("messageKey", request.getParameter("messageKey"));
}
@@ -382,7 +412,6 @@ public class ObservationController extends HttpServlet {
Map<String, String[]> parameterMap;
List<FileItem> items = null;
if (JakartaServletFileUpload.isMultipartContent(request)) {
-
// Create a new file upload handler
DiskFileItemFactory dfif = DiskFileItemFactory.builder().get();
JakartaServletFileUpload upload = new JakartaServletFileUpload(dfif);
@@ -399,16 +428,12 @@ public class ObservationController extends HttpServlet {
Observation observation =
observationId > 0 ? em.find(Observation.class, observationId) : new Observation();
-
-
if (formValidation.isValid()) {
// Storing observation
// Only new observations can set the organism
if (observationId <= 0 || user.isSuperUser() || user.isOrganizationAdmin()) {
- observation.setOrganism(em.find(Organism.class,
- formValidation.getFormField("organismId").getValueAsInteger()));
- observation.setCropOrganism(em.find(Organism.class,
- formValidation.getFormField("cropOrganismId").getValueAsInteger()));
+ observation.setOrganism(em.find(Organism.class, formValidation.getFormField("organismId").getValueAsInteger()));
+ observation.setCropOrganism(em.find(Organism.class, formValidation.getFormField("cropOrganismId").getValueAsInteger()));
}
observation.setTimeOfObservation(
formValidation.getFormField("timeOfObservation").getValueAsDate());
@@ -417,11 +442,16 @@ public class ObservationController extends HttpServlet {
} else {
observation.setLastEditedBy(user.getUserId());
}
+ FormField observationTimeSeriesField = formValidation.getFormField("observationTimeSeriesId");
+ if(observationTimeSeriesField != null && !observationTimeSeriesField.isEmpty()) {
+ Integer otsIdInt = observationTimeSeriesField.getValueAsInteger();
+ observation.setObservationTimeSeries(em.find(ObservationTimeSeries.class, otsIdInt));
+ }
observation.setLastEditedTime(new Date());
observation.setObservationHeading(
formValidation.getFormField("observationHeading").getWebValue());
observation.setObservationText(
- formValidation.getFormField("observationText").getWebValue());
+ formValidation.getFormField("observationText").getWebValue().isEmpty() ? null : formValidation.getFormField("observationText").getWebValue());
observation.setObservationData(
formValidation.getFormField("observationData").getWebValue().isEmpty() ?
null
@@ -506,11 +536,12 @@ public class ObservationController extends HttpServlet {
}
// Redirect to form
- response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://")
- .append(ServletUtil.getServerName(request))
- .append("/observation?action=editObservationForm&observationId=")
- .append(observation.getObservationId())
- .append("&messageKey=").append("observationStored").toString()
+ response.sendRedirect(Globals.PROTOCOL + "://" +
+ ServletUtil.getServerName(request) +
+ "/observation?action=editObservationForm&observationId=" +
+ observation.getObservationId() +
+ "&returnTo=" + returnTo +
+ "&messageKey=" + "observationStored"
);
} else {
@@ -528,11 +559,8 @@ public class ObservationController extends HttpServlet {
request.getRequestDispatcher("/observationForm.ftl").forward(request, response);
}
- } catch (NullPointerException | NumberFormatException | FormValidationException |
- FileUploadException ex) {
- response.sendError(500, ExceptionUtil.getStackTrace(ex));
} catch (Exception ex) {
- ex.printStackTrace();
+ LOGGER.error("Exception occurred while saving observation", ex);
response.sendError(500, ExceptionUtil.getStackTrace(ex));
}
} else {
@@ -545,13 +573,18 @@ public class ObservationController extends HttpServlet {
VipsLogicRole.SUPERUSER)) {
try {
Integer observationId = Integer.valueOf(request.getParameter("observationId"));
+ Observation observation = observationId > 0 ? em.find(Observation.class, observationId) : new Observation();
+ Integer observationTimeSeriesId = observation.getObservationTimeSeriesId();
+
observationBean.deleteObservation(observationId);
+ String redirectTo = returnToTimeSeries ? "/observationTimeSeries?observationTimeSeries=" + observationTimeSeriesId + "&": "/observation?";
+
// Redirect to list
- response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://")
- .append(ServletUtil.getServerName(request))
- .append("/observation")
- .append("?messageKey=").append("observationDeleted").toString()
+ response.sendRedirect(Globals.PROTOCOL + "://" +
+ ServletUtil.getServerName(request) +
+ redirectTo +
+ "messageKey=observationDeleted"
);
} catch (NullPointerException | NumberFormatException ex) {
response.sendError(500, ExceptionUtil.getStackTrace(ex));
@@ -635,21 +668,37 @@ public class ObservationController extends HttpServlet {
}
}
+ private Date getMinDate(int year) {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.YEAR, year);
+ cal.set(Calendar.MONTH, Calendar.JANUARY);
+ cal.set(Calendar.DAY_OF_MONTH, 1);
+ return cal.getTime();
+ }
+
+ private Date getMaxDate(int year) {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.YEAR, year);
+ cal.set(Calendar.MONTH, Calendar.DECEMBER);
+ cal.set(Calendar.DAY_OF_MONTH, 31);
+ return cal.getTime();
+ }
+
public static final String LOCATION_VISIBILITY_FORM_VALUE_PRIVATE = "private";
public static final String LOCATION_VISIBILITY_FORM_VALUE_PUBLIC = "public";
public static final String LOCATION_VISIBILITY_FORM_VALUE_MASK_PREFIX = "mask_";
- private String getLocationVisibilityFormValue(Observation observation) {
+ private String getLocationVisibilityFormValue(PolygonService polygonService, Boolean locationIsPrivate) {
// Private is private, no matter what
- if (observation.getLocationIsPrivate()) {
+ if (locationIsPrivate) {
return ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PRIVATE;
}
// Public can either be completely public or masked by a polygon layer
// e.g. county borders
- if (observation.getPolygonService() == null) {
+ if (polygonService == null) {
return ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PUBLIC;
}
- return ObservationController.LOCATION_VISIBILITY_FORM_VALUE_MASK_PREFIX + observation.getPolygonService()
+ return ObservationController.LOCATION_VISIBILITY_FORM_VALUE_MASK_PREFIX + polygonService
.getPolygonServiceId();
}
diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationTimeSeriesController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationTimeSeriesController.java
new file mode 100644
index 0000000000000000000000000000000000000000..da442409c7c083e3f6ac8fe596274fa93a1ef641
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationTimeSeriesController.java
@@ -0,0 +1,274 @@
+package no.nibio.vips.logic.controller.servlet;
+
+import jakarta.ejb.EJB;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import no.nibio.vips.logic.controller.session.*;
+import no.nibio.vips.logic.entity.*;
+import no.nibio.vips.logic.i18n.SessionLocaleUtil;
+import no.nibio.vips.logic.messaging.MessagingBean;
+import no.nibio.vips.logic.util.Globals;
+import no.nibio.vips.util.ExceptionUtil;
+import no.nibio.vips.util.ServletUtil;
+import no.nibio.web.forms.*;
+import org.apache.commons.fileupload2.core.DiskFileItemFactory;
+import org.apache.commons.fileupload2.core.FileItem;
+import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.util.*;
+
+public class ObservationTimeSeriesController extends HttpServlet {
+ private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ObservationTimeSeriesController.class);
+
+ private static final int START_YEAR = 2016;
+ public static final String LOCATION_VISIBILITY_FORM_VALUE_PRIVATE = "private";
+ public static final String LOCATION_VISIBILITY_FORM_VALUE_PUBLIC = "public";
+ public static final String LOCATION_VISIBILITY_FORM_VALUE_MASK_PREFIX = "mask_";
+
+ @PersistenceContext(unitName = "VIPSLogic-PU")
+ EntityManager em;
+
+ @EJB
+ UserBean userBean;
+ @EJB
+ ObservationTimeSeriesBean observationTimeSeriesBean;
+ @EJB
+ ObservationBean observationBean;
+ @EJB
+ MessagingBean messagingBean;
+ @EJB
+ OrganismBean organismBean;
+ @EJB
+ PointOfInterestBean pointOfInterestBean;
+
+ /**
+ * Handles the HTTP <code>GET</code> method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+
+ /**
+ * Handles the HTTP <code>POST</code> method.
+ *
+ * @param request servlet request
+ * @param response servlet response
+ * @throws ServletException if a servlet-specific error occurs
+ * @throws IOException if an I/O error occurs
+ */
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+
+ protected void processRequest(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ response.setContentType("text/html;charset=UTF-8");
+ String action = request.getParameter("action");
+ VipsLogicUser user = (VipsLogicUser) request.getSession().getAttribute("user");
+ request.setAttribute("messageKey", request.getParameter("messageKey"));
+ List<ObservationTimeSeries> observationTimeSeriesList = observationTimeSeriesBean.getObservationTimeSeriesListForUser(user);
+
+ if (request.getServletPath().endsWith("/observationTimeSeries")) {
+ if (action == null) {
+ request.setAttribute("observationTimeSeriesList", observationTimeSeriesList);
+ LOGGER.info("Display list of {} observationTimeSeries", observationTimeSeriesList.size());
+
+ Map<String, Long> observationCounts = new HashMap<>();
+ for (ObservationTimeSeries observationTimeSeries : observationTimeSeriesList) {
+ Long count = em.createNamedQuery("Observation.findCountByObservationTimeSeries", Long.class)
+ .setParameter("observationTimeSeries", observationTimeSeries).getSingleResult();
+ observationCounts.put(String.valueOf(observationTimeSeries.getObservationTimeSeriesId()), count);
+ }
+ request.setAttribute("userIsObservationAuthority",
+ userBean.authorizeUser(user, VipsLogicRole.OBSERVATION_AUTHORITY,
+ VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER));
+ request.setAttribute("observationCounts", observationCounts);
+ request.getRequestDispatcher("/observationTimeSeriesList.ftl").forward(request, response);
+ return;
+ }
+ boolean authorized =
+ userBean.authorizeUser(user, VipsLogicRole.OBSERVER, VipsLogicRole.ORGANIZATION_ADMINISTRATOR,
+ VipsLogicRole.SUPERUSER);
+ if (!authorized) {
+ LOGGER.error("User {} not authorized", user.getUserId());
+ response.sendError(403, "Access not authorized");
+ }
+ if (action.equals("editObservationTimeSeriesForm")) {
+ Integer observationTimeSeriesId = Integer.valueOf(request.getParameter("observationTimeSeriesId"));
+ ObservationTimeSeries observationTimeSeries =
+ observationTimeSeriesBean.getObservationTimeSeries(observationTimeSeriesId);
+ Long observationCount = em.createNamedQuery("Observation.findCountByObservationTimeSeries", Long.class)
+ .setParameter("observationTimeSeries", observationTimeSeries).getSingleResult();
+
+ request.setAttribute("observationTimeSeries", observationTimeSeries);
+ request.setAttribute("isEditable", observationCount == 0);
+ request.setAttribute("userIsObservationAuthority",
+ userBean.authorizeUser(user, VipsLogicRole.OBSERVATION_AUTHORITY,
+ VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER));
+ buildFormRequest(request, user, observationTimeSeries);
+ request.setAttribute("observations",
+ observationBean.getObservationsForTimeSeries(observationTimeSeries));
+ request.getRequestDispatcher("/observationTimeSeriesForm.ftl").forward(request, response);
+ } else if (action.equals("newObservationTimeSeriesForm")) {
+ ObservationTimeSeries observationTimeSeries = new ObservationTimeSeries();
+ request.setAttribute("observationTimeSeries", observationTimeSeries);
+ buildFormRequest(request, user, observationTimeSeries);
+ request.setAttribute("isEditable", true);
+ request.getRequestDispatcher("/observationTimeSeriesForm.ftl").forward(request, response);
+ } else if (action.equals("observationTimeSeriesFormSubmit")) {
+ Map<String, String[]> parameterMap;
+ List<FileItem> items;
+ if (JakartaServletFileUpload.isMultipartContent(request)) {
+ DiskFileItemFactory dfif = DiskFileItemFactory.builder().get();
+ JakartaServletFileUpload upload = new JakartaServletFileUpload(dfif);
+ items = upload.parseRequest(request);
+ parameterMap = FormUtil.getParameterMap(items, "UTF-8");
+ } else {
+ parameterMap = request.getParameterMap();
+ }
+ ObservationTimeSeries observationTimeSeries = new ObservationTimeSeries();
+ try {
+ FormValidation formValidation = FormValidator.validateForm(
+ "observationTimeSeriesForm",
+ parameterMap,
+ SessionLocaleUtil.getI18nBundle(request),
+ getServletContext()
+ );
+ if (formValidation.isValid()) {
+ observationTimeSeries.setUserId(user.getUserId()); // Should only be set for new time series
+ Long observationCount = 0L;
+
+ Integer observationTimeSeriesId =
+ formValidation.getFormField("observationTimeSeriesId").getValueAsInteger();
+ if (observationTimeSeriesId > 0) {
+ // Fetch existing time series if id > 0
+ observationTimeSeries = em.find(ObservationTimeSeries.class, observationTimeSeriesId);
+ observationCount = em.createNamedQuery("Observation.findCountByObservationTimeSeries", Long.class)
+ .setParameter("observationTimeSeries", observationTimeSeries).getSingleResult();
+ }
+ observationTimeSeries.setLastModifiedBy(user.getUserId());
+ observationTimeSeries.setLastModified(new Date());
+
+ // Only observation time series without observations can set the crop, pest and location
+ if (observationCount == 0) {
+ observationTimeSeries.setOrganism(em.find(Organism.class, formValidation.getFormField("organismId").getValueAsInteger()));
+ observationTimeSeries.setCropOrganism(em.find(Organism.class, formValidation.getFormField("cropOrganismId").getValueAsInteger()));
+ observationTimeSeries.setYear(formValidation.getFormField("year").getValueAsInteger());
+ setObservationLocationVisibility(observationTimeSeries, formValidation.getFormField("locationVisibility").getWebValue());
+ observationTimeSeries.setLocationPointOfInterestId(
+ formValidation.getFormField("locationPointOfInterestId").getValueAsInteger());
+ }
+ observationTimeSeries.setName(formValidation.getFormField("name").getWebValue());
+ observationTimeSeries.setDescription(formValidation.getFormField("description").getWebValue());
+ observationTimeSeries = observationTimeSeriesBean.storeObservationTimeSeries(observationTimeSeries);
+ LOGGER.info("Submit changes for observationTimeSeries {}", observationTimeSeries.getName());
+
+ response.sendRedirect(Globals.PROTOCOL + "://" +
+ ServletUtil.getServerName(request) +
+ "/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=" +
+ observationTimeSeries.getObservationTimeSeriesId() +
+ "&messageKey=observationTimeSeriesStored"
+ );
+ } else {
+ // Redirect to form with error messages
+ buildFormRequest(request, user, observationTimeSeries);
+ request.setAttribute("formValidation", formValidation);
+ request.getRequestDispatcher("/observationTimeSeriesForm.ftl").forward(request, response);
+ }
+ } catch (FormValidationException e) {
+ LOGGER.error("Exception at form validation", e);
+ response.sendError(500, ExceptionUtil.getStackTrace(e));
+ }
+ } else if (action.equals("deleteObservationTimeSeries")) {
+ try {
+ Integer observationTimeSeriesId = Integer.valueOf(request.getParameter("observationTimeSeriesId"));
+ LOGGER.info("Delete observationTimeSeries {}", observationTimeSeriesId);
+ observationTimeSeriesBean.deleteObservationTimeSeries(observationTimeSeriesId);
+
+ // Redirect to list
+ response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://")
+ .append(ServletUtil.getServerName(request))
+ .append("/observationTimeSeries")
+ .append("?messageKey=").append("observationTimeSeriesDeleted").toString()
+ );
+ } catch (NullPointerException | NumberFormatException ex) {
+ response.sendError(500, ExceptionUtil.getStackTrace(ex));
+ }
+
+ }
+ }
+ }
+
+ private void buildFormRequest(HttpServletRequest request, VipsLogicUser user, ObservationTimeSeries observationTimeSeries) {
+ request.setAttribute("observationTimeSeries", observationTimeSeries);
+ request.setAttribute("years", generateYearList());
+ request.setAttribute("mapLayers", userBean.getMapLayerJSONForUser(user));
+ request.setAttribute("defaultMapCenter", user.getOrganizationId().getDefaultMapCenter());
+ request.setAttribute("defaultMapZoom", user.getOrganizationId().getDefaultMapZoom());
+ request.setAttribute("locationPointOfInterests", pointOfInterestBean.getPoisForUser(user));
+ request.setAttribute("locationVisibilityFormValue",
+ getLocationVisibilityFormValue(observationTimeSeries));
+ List<PolygonService> polygonServices =
+ observationBean.getPolygonServicesForOrganization(user.getOrganization_id());
+ request.setAttribute("polygonServices", polygonServices);
+ List<Organism> allPests = em.createNamedQuery("Organism.findAllPests").getResultList();
+ request.setAttribute("allPests", organismBean.sortOrganismsByLocalName(allPests,
+ SessionLocaleUtil.getCurrentLocale(request).getLanguage()));
+ List<Organism> allCrops = em.createNamedQuery("Organism.findAllCrops").getResultList();
+ request.setAttribute("allCrops", organismBean.sortOrganismsByLocalName(allCrops,
+ SessionLocaleUtil.getCurrentLocale(request).getLanguage()));
+ request.setAttribute("hierarchyCategories",
+ organismBean.getHierarchyCategoryNames(SessionLocaleUtil.getCurrentLocale(request)));
+ }
+
+ private List<Integer> generateYearList() {
+ List<Integer> years = new ArrayList<>();
+ for (int year = START_YEAR; year <= LocalDate.now().getYear(); year++) {
+ years.add(year);
+ }
+ return years;
+ }
+
+ private String getLocationVisibilityFormValue(ObservationTimeSeries observationTimeSeries) {
+ // Private is private, no matter what
+ if (observationTimeSeries.getLocationIsPrivate()) {
+ return LOCATION_VISIBILITY_FORM_VALUE_PRIVATE;
+ }
+ // Public can either be completely public or masked by a polygon layer. e.g. county borders
+ if (observationTimeSeries.getPolygonService() == null) {
+ return LOCATION_VISIBILITY_FORM_VALUE_PUBLIC;
+ }
+ return LOCATION_VISIBILITY_FORM_VALUE_MASK_PREFIX + observationTimeSeries.getPolygonService()
+ .getPolygonServiceId();
+ }
+
+ private void setObservationLocationVisibility(ObservationTimeSeries observationTimeSeries, String formValue) {
+ // Private is private, no matter what
+ observationTimeSeries.setLocationIsPrivate(
+ formValue.equals(ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PRIVATE));
+ observationTimeSeries.setPolygonService(
+ // If private or public, set no polygon service
+ formValue.equals(ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PRIVATE) || formValue.equals(
+ ObservationController.LOCATION_VISIBILITY_FORM_VALUE_PUBLIC)
+ ? null
+ // Otherwise, set selected polygon service
+ : em.find(PolygonService.class, Integer.valueOf(formValue.split("_")[1]))
+ );
+ }
+}
diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java
index b296e0e5154b075a1db74f27bb98e6ccca2fe4c8..a8d1cbd8b9c0efa92385da27ec474496136f89d4 100755
--- a/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java
+++ b/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java
@@ -24,6 +24,9 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+
+import no.nibio.vips.logic.controller.session.*;
+import no.nibio.vips.logic.entity.*;
import org.apache.http.client.utils.URIBuilder;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
@@ -36,23 +39,6 @@ import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import no.nibio.vips.gis.GISUtil;
-import no.nibio.vips.logic.controller.session.ForecastBean;
-import no.nibio.vips.logic.controller.session.ObservationBean;
-import no.nibio.vips.logic.controller.session.PointOfInterestBean;
-import no.nibio.vips.logic.controller.session.UserBean;
-import no.nibio.vips.logic.entity.Country;
-import no.nibio.vips.logic.entity.ForecastConfiguration;
-import no.nibio.vips.logic.entity.ModelInformation;
-import no.nibio.vips.logic.entity.Observation;
-import no.nibio.vips.logic.entity.Organization;
-import no.nibio.vips.logic.entity.PointOfInterest;
-import no.nibio.vips.logic.entity.PointOfInterestExternalResource;
-import no.nibio.vips.logic.entity.PointOfInterestExternalResourcePK;
-import no.nibio.vips.logic.entity.PointOfInterestWeatherStation;
-import no.nibio.vips.logic.entity.VipsLogicRole;
-import no.nibio.vips.logic.entity.VipsLogicUser;
-import no.nibio.vips.logic.entity.WeatherForecastProvider;
-import no.nibio.vips.logic.entity.WeatherStationDataSource;
import no.nibio.vips.logic.entity.helpers.PointOfInterestFactory;
import no.nibio.vips.logic.i18n.SessionLocaleUtil;
import no.nibio.vips.logic.util.Globals;
@@ -81,8 +67,9 @@ public class PointOfInterestController extends HttpServlet {
ForecastBean forecastBean;
@EJB
ObservationBean observationBean;
-
-
+ @EJB
+ ObservationTimeSeriesBean observationTimeSeriesBean;
+
/**
* Processes requests for both HTTP
* <code>GET</code> and
@@ -795,12 +782,10 @@ public class PointOfInterestController extends HttpServlet {
// Are there forecasts attached to this location
List<ForecastConfiguration> forecastConfigurations = forecastBean.getForecastConfigurationsByLocation(poi);
-
- // TODO: Are there observations attached to this location?
List<Observation> observations = observationBean.getObservationsByLocation(poi);
- //
- // If no strings attached, delete immediately
- if(forecastConfigurations.isEmpty() && observations.isEmpty())
+ List<ObservationTimeSeries> observationTimeSeries = observationTimeSeriesBean.getObservationTimeSeriesListForLocation(poi);
+
+ if(forecastConfigurations.isEmpty() && observations.isEmpty() && observationTimeSeries.isEmpty())
{
response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://")
.append(ServletUtil.getServerName(request))
@@ -814,8 +799,8 @@ public class PointOfInterestController extends HttpServlet {
request.setAttribute("returnURL","poi?action=editPoiForm&pointOfInterestId=" + pointOfInterestId);
request.setAttribute("poi", poi);
request.setAttribute("forecastConfigurations", forecastConfigurations);
- // TODO Set observations
request.setAttribute("observations", observations);
+ request.setAttribute("observationTimeSeries", observationTimeSeries);
request.setAttribute("modelInformation", modelInformationMap);
request.getRequestDispatcher("/poiDeletePreview.ftl").forward(request, response);
}
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
index 804178624ab6f309822e4e0968f85e05c9f58522..e970296e6768bafdfd7ad0e572fe1f1e5ad37192 100755
--- a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
@@ -189,6 +189,15 @@ public class ObservationBean {
return retVal;
}
+ public List<Observation> getObservationsForTimeSeries(ObservationTimeSeries observationTimeSeries) {
+ List<Observation> retVal = this.getObservationsWithGeoInfo(em.createNamedQuery("Observation.findByObservationTimeSeries", Observation.class)
+ .setParameter("observationTimeSeries", observationTimeSeries)
+ .getResultList());
+ retVal = this.getObservationsWithLocations(retVal);
+ retVal = this.getObservationsWithObservers(retVal);
+ return retVal;
+ }
+
public Observation getObservation(Integer observationId) {
Observation retVal = em.find(Observation.class, observationId);
if (retVal != null) {
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java
index 627002b9b0d6ac5e0680c346f8ed715f4c069b12..d39d0954d31daa89a02ffadce35008aac2da1c55 100644
--- a/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationTimeSeriesBean.java
@@ -23,9 +23,7 @@ import jakarta.ejb.EJB;
import jakarta.ejb.Stateless;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
-import no.nibio.vips.logic.entity.ObservationTimeSeries;
-import no.nibio.vips.logic.entity.PointOfInterest;
-import no.nibio.vips.logic.entity.VipsLogicUser;
+import no.nibio.vips.logic.entity.*;
@Stateless
public class ObservationTimeSeriesBean {
@@ -39,6 +37,12 @@ public class ObservationTimeSeriesBean {
@EJB
ObservationBean observationBean;
+ /**
+ * Get list of observation time series registered by given user
+ *
+ * @param user The user
+ * @return a list of observation time series
+ */
public List<ObservationTimeSeries> getObservationTimeSeriesListForUser(VipsLogicUser user) {
List<ObservationTimeSeries> resultList = em.createNamedQuery("ObservationTimeSeries.findByUserId", ObservationTimeSeries.class)
.setParameter("userId", user.getUserId())
@@ -48,6 +52,18 @@ public class ObservationTimeSeriesBean {
return resultList;
}
+ /**
+ * Get list of observation time series registered for given poi
+ *
+ * @param poi The point of interest
+ * @return a list of observation time series
+ */
+ public List<ObservationTimeSeries> getObservationTimeSeriesListForLocation(PointOfInterest poi) {
+ return em.createNamedQuery("ObservationTimeSeries.findByLocationPointOfInterestId", ObservationTimeSeries.class)
+ .setParameter("locationPointOfInterestId", poi.getPointOfInterestId())
+ .getResultList();
+ }
+
/**
* Get observation time series with given id. Enrich object with user information before returning.
* @param observationTimeSeriesId the id of the observation time series to retrieve
@@ -144,4 +160,17 @@ public class ObservationTimeSeriesBean {
o.setUser(mappedUsers.get(o.getUserId()));
});
}
+
+ /**
+ * Delete all observation time series registered for the given point of interest
+ *
+ * @param poi The point of interest
+ */
+ public void deleteObservationTimeSeriesForLocation(PointOfInterest poi) {
+ em.createNamedQuery("ObservationTimeSeries.findByLocationPointOfInterestId", ObservationTimeSeries.class)
+ .setParameter("locationPointOfInterestId", poi.getPointOfInterestId())
+ .getResultList().stream()
+ .forEach(ots -> em.remove(ots));
+
+ }
}
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
index b064c913c1388b756b042bf46fc90265a8a7f458..18dadec7afc5ce6a888e7ea4e1a4759ee4990d2d 100755
--- a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java
@@ -72,6 +72,8 @@ public class PointOfInterestBean {
ForecastBean forecastBean;
@EJB
ObservationBean observationBean;
+ @EJB
+ ObservationTimeSeriesBean observationTimeSeriesBean;
public List<PointOfInterest> getWeatherstations(String countryCode)
{
@@ -467,6 +469,7 @@ public class PointOfInterestBean {
PointOfInterest poi = em.find(PointOfInterest.class, pointOfInterestId);
forecastBean.deleteForecastConfigurationsForLocation(poi);
observationBean.deleteObservationsForLocation(poi);
+ observationTimeSeriesBean.deleteObservationTimeSeriesForLocation(poi);
em.remove(poi);
}
diff --git a/src/main/java/no/nibio/vips/logic/entity/Observation.java b/src/main/java/no/nibio/vips/logic/entity/Observation.java
index 478e8dd610a6ebe5e9d37df9d499f097afd80445..a845e383fcb3512df374114013d92d0f3c2166c3 100755
--- a/src/main/java/no/nibio/vips/logic/entity/Observation.java
+++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java
@@ -53,6 +53,7 @@ import org.hibernate.type.SqlTypes;
@NamedQuery(name = "Observation.findByLastEditedBy", query = "SELECT o FROM Observation o WHERE o.lastEditedBy IN(:lastEditedBy)"),
@NamedQuery(name = "Observation.findByLocationPointOfInterestId", query = "SELECT o FROM Observation o WHERE o.locationPointOfInterestId = :locationPointOfInterestId"),
@NamedQuery(name = "Observation.findByObservationTimeSeries", query = "SELECT o FROM Observation o WHERE o.observationTimeSeries = :observationTimeSeries"),
+ @NamedQuery(name = "Observation.findCountByObservationTimeSeries", query = "SELECT count(o) FROM Observation o WHERE o.observationTimeSeries = :observationTimeSeries"),
@NamedQuery(name = "Observation.findByStatusChangedByUserId", query = "SELECT o FROM Observation o WHERE o.statusChangedByUserId IN(:statusChangedByUserId)"),
@NamedQuery(name = "Observation.findByUserIdAndPeriod", query = "SELECT o FROM Observation o WHERE o.timeOfObservation BETWEEN :start AND :end AND o.userId IN(:userId)"),
@NamedQuery(name = "Observation.findByUserIdAndStatusTypeId", query = "SELECT o FROM Observation o WHERE o.userId IN(:userId) AND o.statusTypeId= :statusTypeId"),
diff --git a/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java b/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java
index 04db0628f62c6fae7e840310a594a927eca58672..6ee76a5bd10118abc62872895b5225cecdcc84cf 100644
--- a/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java
+++ b/src/main/java/no/nibio/vips/logic/entity/ObservationTimeSeries.java
@@ -33,7 +33,8 @@ import java.util.*;
@NamedQuery(name = "ObservationTimeSeries.findAll", query = "SELECT ots FROM ObservationTimeSeries ots"),
@NamedQuery(name = "ObservationTimeSeries.findByObservationTimeSeriesId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.observationTimeSeriesId = :id"),
@NamedQuery(name = "ObservationTimeSeries.findByObservationTimeSeriesIds", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.observationTimeSeriesId IN :observationTimeSeriesIds"),
- @NamedQuery(name = "ObservationTimeSeries.findByUserId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.userId IN(:userId)")
+ @NamedQuery(name = "ObservationTimeSeries.findByUserId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.userId IN(:userId) order by ots.year desc, ots.created desc"),
+ @NamedQuery(name = "ObservationTimeSeries.findByLocationPointOfInterestId", query = "SELECT ots FROM ObservationTimeSeries ots WHERE ots.locationPointOfInterestId = :locationPointOfInterestId")
})
public class ObservationTimeSeries implements Serializable {
@@ -61,9 +62,12 @@ public class ObservationTimeSeries implements Serializable {
public ObservationTimeSeries() {
this.GISEntityUtil = new GISEntityUtil();
+ this.source = "WEB";
+ this.created = new Date();
}
public ObservationTimeSeries(String source) {
+ this();
this.source = source;
}
diff --git a/src/main/java/no/nibio/vips/logic/modules/barleynetblotch/BarleyNetBlotchModelService.java b/src/main/java/no/nibio/vips/logic/modules/barleynetblotch/BarleyNetBlotchModelService.java
index 9241bd6d341b4787229bffe1286051e7ed113810..9a4545b3657d9b00a2f8a3e161dc470f03085b5e 100755
--- a/src/main/java/no/nibio/vips/logic/modules/barleynetblotch/BarleyNetBlotchModelService.java
+++ b/src/main/java/no/nibio/vips/logic/modules/barleynetblotch/BarleyNetBlotchModelService.java
@@ -196,7 +196,6 @@ public class BarleyNetBlotchModelService {
if ("weatherstation".equals(weatherDataSourceType.trim())) {
PointOfInterestWeatherStation weatherStation =
em.find(PointOfInterestWeatherStation.class, weatherStationId);
- LOGGER.info("Run model with weatherdata from weatherstation {}", weatherStation.getName());
try {
observations = wsdUtil.getWeatherObservations(
weatherStation,
diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
index 2065fbd264fa236f08f46c8450de6c9774b6e487..09f31ed6ec7594ce820482b1b121d09b9673aa93 100755
--- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java
+++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
@@ -578,13 +578,10 @@ public class ObservationService {
public Response getObservationsForUser(
@QueryParam("observationIds") String observationIds
) {
- LOGGER.info("getObservationsForUser for observationIds={}", observationIds);
try {
VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest);
if (user != null) {
- LOGGER.info("Get observations for user={}", user.getUserId());
List<Observation> allObs = observationBean.getObservationsForUser(user);
- LOGGER.info("Found {} observations for user {}", allObs.size(), user.getUserId());
if (observationIds != null) {
Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream()
.map(s -> Integer.valueOf(s))
@@ -991,8 +988,6 @@ public class ObservationService {
public Response syncObservationFromApp(
String observationJson
) {
- LOGGER.info("In syncObservationFromApp");
-
try {
VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest);
if (user == null) {
@@ -1119,8 +1114,8 @@ public class ObservationService {
String disableMessagingSystemProperty = System.getProperty("no.nibio.vips.logic.DISABLE_MESSAGING_SYSTEM");
boolean messagingSystemDisabled = disableMessagingSystemProperty != null && disableMessagingSystemProperty.equals("true");
- LOGGER.info("Send notification? " + sendNotification);
- LOGGER.info("Messaging system enabled? " + !messagingSystemDisabled);
+ LOGGER.info("Send notification? {}", sendNotification);
+ LOGGER.info("Messaging system enabled? {}", !messagingSystemDisabled);
// All transactions finished, we can send notifications
// if conditions are met
diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java b/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java
index e18d4d5333d34da98f695329e3c269f0aa84408c..f92c6340fa819562bc4da6357f286a70eb2549de 100644
--- a/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java
+++ b/src/main/java/no/nibio/vips/logic/service/ObservationTimeSeriesService.java
@@ -176,7 +176,6 @@ public class ObservationTimeSeriesService {
@Produces(APPLICATION_JSON)
@TypeHint(ObservationTimeSeries.class)
public Response syncObservationTimeSeriesFromApp(String jsonOts) {
- LOGGER.info("In syncObservationTimeSeriesFromApp");
LOGGER.info(jsonOts);
VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest);
if (user == null) {
@@ -241,7 +240,6 @@ public class ObservationTimeSeriesService {
LOGGER.error("The observation time series is missing location data");
return Response.status(Response.Status.BAD_REQUEST).entity("The observation time series is missing location data").build();
}
- LOGGER.info("otsToSave before storing: " + otsToSave);
otsToSave = observationTimeSeriesBean.storeObservationTimeSeries(otsToSave);
return Response.ok().entity(otsToSave).build();
} catch (IOException e) {
diff --git a/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java b/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java
index 30b120b4e47eb54d3c0df15e9c2ddaab6794b3f0..dd9eb671e3f25f4825265a19beb7154542e1467b 100755
--- a/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java
+++ b/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java
@@ -73,7 +73,6 @@ public class WeatherDataSourceUtil {
*/
public List<WeatherObservation> getWeatherObservations(PointOfInterestWeatherStation station, Integer logIntervalId, String[] elementMeasurementTypes, Date startTime, Date endTime, Boolean ignoreErrors, Set<Integer> toleratedLogIntervalIds) throws WeatherDataSourceException {
// Get measured (and possibly forecasted, depending on the data source) observations
- LOGGER.info("Get weather observations for {} with URL {}", station.getName(), station.getDataFetchUri());
List<WeatherObservation> observations = this.getWeatherObservations(station.getDataFetchUri(), logIntervalId, elementMeasurementTypes, startTime, endTime, TimeZone.getTimeZone(station.getTimeZone()), ignoreErrors, toleratedLogIntervalIds);
Collections.sort(observations);
// Append forecasts, if available
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
index e8b9b640b676d1c744d36c4e6df32168766aa44d..c2b5274f2470d1dda5eae191d7be67eda9691ae1 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -1088,3 +1088,20 @@ observationCount=Observation count
weatherDatasource=Weather datasource
useWeatherStation=Use weather station
multipleNewWarningMsg=This form is for adding the same forecast configuration to many weather stations simultaneously.
+
+timeSeriesList = Time series
+addNewTimeSeries = Add new time series
+forTimeSeries=for time series
+newObservationTimeSeries=New observation time series
+editObservationTimeSeries=Edit observation time series
+addNewObservationInTimeSeries=Add observation to time series
+observationTimeSeriesName=Title
+observationTimeSeriesDescription=Description
+year=Year
+observationTimeSeriesDeleted = Observation time series was deleted
+observationTimeSeriesStored = Observation time series was stored
+noTimeSeries = No observation time series
+noObservations = No observations
+noForecasts = No forecasts
+timeSeriesNoAvailablePoi=You must create a location before you can register a new time series
+
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
index 69c8db61d34bb071bc609b7776cb5b8a21836e44..3488de80066796a82efd6a84e41db53d5215894f 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
@@ -1087,3 +1087,19 @@ observationCount=Antall observasjoner
weatherDatasource=V\u00e6rdatakilde
useWeatherStation=Bruk v\u00e6rstasjon
multipleNewWarningMsg=Dette skjemaet er en snarvei for \u00e5 opprette likelydende varsel p\u00e5 flere v\u00e6rstasjoner. <a href="/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=-1">Klikk her</a> hvis du vil opprette bare ett varsel.
+
+timeSeriesList = Tidsserier
+addNewTimeSeries = Legg til ny tidsserie
+forTimeSeries=for tidsserie
+newObservationTimeSeries=Ny tidsserie
+editObservationTimeSeries=Rediger tidsserie
+addNewObservationInTimeSeries=Legg til ny observasjon i tidsserien
+observationTimeSeriesName=Tittel
+observationTimeSeriesDescription=Beskrivelse
+year=\u00c5r
+observationTimeSeriesDeleted = Tidsserie slettet
+observationTimeSeriesStored = Tidsserie lagret
+noTimeSeries = Ingen tidsserier
+noObservations = Ingen observasjoner
+noForecasts = Ingen varsler
+timeSeriesNoAvailablePoi=Du m� opprette et nytt sted f�r du kan registrere tidsserie
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index d532ad2e29b7e57402b21ae34999fefc96e3f58d..85ee5b44c4b2c05cdfe756599fabf8f617b6761b 100755
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -59,6 +59,10 @@
<servlet-name>MessageController</servlet-name>
<servlet-class>no.nibio.vips.logic.controller.servlet.MessageController</servlet-class>
</servlet>
+ <servlet>
+ <servlet-name>ObservationTimeSeriesServlet</servlet-name>
+ <servlet-class>no.nibio.vips.logic.controller.servlet.ObservationTimeSeriesController</servlet-class>
+ </servlet>
<servlet>
<servlet-name>ObservationServlet</servlet-name>
<servlet-class>no.nibio.vips.logic.controller.servlet.ObservationController</servlet-class>
@@ -139,6 +143,10 @@
<servlet-name>MessageController</servlet-name>
<url-pattern>/message</url-pattern>
</servlet-mapping>
+ <servlet-mapping>
+ <servlet-name>ObservationTimeSeriesServlet</servlet-name>
+ <url-pattern>/observationTimeSeries</url-pattern>
+ </servlet-mapping>
<servlet-mapping>
<servlet-name>ObservationServlet</servlet-name>
<url-pattern>/observation</url-pattern>
diff --git a/src/main/webapp/formdefinitions/observationForm.json b/src/main/webapp/formdefinitions/observationForm.json
index 82fa5aab502ab2eecb64c19152d57259c4d49487..70d4e4959ca83d1d38f464c95992ab7068be10c8 100755
--- a/src/main/webapp/formdefinitions/observationForm.json
+++ b/src/main/webapp/formdefinitions/observationForm.json
@@ -24,6 +24,11 @@
"dataType" : "INTEGER",
"required" : true
},
+ {
+ "name" : "observationTimeSeriesId",
+ "dataType" : "INTEGER",
+ "required" : false
+ },
{
"name" : "organismId",
"dataType" : "INTEGER",
diff --git a/src/main/webapp/formdefinitions/observationTimeSeriesForm.json b/src/main/webapp/formdefinitions/observationTimeSeriesForm.json
new file mode 100644
index 0000000000000000000000000000000000000000..1510bfb16b965af2a1b33bfc294e7ac0f6d317a9
--- /dev/null
+++ b/src/main/webapp/formdefinitions/observationTimeSeriesForm.json
@@ -0,0 +1,70 @@
+{
+ "_licenseNote": [
+ "Copyright (c) 2014 NIBIO <http://www.nibio.no/>. ",
+ "",
+ "This file is part of VIPSLogic. ",
+
+ "This program is free software: you can redistribute it and/or modify",
+ "it under the terms of the GNU Affero General Public License as published by",
+ "the Free Software Foundation, either version 3 of the License, or",
+ "(at your option) any later version.",
+
+ "This program is distributed in the hope that it will be useful,",
+ "but WITHOUT ANY WARRANTY; without even the implied warranty of",
+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the",
+ "GNU Affero General Public License for more details.",
+
+ "You should have received a copy of the GNU Affero General Public License",
+ "along with this program. If not, see <https://www.gnu.org/licenses/>."
+ ],
+ "_comment" : "Structure of the observationTimeSeriesForm and how to validate it",
+ "fields": [
+ {
+ "name" : "observationTimeSeriesId",
+ "dataType" : "INTEGER",
+ "required" : true
+ },
+ {
+ "name" : "organismId",
+ "dataType" : "INTEGER",
+ "fieldType" : "SELECT_SINGLE",
+ "required" : true,
+ "nullValue" : "-1"
+ },
+ {
+ "name" : "cropOrganismId",
+ "dataType" : "INTEGER",
+ "fieldType" : "SELECT_SINGLE",
+ "required" : true,
+ "nullValue" : "-1"
+ },
+ {
+ "name" : "year",
+ "dataType" : "INTEGER",
+ "timestampFormat" : "SELECT_SINGLE",
+ "required" : true
+ },
+ {
+ "name" : "locationPointOfInterestId",
+ "dataType" : "INTEGER",
+ "fieldType" : "SELECT_SINGLE",
+ "required" : true
+ },
+ {
+ "name" : "locationVisibility",
+ "dataType" : "STRING",
+ "fieldType" : "RADIO",
+ "required" : true
+ },
+ {
+ "name" : "name",
+ "dataType" : "STRING",
+ "required" : true
+ },
+ {
+ "name" : "description",
+ "dataType" : "STRING",
+ "required" : false
+ }
+ ]
+}
diff --git a/src/main/webapp/js/objectGISInfoMap.js b/src/main/webapp/js/objectGISInfoMap.js
index cebeafc053913e93c6fe1f4ad7a11297f491a5f2..5499fea446bd833afae878ed47d8badf3b00210f 100755
--- a/src/main/webapp/js/objectGISInfoMap.js
+++ b/src/main/webapp/js/objectGISInfoMap.js
@@ -662,6 +662,9 @@ var app = window.app;
* @return {GEOJson} returns all features currently on the map in GEOJson format
*/
function getFeatures() {
+ if(!featureOverlay){
+ return null;
+ }
var features = featureOverlay.getSource().getFeatures();
var format = new ol.format.GeoJSON();
// write features to GeoJSON format using projection EPSG:4326
diff --git a/src/main/webapp/templates/master.ftl b/src/main/webapp/templates/master.ftl
index 712e6aafac85f7a6ec85871ea3f78bac317a28ec..ef26ccca828d61750d9541c7e9782179273b347a 100755
--- a/src/main/webapp/templates/master.ftl
+++ b/src/main/webapp/templates/master.ftl
@@ -66,9 +66,12 @@
<#if user.isOrganizationAdmin() || user.isSuperUser() || user.isMessageAuthor()>
<li><a href="/message">${i18nBundle.messages}</a></li>
</#if>
- <#if user.isOrganizationAdmin() || user.isSuperUser() || user.isAppleFruitMothAdministrator() || user.hasRole(3?int) || user.hasRole(4?int)>
- <li><a href="/observation">${i18nBundle.observations}</a></li>
- </#if>
+ <#if user.isOrganizationAdmin() || user.isSuperUser() || user.isAppleFruitMothAdministrator() || user.hasRole(3?int) || user.hasRole(4?int)>
+ <li><a href="/observationTimeSeries">${i18nBundle.timeSeriesList}</a></li>
+ </#if>
+ <#if user.isOrganizationAdmin() || user.isSuperUser() || user.isAppleFruitMothAdministrator() || user.hasRole(3?int) || user.hasRole(4?int)>
+ <li><a href="/observation">${i18nBundle.observations}</a></li>
+ </#if>
<li><a href="/forecastConfiguration">${i18nBundle.forecasts}</a></li>
<#if user.isOrganizationAdmin() || user.isSuperUser() || user.isOrganismEditor()>
<li><a href="/organism">${i18nBundle.organisms}</a></li>
diff --git a/src/main/webapp/templates/observationForm.ftl b/src/main/webapp/templates/observationForm.ftl
index d4cd9e01b7de265fff78eb981a2d5bd1f49f0bba..043d77f9cb5f92c195b36f9b144950d2e169a531 100755
--- a/src/main/webapp/templates/observationForm.ftl
+++ b/src/main/webapp/templates/observationForm.ftl
@@ -44,7 +44,6 @@
var organizationId = ${user.organizationId.organizationId};
var selectedCropId = <#if observation.cropOrganism?has_content>${observation.cropOrganism.organismId?c}<#else>null</#if>;
-
$(document).ready(function () {
// Load main form definition (for validation)
@@ -54,29 +53,27 @@
// If observation already registered center on location
// Otherwise, center and zoom to organizations's default
<#if observation.location?has_content>
- initGisInfoMap([${(observation.location.x?c)!""}, ${(observation.location.y?c)!""}], 10, true);
+ initGisInfoMap([${(observation.location.x?c)!""}, ${(observation.location.y?c)!""}], 10, true);
<#elseif !observation.observationTimeSeries?has_content>
- var geoInfo = <#if observation.geoinfo?has_content>${observation.geoinfo}<#else>
- {
- }
+ var geoInfo = <#if observation.geoinfo?has_content>${observation.geoinfo}<#else>{}
</#if>;
var chooseFromMapLayers = {"chooseFromMapLayers": <#if mapLayers?has_content>${mapLayers}<#else>[]</#if>};
initGisInfoMap("observationFormMap", [${defaultMapCenter.x?c}, ${defaultMapCenter.y?c}], ${defaultMapZoom}, false, geoInfo, chooseFromMapLayers);
</#if>
<#if editAccess!="W">
- GISInfoMap.removeInteraction(draw);
- document.getElementById("drawOptions").innerHTML = "";
+ GISInfoMap.removeInteraction(draw);
+ document.getElementById("drawOptions").innerHTML = "";
</#if>
<#if observation.observationData?has_content>
- observationData = ${observation.observationData};
- getDataSchema(${observation.organism.organismId}, organizationId);
+ observationData = ${observation.observationData};
+ getDataSchema(${observation.organism.organismId}, organizationId);
<#elseif observation.organism?has_content>
- // Setting
- initObservationData(${observation.organism.organismId}, organizationId);
+ initObservationData(${observation.organism.organismId}, organizationId);
+ <#elseif observation.observationTimeSeries?has_content>
+ initObservationData(${observation.observationTimeSeries.organismId}, organizationId);
</#if>
-
<#if observation.observationTimeSeries?has_content>
- displayObservationTimeSeriesInfo("${observation.cropOrganismId}", "${observation.organismId}", "${observation.locationPointOfInterestId}")
+ displayObservationTimeSeriesInfo("${observation.observationTimeSeries.cropOrganismId}", "${observation.observationTimeSeries.organismId}", "${observation.observationTimeSeries.locationPointOfInterestId}")
</#if>
// Activating file selection
@@ -94,8 +91,8 @@
});
refreshLocationPointOfInterests(<#if observation.locationPointOfInterestId?has_content>${observation.locationPointOfInterestId}<#else>null</#if>);
- <#if ! observation.organism?has_content>
- initCropCategories();
+ <#if !observation.organism?has_content>
+ initCropCategories();
</#if>
// Activating chosen plugin
$(".chosen-select").chosen();
@@ -139,7 +136,7 @@
const locationName = nameForLocationPointOfInterestId(locationPointOfInterestId, allPois);
document.getElementById("locationDisplayName").innerHTML = locationName || null;
});
- initLocationMap(locationPointOfInterestId)
+ initLocationMap(locationPointOfInterestId);
}
function getDataSchema(organismId, organizationId) {
@@ -341,18 +338,21 @@
}
function renderCropCategories() {
- var cropCategoryIdList = document.getElementById("cropCategoryIdList");
- for (var i in cropCategories) {
- var cropCategory = cropCategories[i];
+ let cropCategoryIdList = document.getElementById("cropCategoryIdList");
+ if(!cropCategoryIdList) {
+ return;
+ }
+ for (let i in cropCategories) {
+ let cropCategory = cropCategories[i];
// Best effort getting name for crop category
- var catName = cropCategory.defaultName;
- for (var j in cropCategory.cropCategoryLocalSet) {
- var cLocal = cropCategory.cropCategoryLocalSet[j];
+ let catName = cropCategory.defaultName;
+ for (let j in cropCategory.cropCategoryLocalSet) {
+ let cLocal = cropCategory.cropCategoryLocalSet[j];
if (cLocal.cropCategoryLocalPK.locale == "${currentLocale}") {
catName = cLocal.localName;
}
}
- var cOption = new Option(catName, cropCategory.cropCategoryId);
+ let cOption = new Option(catName, cropCategory.cropCategoryId);
cropCategoryIdList.options[cropCategoryIdList.options.length] = cOption;
}
}
@@ -439,7 +439,6 @@
}
<#if noBroadcast>
-
/**
* In the unlikely event that the user in a pre-filled form decides to
* change the organism, we'll update the observationText and heading
@@ -449,7 +448,6 @@
document.getElementById("observationHeading").value = registrationOfText;
document.getElementById("observationText").value = registrationOfText;
}
-
</#if>
var cropList = [
<#if ! observation.observationId?has_content || user.isSuperUser() || user.isOrganizationAdmin()>
@@ -487,8 +485,6 @@
* Does all the ifs and buts before form can potentially be submitted
*/
function prepareFormSubmit(theForm) {
- // Extract GIS info from OpenLayers
- theForm['geoInfo'].value = getFeatures();
try {
// If the form is quantified: Inspect the fields and write
// JSON string to the generic form field "observationData";
@@ -500,10 +496,15 @@
}
theForm['observationData'].value = JSON.stringify(editor.getValue());
}
- validateGIS(theForm);
+ // Extract GIS info from OpenLayers
+ let theFeatures = getFeatures();
+ if (theFeatures) {
+ theForm['geoInfo'].value = theFeatures;
+ return validateForm(theForm) && validateGIS(theForm);
+ }
+ return validateForm(theForm);
//console.info('validateGIS = ' + (validateGIS(this)));
//return false; // DEBUG setting
- return validateForm(theForm) && validateGIS(theForm); // PROD setting
} catch (e) {
console.log(e.message);
console.log(e);
@@ -514,11 +515,11 @@
</#macro>
<#macro page_contents>
<div class="singleBlockContainer">
- <p><a href="/observation" class="btn btn-default back"
- role="button">${i18nBundle.back}</a><#if observation.observationId?has_content><a
- href="/observation?action=newObservationForm" class="btn btn-default"
+ <p><a href="${returnPath}" class="btn btn-default back" role="button">${i18nBundle.back}</a>
+ <#if observation.observationId?has_content><a
+ href="/observation?action=newObservationForm<#if observation.observationTimeSeriesId?has_content>&observationTimeSeriesId=${observation.observationTimeSeriesId}</#if>" class="btn btn-default"
role="button">${i18nBundle.addNew}</a></#if></p>
- <h1><#if observation.observationId?has_content>${i18nBundle.editObservation}<#else>${i18nBundle.newObservation}</#if><#if shortcut?has_content> - ${shortcut.getLocalLabel(currentLocale.language)?lower_case}</#if></h1>
+ <h1><#if observation.observationId?has_content>${i18nBundle.editObservation}<#else>${i18nBundle.newObservation}</#if><#if observation.observationTimeSeriesId?has_content> ${i18nBundle.forTimeSeries}</#if><#if shortcut?has_content> - ${shortcut.getLocalLabel(currentLocale.language)?lower_case}</#if></h1>
<div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>>
<#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if>
</div>
@@ -528,7 +529,7 @@
<div class="row">
<div class="col-md-6">
<#assign formId = "observationForm">
- <form id="${formId}" role="form" action="/observation?action=observationFormSubmit"
+ <form id="${formId}" role="form" action="/observation?action=observationFormSubmit<#if returnTo?has_content>&returnTo=${returnTo}</#if>"
enctype="multipart/form-data" method="POST" onsubmit="return prepareFormSubmit(this);">
<!--form id="${formId}" role="form" action="/observation?action=observationFormSubmit" method="POST" onsubmit="this['geoInfo'].value=getFeatures();mw.save();console.log(this['observationData']);this['observationData'].value=JSON.stringify(mw.toInspect);return validateForm(this);"-->
<!--form id="${formId}" role="form" action="/observation?action=observationFormSubmit" method="POST" onsubmit="this['geoInfo'].value=getFeatures();mw.save();console.log(this['observationData']);this['observationData'].value=JSON.stringify(mw.toInspect);validateForm(this);return false;"-->
@@ -549,26 +550,45 @@
</div>
</#if>
<#if observation.observationTimeSeries?has_content>
- <div class="form-group">
- <label>${i18nBundle.timeSeries}: ${observation.observationTimeSeries.name}</label><br>
+ <div class="alert alert-info">
<i>${i18nBundle.timeSeriesNonEditable}</i>
</div>
<div class="form-group">
- <label for="cropOrganismId">${i18nBundle.cropOrganismId}</label>
+ <label for="timeSeriesName">${i18nBundle.timeSeries}:</label>
+ <span id="timeSeriesName"><a href="/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=${observation.observationTimeSeries.observationTimeSeriesId}">${observation.observationTimeSeries.name} (${observation.observationTimeSeries.year})</a></span>
+ </div>
+ <div class="form-group">
+ <label for="cropOrganismId">${i18nBundle.cropOrganismId}:</label>
<span id="cropDisplayName"></span>
</div>
<div class="form-group">
- <label for="organismId">${i18nBundle.organism}</label>
+ <label for="organismId">${i18nBundle.organism}:</label>
<span id="pestDisplayName"></span>
</div>
<div class="form-group">
- <label for="locationPointOfInterestId">${i18nBundle.location}</label>
+ <label for="locationPointOfInterestId">${i18nBundle.location}:</label>
<span id="locationDisplayName"></span>
</div>
- <input type="hidden" name="cropOrganismId" value="${observation.cropOrganism.organismId}">
- <input type="hidden" name="organismId" value="${observation.organism.organismId}">
- <input type="hidden" name="locationPointOfInterestId"
- value="${observation.locationPointOfInterestId}">
+ <div class="form-group">
+ <label for="locationVisibility">Synlighet:</label>
+ <span id="locationVisibility">
+ <#if locationVisibilityFormValue == 'public'>
+ ${i18nBundle.locationIsPublic}
+ <#elseif locationVisibilityFormValue == 'private'>
+ ${i18nBundle.locationIsPrivate}
+ <#else>
+ <#list polygonServices as polygonService>
+ <#if locationVisibilityFormValue == "mask_" + polygonService.polygonServiceId>
+ ${i18nBundle.maskObservationWith} ${polygonService.polygonServiceName}
+ </#if>
+ </#list>
+ </#if>
+ </span>
+ </div>
+ <input type="hidden" name="observationTimeSeriesId" value="${observation.observationTimeSeries.observationTimeSeriesId}">
+ <input type="hidden" name="cropOrganismId" value="${observation.observationTimeSeries.cropOrganism.organismId}">
+ <input type="hidden" name="organismId" value="${observation.observationTimeSeries.organism.organismId}">
+ <input type="hidden" name="locationPointOfInterestId" value="${observation.observationTimeSeries.locationPointOfInterestId}">
<input type="hidden" name="locationVisibility" value="${locationVisibilityFormValue}">
<#else>
<#if ! observation.organism?has_content>
@@ -681,7 +701,7 @@
<#setting time_zone=user.organizationId.defaultTimeZone!"UTC">
<div class="form-group">
<label for="timeOfObservation">${i18nBundle.timeOfObservation}</label>
- <input type="date" class="form-control" id="timeOfObservation" name="timeOfObservation" placeholder="${i18nBundle.timeOfObservation}" value="${observation.timeOfObservation?string("yyyy-MM-dd")!""}" onblur="validateField(this);" <#if editAccess!="W">readonly="readonly"</#if>/>
+ <input type="date" <#if observation.observationTimeSeries?has_content>min="${minTimeOfObservation?string("yyyy-MM-dd")}" max="${maxTimeOfObservation?string("yyyy-MM-dd")}"</#if> class="form-control" id="timeOfObservation" name="timeOfObservation" placeholder="${i18nBundle.timeOfObservation}" value="${observation.timeOfObservation?string("yyyy-MM-dd")}" onblur="validateField(this);" <#if editAccess!="W">readonly="readonly"</#if>/>
<span class="help-block" id="${formId}_timeOfObservation_validation"></span>
</div>
<div class="form-group">
@@ -716,10 +736,10 @@
<div class="form-group">
<div class="checkbox">
<#if editAccess!="W" && observation.broadcastMessage?has_content && observation.broadcastMessage ==true>
- <input type="hidden" name="isQuantified" value="true"/></#if>
+ <input type="hidden" name="broadcastMessage" value="true"/></#if>
<label>
<input type="checkbox" name="broadcastMessage"
- <#if (observation.broadcastMessage?has_content && observation.broadcastMessage == false) || noBroadcast><#else>checked="checked"</#if> <#if editAccess!="W">disabled="disabled"</#if>/>
+ <#if (!observation.observationId?has_content && observation.observationTimeSeries?has_content) || (observation.broadcastMessage?has_content && observation.broadcastMessage == false) || noBroadcast><#else>checked="checked"</#if> <#if editAccess!="W">disabled="disabled"</#if>/>
</label>
${i18nBundle.broadcastMessage}
</div>
@@ -789,7 +809,7 @@
<button type="submit" class="btn btn-default">${i18nBundle.submit}</button>
<#if observation.observationId?has_content && editAccess == "W">
<button type="button" class="btn btn-danger"
- onclick="if(confirm('${i18nBundle.confirmDelete}')){window.location.href='/observation?action=deleteObservation&observationId=${observation.observationId}';}">${i18nBundle.delete}</button>
+ onclick="if(confirm('${i18nBundle.confirmDelete}')){window.location.href='/observation?action=deleteObservation&observationId=${observation.observationId}<#if returnTo?has_content>&returnTo=${returnTo}</#if>';}">${i18nBundle.delete}</button>
</#if>
</form>
</div>
diff --git a/src/main/webapp/templates/observationList.ftl b/src/main/webapp/templates/observationList.ftl
index 90db0a5a6acc884a9626c53ad5ff2f3722113ce6..93845dab49518442b0c805798d4613f31b2d064c 100755
--- a/src/main/webapp/templates/observationList.ftl
+++ b/src/main/webapp/templates/observationList.ftl
@@ -117,22 +117,24 @@
<thead>
<th>${i18nBundle.timeOfObservation}</th>
<th>${i18nBundle.organism}</th>
- <th>${i18nBundle.cropOrganismId}</th>
- <th>${i18nBundle.location}</th>
- <th>${i18nBundle.observer}</th>
- <th>${i18nBundle.heading}</th>
+ <th>${i18nBundle.cropOrganismId}</th>
+ <th>${i18nBundle.location}</th>
+ <th>${i18nBundle.observer}</th>
+ <th>${i18nBundle.heading}</th>
+ <th>Tidsserie</th>
<th>${i18nBundle.status}</th>
<th></th>
</thead>
<tbody>
<#list observations as observation>
<tr>
- <td>${observation.timeOfObservation?string("yyyy-MM-dd HH:mmZ")}</td>
+ <td>${observation.timeOfObservation?string("yyyy-MM-dd")}</td>
<td>${observation.organism.latinName!""}/${observation.organism.getLocalName(currentLocale.language)!""}</td>
- <td>${observation.cropOrganism.latinName!""}/${observation.cropOrganism.getLocalName(currentLocale.language)!""}</td>
+ <td>${observation.cropOrganism.latinName!""}/${observation.cropOrganism.getLocalName(currentLocale.language)!""}</td>
<td><#if observation.location?has_content>${observation.location.name!""}</#if></td>
- <td><#if observation.user?has_content>${observation.user.firstName!""} ${observation.user.lastName!""}</#if></td>
- <td>${observation.observationHeading}</td>
+ <td><#if observation.user?has_content>${observation.user.firstName!""} ${observation.user.lastName!""}</#if></td>
+ <td>${observation.observationHeading}</td>
+ <td><#if observation.observationTimeSeries?has_content><a href="/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=${observation.observationTimeSeriesId}">${observation.observationTimeSeries.name}</a></#if></td>
<td><#switch observation.statusTypeId><#case 1>${i18nBundle.pending}<#break><#case 2>${i18nBundle.rejected}<#break><#case 3>${i18nBundle.approved}</#switch></td>
<td><#if user.isSuperUser() || user.isOrganizationAdmin() || userIsObservationAuthority || observation.userId == user.userId><a href="/observation?action=editObservationForm&observationId=${observation.observationId}" class="btn btn-default" role="button">${i18nBundle.edit}</a></#if></td>
</tr>
diff --git a/src/main/webapp/templates/observationTimeSeriesForm.ftl b/src/main/webapp/templates/observationTimeSeriesForm.ftl
new file mode 100644
index 0000000000000000000000000000000000000000..3877d87e493dbefde8cdecd546c0de178641726d
--- /dev/null
+++ b/src/main/webapp/templates/observationTimeSeriesForm.ftl
@@ -0,0 +1,540 @@
+<#include "master.ftl">
+<#macro page_head>
+ <title><#if observationTimeSeries.observationTimeSeriesId?has_content>${i18nBundle.editObservation}<#else>${i18nBundle.newObservation}</#if></title>
+</#macro>
+<#macro custom_css>
+ <link rel="stylesheet" type="text/css" href="/css/3rdparty/jquery.datetimepicker.css"/>
+ <link rel="stylesheet" type="text/css" href="/css/3rdparty/ol.css?t=20170623"/>
+ <link rel="stylesheet" type="text/css" href="/css/3rdparty/ol-layerswitcher.css"/>
+ <link rel="stylesheet" type="text/css" href="/css/map.css"/>
+</#macro>
+<#macro custom_js>
+<script type="text/javascript" src="/js/3rdparty/modernizr_custom.js"></script>
+<script type="text/javascript" src="/js/3rdparty/moment.min.js"></script>
+<script type="text/javascript" src="/js/3rdparty/jsoneditor.js"></script>
+<script type="text/javascript" src="/js/3rdparty/ol.js?t=20170623"></script>
+<script type="text/javascript" src="js/3rdparty/ol-layerswitcher.js"></script>
+
+<script type="text/javascript" src="/js/constants.js"></script>
+<script type="text/javascript" src="/js/resourcebundle.js"></script>
+<script type="text/javascript" src="/js/validateForm.js"></script>
+<script type="text/javascript" src="/js/util.js"></script>
+<script type="text/javascript" src="/js/objectGISInfoMap.js?t=20170623"></script>
+<script type="text/javascript" src="/js/poiFormMap.js"></script>
+<script>
+ let organizationId = ${user.organizationId.organizationId};
+ let selectedCropId = <#if observationTimeSeries.cropOrganism?has_content>${observationTimeSeries.cropOrganism.organismId?c}<#else>null</#if>;
+
+ $(document).ready(function () {
+ loadFormDefinition("observationTimeSeriesForm");
+ refreshLocationPointOfInterests(<#if observationTimeSeries.locationPointOfInterestId?has_content>${observationTimeSeries.locationPointOfInterestId}<#else>null</#if>);
+ <#if !observationTimeSeries.organism?has_content>
+ initCropCategories();
+ </#if>
+ <#if isEditable?has_content && !isEditable>
+ displayObservationTimeSeriesInfo("${observationTimeSeries.cropOrganismId}", "${observationTimeSeries.organismId}", "${observationTimeSeries.locationPointOfInterestId}")
+ </#if>
+ filterCrops(-1);
+ });
+
+ let thePopup;
+ function addNewLocationPopup() {
+ thePopup = window.open("/poi?action=newPoiForm&organizationId=1&returnIdCallback=refreshLocationPointOfInterests");
+ }
+
+ let allPois = [];
+ function fetchPOIs(callback) {
+ $.getJSON("/rest/poi/user", function (json) {
+ callback(json);
+ });
+ }
+
+ function refreshLocationPointOfInterests(selectedPointOfInterestId) {
+ const poiTypes = {
+ 0: "${i18nBundle.genericPlaces}",
+ 2: "${i18nBundle.farms}",
+ 3: "${i18nBundle.fields}",
+ 5: "${i18nBundle.traps}"
+ };
+ const poiListElement = document.getElementById("locationPointOfInterestId");
+ if (!poiListElement) {
+ return;
+ }
+
+ fetchPOIs(function (allPois) {
+ buildPOIList(poiListElement, allPois, poiTypes, selectedPointOfInterestId, ${user.userId});
+ showCorrectMap();
+ });
+ }
+
+ function buildPOIList(poiListElement, allPois, poiTypes, selectedPointOfInterestId, userId) {
+ poiListElement.options.length = 1;
+ const userPois = allPois.filter(poi => String(poi.userId) === String(userId));
+ if(userPois.length === 0) {
+ document.getElementById("noAvailablePoi").style.display = "block";
+ } else {
+ document.getElementById("noAvailablePoi").style.display = "none";
+ }
+ for (const [typeId, typeName] of Object.entries(poiTypes)) {
+ const poisOfType = userPois.filter(poi => String(poi.pointOfInterestTypeId) === String(typeId));
+ if (poisOfType.length === 0) continue;
+ const poiTypeOption = new Option("-- " + typeName + " --", "");
+ poiTypeOption.disabled = true;
+ poiListElement.options[poiListElement.options.length] = poiTypeOption;
+
+ for (const poi of poisOfType) {
+ const poiOption = new Option(poi.name, poi.pointOfInterestId);
+ if (poi.pointOfInterestId === selectedPointOfInterestId) {
+ poiOption.selected = true;
+ }
+ poiListElement.options[poiListElement.options.length] = poiOption;
+ }
+ }
+ }
+
+ function initLocationMap(locationPointOfInterestId) {
+ if (locationPointOfInterestId != null) {
+ $.getJSON("/rest/poi/" + locationPointOfInterestId, function (json) {
+ var poi = json;
+ if (map === undefined || map === null) {
+ initMap([poi.longitude, poi.latitude], 10, ${user.organizationId.organizationId}, locationPointOfInterestId, true);
+ } else {
+ map.getView().setCenter(ol.proj.transform([poi.longitude, poi.latitude], 'EPSG:4326', map.getView().getProjection().getCode()));
+ map.getView().setZoom(10);
+ stationMarker.setPosition(ol.proj.transform([poi.longitude, poi.latitude], 'EPSG:4326', map.getView().getProjection().getCode()));
+ }
+ }
+ );
+ } else {
+ initMap([${defaultMapCenter.x?c}, ${defaultMapCenter.y?c}], ${defaultMapZoom}, ${user.organizationId.organizationId}, locationPointOfInterestId, true);
+ }
+ }
+
+ function showCorrectMap() {
+ let locationList = document.getElementById("locationPointOfInterestId");
+ if (locationList.options[locationList.options.selectedIndex].value === "-1") {
+ document.getElementById("poiFormMap").style.display = "block";
+ initLocationMap(null);
+ } else {
+ document.getElementById("poiFormMap").style.display = "block";
+ initLocationMap(locationList.options[locationList.options.selectedIndex].value);
+ }
+ }
+
+ /**
+ * Crop, pest and location should not be editable for time series with observations. We avoid displaying
+ * form elements in these cases, and rather display the values statically.
+ */
+ function displayObservationTimeSeriesInfo(cropOrganismId, organismId, locationPointOfInterestId) {
+ document.getElementById("cropDisplayName").innerHTML = nameForCropOrganismId(cropOrganismId);
+ document.getElementById("pestDisplayName").innerHTML = nameForOrganismId(organismId);
+ fetchPOIs(function (allPois) {
+ const locationName = nameForLocationPointOfInterestId(locationPointOfInterestId, allPois);
+ document.getElementById("locationDisplayName").innerHTML = locationName || null;
+ });
+ initLocationMap(locationPointOfInterestId)
+ }
+
+ // Return name for given cropOrganismId
+ function nameForCropOrganismId(cropOrganismId) {
+ const crop = cropList.find(crop => crop.organismId == cropOrganismId);
+ return crop ? crop.displayName : null;
+ }
+
+ // Return name for given organismId
+ function nameForOrganismId(organismId) {
+ const pest = organismList.find(pest => pest.organismId == organismId);
+ return pest ? pest.displayName : null;
+ }
+
+ // Return name for given point of interest id
+ function nameForLocationPointOfInterestId(pointOfInterestId, allPois) {
+ const location = allPois.find(loc => loc.pointOfInterestId == pointOfInterestId);
+ return location ? location.name : null;
+ }
+
+ let cropCategories = [];
+
+ function initCropCategories() {
+ $.getJSON("/rest/organism/cropcategory/${user.organizationId.organizationId}", function (json) {
+ cropCategories = json;
+ renderCropCategories();
+ }
+ );
+ }
+
+ function renderCropCategories() {
+ let cropCategoryIdList = document.getElementById("cropCategoryIdList");
+ for (let i in cropCategories) {
+ let cropCategory = cropCategories[i];
+ // Best effort getting name for crop category
+ let catName = cropCategory.defaultName;
+ for (let j in cropCategory.cropCategoryLocalSet) {
+ let cLocal = cropCategory.cropCategoryLocalSet[j];
+ if (cLocal.cropCategoryLocalPK.locale == "${currentLocale}") {
+ catName = cLocal.localName;
+ }
+ }
+ cropCategoryIdList.options[cropCategoryIdList.options.length] = new Option(catName, cropCategory.cropCategoryId);
+ }
+ }
+
+ function filterCrops(selectedCropCategoryId) {
+ // Nothing has been selected, render all in one go
+ if (selectedCropCategoryId < 0) {
+ var cropOptions = [];
+ for (var j = 0; j < cropList.length; j++) {
+ //var cropOption = cropOrganismIdList.options[j];
+ var crop = cropList[j];
+ var cropOption = new Option(crop.displayName, crop.organismId);
+ if (selectedCropId != null && selectedCropId == crop.organismId) {
+ cropOption.selected = true;
+ }
+ cropOptions.push(cropOption);
+
+ }
+ renderCropList(cropOptions, []);
+ } else {
+ // Searching for selected crop category
+ for (var i in cropCategories) {
+ var cropCategory = cropCategories[i];
+ if (cropCategory.cropCategoryId == parseInt(selectedCropCategoryId)) {
+ // Filter based on match
+ var matchingCropOrganismOptions = [];
+ var theRest = [];
+ //var cropOrganismIdList = document.getElementById("cropOrganismIdList");
+ for (var j = 0; j < cropList.length; j++) {
+ //var cropOption = cropOrganismIdList.options[j];
+ var crop = cropList[j];
+ var cropOption = new Option(crop.displayName, crop.organismId);
+ if (selectedCropId != null && selectedCropId == crop.organismId) {
+ cropOption.selected = true;
+ }
+ if (cropCategory.cropOrganismIds.indexOf(crop.organismId) >= 0 && cropCategory.maxHierarchyCategoryId >= crop.hierarchyCategoryId) {
+ matchingCropOrganismOptions.push(cropOption);
+ } else if (crop.organismId > 0) {
+ theRest.push(cropOption);
+ }
+ }
+ renderCropList(matchingCropOrganismOptions, theRest);
+ }
+ }
+ }
+ }
+
+ function renderCropList(matchingCropOrganismOptions, theRest) {
+ let cropOrganismIdList = document.getElementById("cropOrganismIdList");
+ if (!cropOrganismIdList) {
+ // HTML element does not exist for observations in time series
+ return;
+ }
+ cropOrganismIdList.options.length = 1;
+ for (let i in matchingCropOrganismOptions) {
+ cropOrganismIdList.options[cropOrganismIdList.options.length] = matchingCropOrganismOptions[i];
+ }
+ }
+
+ let cropList = [
+ <#if ! observationTimeSeries.observationTimeSeriesId?has_content || user.isSuperUser() || user.isOrganizationAdmin()>
+ {organismId: -10, displayName: "${i18nBundle.missingInDatabase}", hierarchyCategoryId: -1},
+ <#list allCrops as cropOrganism>
+ {
+ organismId: ${cropOrganism.organismId?c},
+ displayName: "${cropOrganism.getLocalName(currentLocale.language)!""} (${cropOrganism.latinName!""}) ${hierarchyCategories.getName(cropOrganism.hierarchyCategoryId)?upper_case}",
+ hierarchyCategoryId: ${cropOrganism.hierarchyCategoryId!"-1"}
+ }<#sep>,
+ </#list>
+ <#else>
+ <#list allCrops as cropOrganism>
+ <#if (observationTimeSeries.cropOrganism?has_content && observationTimeSeries.cropOrganism.organismId == cropOrganism.organismId)>
+ {
+ organismId: ${cropOrganism.organismId},
+ displayName: "${cropOrganism.getLocalName(currentLocale.language)!""} (${cropOrganism.latinName!""}) ${hierarchyCategories.getName(cropOrganism.hierarchyCategoryId)?upper_case}",
+ hierarchyCategoryId: ${cropOrganism.hierarchyCategoryId}
+ }
+ </#if>
+ </#list>
+ </#if>
+ ];
+
+ let organismList = [
+ <#list allPests as organism>
+ {
+ organismId: ${organism.organismId},
+ displayName: "${organism.getLocalName(currentLocale.language)!""} (${organism.latinName!""})"
+ },
+ </#list>
+ ];
+ let updateCropPests = function () {
+ let theForm = document.getElementById('observationTimeSeriesForm');
+ theForm["organismId"].options.selectedIndex = -1;
+
+ let selectedCropId = theForm["cropOrganismId"].options[theForm["cropOrganismId"].options.selectedIndex].value;
+ // If this is not a new observation, or the selected crop is not in the database, we keep calm
+ if (theForm["observationTimeSeriesId"].value !== "-1" || selectedCropId === "-10") {
+ return;
+ }
+ $.getJSON("/rest/organism/croppest/" + selectedCropId, function (json) {
+ updateCropPestsCallback(json);
+ })
+ .fail(function () {
+ alert("Error getting pests for this crop. Please contact system administrator");
+ });
+ };
+
+ let updateCropPestsCallback = function (cropPest) {
+ let pestList = document.getElementById('observationTimeSeriesForm')["organismId"];
+ if (cropPest == null) {
+ // Need to reorganize pests back to default
+ let allPests = [];
+ for (let i = 2; i < pestList.options.length; i++) {
+ allPests.push(pestList.options[i]);
+ }
+ allPests.sort(compareSelectListOptions);
+ pestList.options.length = 2; // Keeping the top two
+ for (let i = 0; i < allPests.length; i++) {
+ pestList.options[pestList.options.length] = allPests[i];
+ }
+ } else {
+ let prioritizedPests = [];
+ for (let i = 2; i < pestList.options.length; i++) {
+ if (cropPest.pestOrganismIds.indexOf(parseInt(pestList.options[i].value)) >= 0) {
+ prioritizedPests.push(pestList.options[i]);
+ }
+ }
+ pestList.options.length = 2; // Keeping the top two
+ for (let i = 0; i < prioritizedPests.length; i++) {
+ pestList.options[pestList.options.length] = prioritizedPests[i];
+ }
+ }
+ };
+</script>
+
+</#macro>
+
+<#macro page_contents>
+<div class="singleBlockContainer">
+ <p><a href="/observationTimeSeries" class="btn btn-default back" role="button">${i18nBundle.back}</a><#if observationTimeSeries.observationTimeSeriesId?has_content><a href="/observation?action=newObservationForm&observationTimeSeriesId=${observationTimeSeries.observationTimeSeriesId}&returnTo=timeseries" class="btn btn-default" role="button">${i18nBundle.addNewObservationInTimeSeries}</a></#if></p>
+ <h1><#if observationTimeSeries.observationTimeSeriesId?has_content>${i18nBundle.editObservationTimeSeries}<#else>${i18nBundle.newObservationTimeSeries}</#if><#if shortcut?has_content> - ${shortcut.getLocalLabel(currentLocale.language)?lower_case}</#if></h1>
+ <div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>>
+ <#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if>
+ </div>
+ <div id="noAvailablePoi" class="alert alert-danger" style="display: none">Du må opprette et nytt sted før du kan registrere tidsserie</div>
+ <#if messageKey?has_content>
+ <div class="alert alert-success">${i18nBundle(messageKey)}</div>
+ </#if>
+ <div class="row">
+ <div class="col-md-6">
+ <#assign formId = "observationTimeSeriesForm">
+ <form id="${formId}" role="form" action="/observationTimeSeries?action=observationTimeSeriesFormSubmit" enctype="multipart/form-data" method="POST">
+ <input type="hidden" name="geoInfo" value=""/>
+ <input type="hidden" name="observationTimeSeriesId" value="${observationTimeSeries.observationTimeSeriesId!"-1"}"/>
+ <#if observationTimeSeries.user?has_content>
+ <div class="form-group">
+ <label>Registrert av</label>
+ <span>${observationTimeSeries.user.firstName} ${observationTimeSeries.user.lastName}</span>
+ </div>
+ </#if>
+ <#if observationTimeSeries.lastModifiedByUser?has_content>
+ <div class="form-group">
+ <label>${i18nBundle.lastEditedBy}</label>
+ <span>${observationTimeSeries.lastModifiedByUser.firstName} ${observationTimeSeries.lastModifiedByUser.lastName}</span>
+ </div>
+ </#if>
+ <#if isEditable?has_content && !isEditable>
+ <div class="alert alert-info">
+ <i>Kultur, organisme, år og sted kan ikke redigeres for tidsserier som inneholder observasjoner.</i>
+ </div>
+ <div class="form-group">
+ <label for="cropDisplayName">${i18nBundle.cropOrganismId}</label>
+ <span id="cropDisplayName"></span>
+ </div>
+ <div class="form-group">
+ <label for="pestDisplayName">${i18nBundle.organism}</label>
+ <span id="pestDisplayName"></span>
+ </div>
+ <div class="form-group">
+ <label>År</label> ${observationTimeSeries.year}
+ </div>
+ <div class="form-group">
+ <label for="locationDisplayName">${i18nBundle.location}</label>
+ <span id="locationDisplayName"></span>
+ </div>
+ <div class="form-group">
+ <label for="locationVisibility">Synlighet</label>
+ <span id="locationVisibility">
+ <#if locationVisibilityFormValue == 'public'>
+ ${i18nBundle.locationIsPublic}
+ <#elseif locationVisibilityFormValue == 'private'>
+ ${i18nBundle.locationIsPrivate}
+ <#else>
+ <#list polygonServices as polygonService>
+ <#if locationVisibilityFormValue == "mask_" + polygonService.polygonServiceId>
+ ${i18nBundle.maskObservationWith} ${polygonService.polygonServiceName}
+ </#if>
+ </#list>
+ </#if>
+ </span>
+ </div>
+ <input type="hidden" name="observationTimeSeriesId" value="${observationTimeSeries.observationTimeSeriesId}">
+ <input type="hidden" name="year" value="${observationTimeSeries.year}">
+ <input type="hidden" name="cropOrganismId" value="${observationTimeSeries.cropOrganism.organismId}">
+ <input type="hidden" name="organismId" value="${observationTimeSeries.organism.organismId}">
+ <input type="hidden" name="locationPointOfInterestId" value="${observationTimeSeries.locationPointOfInterestId}">
+ <input type="hidden" name="locationVisibility" value="${locationVisibilityFormValue}">
+ <#else>
+ <#if ! observationTimeSeries.organism?has_content>
+ <div class="form-group">
+ <label for="cropCategoryIdList">${i18nBundle.listSelectedCropCategoryOnTop}</label>
+ <select class="form-control" id="cropCategoryIdList" name="cropCategoryId"
+ onchange="filterCrops(this.options[this.options.selectedIndex].value);">
+ <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.cropCategory?lower_case}</option>
+ <!-- Options added by JavaScript function renderCropCategories() -->
+ </select>
+ </div>
+ </#if>
+ <div class="form-group">
+ <label for="cropOrganismIdList">${i18nBundle.cropOrganismId}</label>
+ <select class="form-control" id="cropOrganismIdList" name="cropOrganismId" onblur="validateField(this);" onchange="updateCropPests();">
+ <#if !observationTimeSeries.observationTimeSeriesId?has_content>
+ <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.cropOrganismId?lower_case}</option>
+ <option value="-10" <#if (observationTimeSeries.cropOrganism?has_content && observationTimeSeries.cropOrganism.organismId == -10)>selected="selected"</#if>>${i18nBundle.missingInDatabase}</option>
+ </#if>
+ </select>
+ <span class="help-block" id="${formId}_cropOrganismId_validation"></span>
+ </div>
+ <div class="form-group">
+ <label for="organismId">${i18nBundle.organism}</label>
+ <select class="form-control" id="organismId" name="organismId" onblur="validateField(this);">
+ <#if !observationTimeSeries.organism?has_content>
+ <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.organism?lower_case}</option>
+ <option value="-10" <#if (observationTimeSeries.organism?has_content && observationTimeSeries.organism.organismId == -10)>selected="selected"</#if>>${i18nBundle.missingInDatabase}</option>
+ <#list allPests as organism>
+ <option value="${organism.organismId}">${organism.getLocalName(currentLocale.language)!""} (${organism.latinName!""}) ${hierarchyCategories.getName(organism.hierarchyCategoryId)?upper_case}</option>
+ </#list>
+ <#else>
+ <#list allPests as organism>
+ <option value="${organism.organismId}" <#if (observationTimeSeries.organism?has_content && observationTimeSeries.organism.organismId == organism.organismId)>selected="selected"</#if>>${organism.getLocalName(currentLocale.language)!""}
+ (${organism.latinName!""}
+ ) ${hierarchyCategories.getName(organism.hierarchyCategoryId)?upper_case}</option>
+ </#list>
+ </#if>
+ </select>
+ <span class="help-block" id="${formId}_organismId_validation"></span>
+ </div>
+
+ <div class="form-group">
+ <label for="yearList">${i18nBundle.year}</label>
+ <select class="form-control" id="yearList" name="year">
+ <#if !observationTimeSeries.observationTimeSeriesId?has_content>
+ <option value="" disabled selected>${i18nBundle.pleaseSelect} ${i18nBundle.year?lower_case}</option>
+ </#if>
+ <#list years as year>
+ <option value="${year}" <#if observationTimeSeries.observationTimeSeriesId?has_content && observationTimeSeries.year == year>selected</#if>>${year}</option>
+ </#list>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="locationPointOfInterestId">${i18nBundle.location}</label>
+ <button
+ role="button" type="button"
+ onclick="addNewLocationPopup();">${i18nBundle.addNew}</button>
+
+ <select class="form-control" name="locationPointOfInterestId" id="locationPointOfInterestId" onchange="showCorrectMap();">
+ <option value="" disabled selected>${i18nBundle.pleaseSelect} ${i18nBundle.location?lower_case}</option>
+ </select>
+ <span class="help-block" id="${formId}_locationPointOfInterestId_validation"></span>
+ </div>
+ <div class="form-group">
+ <div class="radio">
+ <label>
+ <input type="radio" name="locationVisibility" value="public"
+ <#if locationVisibilityFormValue == "public">checked="checked"</#if>
+ />
+ </label>
+ ${i18nBundle.locationIsPublic}
+ </div>
+ <div class="radio">
+
+ <label>
+ <input type="radio" name="locationVisibility" value="private"
+ <#if locationVisibilityFormValue == "private">checked="checked"</#if>
+ />
+ </label>
+ ${i18nBundle.locationIsPrivate}
+ </div>
+ <#list polygonServices as polygonService>
+ <div class="radio">
+ <label>
+ <input type="radio" name="locationVisibility"
+ value="mask_${polygonService.polygonServiceId}"
+ <#if locationVisibilityFormValue == "mask_" + polygonService.polygonServiceId>checked="checked"</#if>
+ />
+ ${i18nBundle.maskObservationWith} ${polygonService.polygonServiceName}
+ </label>
+ </div>
+ </#list>
+ </div>
+
+ </#if>
+ <div class="form-group">
+ <label for="name">${i18nBundle.observationTimeSeriesName}</label>
+ <input type="text" class="form-control" id="name" name="name" placeholder=""
+ value="${observationTimeSeries.name!""}" onblur="validateField(this);" />
+ <span class="help-block" id="${formId}_name_validation"></span>
+ </div>
+ <div class="form-group">
+ <label for="description">${i18nBundle.observationTimeSeriesDescription}</label>
+ <textarea class="form-control" id="description" name="description" placeholder="">${observationTimeSeries.description!""}</textarea>
+ <span class="help-block" id="${formId}_description_validation"></span>
+ </div>
+ <button type="submit" class="btn btn-default">${i18nBundle.submit}</button>
+ <#if observationTimeSeries.observationTimeSeriesId?has_content && isEditable>
+ <button type="button" class="btn btn-danger"
+ onclick="if(confirm('${i18nBundle.confirmDelete}')){window.location.href='/observationTimeSeries?action=deleteObservationTimeSeries&observationTimeSeriesId=${observationTimeSeries.observationTimeSeriesId}';}">${i18nBundle.delete}</button>
+ </#if>
+
+ </form>
+ </div>
+ <div class="col-md-6">
+ <div id="poiFormMap" class="map" style="height: 400px;">
+ <div id="popover"></div>
+ </div>
+ </div>
+ </div>
+ <#if observations?has_content>
+ <h2>Registrerte observasjoner</h2>
+ <div class="row mt-3">
+ <div class="table-responsive">
+ <table class="table table-striped">
+ <thead>
+ <th>${i18nBundle.timeOfObservation}</th>
+ <th>${i18nBundle.cropOrganismId}</th>
+ <th>${i18nBundle.organism}</th>
+ <th>${i18nBundle.location}</th>
+ <th>${i18nBundle.observer}</th>
+ <th>${i18nBundle.heading}</th>
+ <th>${i18nBundle.status}</th>
+ <th></th>
+ </thead>
+ <tbody>
+ <#list observations as observation>
+ <tr>
+ <td>${observation.timeOfObservation?string("yyyy-MM-dd")}</td>
+ <td>${observation.cropOrganism.latinName!""}/${observation.cropOrganism.getLocalName(currentLocale.language)!""}</td>
+ <td>${observation.organism.latinName!""}/${observation.organism.getLocalName(currentLocale.language)!""}</td>
+ <td><#if observation.location?has_content>${observation.location.name!""}</#if></td>
+ <td><#if observation.user?has_content>${observation.user.firstName!""} ${observation.user.lastName!""}</#if></td>
+ <td>${observation.observationHeading}</td>
+ <td><#switch observation.statusTypeId><#case 1>${i18nBundle.pending}<#break><#case 2>${i18nBundle.rejected}<#break><#case 3>${i18nBundle.approved}</#switch></td>
+ <td><#if observation.userId == user.userId><a href="/observation?action=editObservationForm&observationId=${observation.observationId}&returnTo=timeseries" class="btn btn-default" role="button">${i18nBundle.edit}</a></#if></td>
+ </tr>
+ </#list>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </#if>
+</div>
+</#macro>
+<@page_html/>
diff --git a/src/main/webapp/templates/observationTimeSeriesList.ftl b/src/main/webapp/templates/observationTimeSeriesList.ftl
new file mode 100644
index 0000000000000000000000000000000000000000..0e8d875ffe447eb0e42e890199bb635d7a72c345
--- /dev/null
+++ b/src/main/webapp/templates/observationTimeSeriesList.ftl
@@ -0,0 +1,68 @@
+<#include "master.ftl">
+<#setting time_zone=user.organizationId.defaultTimeZone!"UTC">
+<#macro page_head>
+ <title>${i18nBundle.timeSeriesList}</title>
+</#macro>
+<#macro custom_css>
+ <link href="/css/3rdparty/chosen.min.css" rel="stylesheet"/>
+</#macro>
+<#macro custom_js>
+ <script type="text/javascript" src="/js/3rdparty/chosen.jquery.min.js"></script>
+ <script type="text/javascript" src="/js/3rdparty/jquery.datetimepicker.js"></script>
+</#macro>
+<#macro page_contents>
+ <div class="singleBlockContainer">
+ <h1>${i18nBundle.timeSeriesList}</h1>
+ <#if messageKey?has_content>
+ <div class="alert alert-success">${i18nBundle(messageKey)}</div>
+ </#if>
+ <a href="/observationTimeSeries?action=newObservationTimeSeriesForm" class="btn btn-default" role="button">${i18nBundle.addNewTimeSeries}</a>
+
+ <div class="table-responsive">
+ <table class="table table-striped">
+ <thead>
+ <tr>
+ <th>Tittel</th>
+ <th>År</th>
+ <th>${i18nBundle.cropOrganismId}</th>
+ <th>${i18nBundle.organism}</th>
+ <th>${i18nBundle.location}</th>
+ <th>${i18nBundle.observer}</th>
+ <th>${i18nBundle.observationCount}</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <#list observationTimeSeriesList as timeSeries>
+ <tr>
+ <td>${timeSeries.name}</td>
+ <td>${timeSeries.year}</td>
+ <td>${timeSeries.cropOrganism.latinName!""}
+ /${timeSeries.cropOrganism.getLocalName(currentLocale.language)!""}</td>
+ <td>${timeSeries.organism.latinName!""}
+ /${timeSeries.organism.getLocalName(currentLocale.language)!""}</td>
+ <td><#if timeSeries.locationPointOfInterest?has_content>${timeSeries.locationPointOfInterest.name!""}</#if></td>
+ <td><#if timeSeries.user?has_content>${timeSeries.user.firstName!""} ${timeSeries.user.lastName!""}</#if></td>
+ <td>${observationCounts[timeSeries.observationTimeSeriesId?string]}</td>
+ <td>
+ <#if userIsObservationAuthority || timeSeries.userId == user.userId>
+ <a
+ href="/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=${timeSeries.observationTimeSeriesId}"
+ class="btn btn-default" role="button">${i18nBundle.edit}</a>
+ </#if>
+ </td>
+ <td>
+ <#if userIsObservationAuthority || timeSeries.userId == user.userId>
+ <a href="/observation?action=newObservationForm&observationTimeSeriesId=${timeSeries.observationTimeSeriesId}&returnTo=timeseries" class="btn btn-default" role="button">Legg til observasjon</a>
+ </#if>
+ </td>
+ </tr>
+ </#list>
+ </tbody>
+ </table>
+ </div>
+
+ </div>
+</#macro>
+<@page_html/>
diff --git a/src/main/webapp/templates/poiDeletePreview.ftl b/src/main/webapp/templates/poiDeletePreview.ftl
index 07cf886cc946dc8e8ab7a511d1cb7c41f51f22ca..7bc3ae7a548d4193858edc8522e0705b2e840705 100644
--- a/src/main/webapp/templates/poiDeletePreview.ftl
+++ b/src/main/webapp/templates/poiDeletePreview.ftl
@@ -38,51 +38,85 @@
</#if>
<p>${i18nBundle.deletePoiPreviewExplanation}</p>
<h2>${i18nBundle.forecasts}</h2>
- <table class="table table-striped">
- <thead>
+ <#if forecastConfigurations?has_content>
+ <table class="table table-striped">
+ <thead>
<th>${i18nBundle.modelId}</th>
<th>${i18nBundle.dateStart}</th>
<th>${i18nBundle.dateEnd}</th>
<th></th>
- </thead>
- <tbody>
+ </thead>
+ <tbody>
<#list forecastConfigurations as forecastConfiguration>
- <tr>
- <td>
- <#if i18nBundle.containsKey(forecastConfiguration.modelId)>
- ${i18nBundle[forecastConfiguration.modelId]}
- <#else>
- ${modelInformation[forecastConfiguration.modelId].defaultName}
- </#if>
- </td>
- <td>${forecastConfiguration.dateStart}</td>
- <td>${forecastConfiguration.dateEnd}</td>
- <td><a href="/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=${forecastConfiguration.forecastConfigurationId}" target="new">${i18nBundle.edit}</a></td>
- </tr>
+ <tr>
+ <td>
+ <#if i18nBundle.containsKey(forecastConfiguration.modelId)>
+ ${i18nBundle[forecastConfiguration.modelId]}
+ <#else>
+ ${modelInformation[forecastConfiguration.modelId].defaultName}
+ </#if>
+ </td>
+ <td>${forecastConfiguration.dateStart}</td>
+ <td>${forecastConfiguration.dateEnd}</td>
+ <td><a href="/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=${forecastConfiguration.forecastConfigurationId}" target="new">${i18nBundle.edit}</a></td>
+ </tr>
</#list>
- </tbody>
- </table>
- <h2>${i18nBundle.observations}</h2>
- <table class="table table-striped">
- <thead>
- <th>${i18nBundle.timeOfObservation}</th>
+ </tbody>
+ </table>
+ <#else>
+ <p>${i18nBundle.noForecasts}</p>
+ </#if>
+ <h2>${i18nBundle.timeSeriesList}</h2>
+ <#if observationTimeSeries?has_content>
+ <table class="table table-striped">
+ <thead>
+ <th>${i18nBundle.observationTimeSeriesName}</th>
+ <th>${i18nBundle.year}</th>
+ <th>${i18nBundle.cropOrganismId}</th>
+ <th>${i18nBundle.organism}</th>
+ <th></th>
+ </thead>
+ <tbody>
+ <#list observationTimeSeries as ots>
+ <tr>
+ <td>${ots.name}</td>
+ <td>${ots.year}</td>
+ <td>${ots.cropOrganism.latinName}</td>
+ <td>${ots.organism.latinName}</td>
+ <td><a href="/observationTimeSeries?action=editObservationTimeSeriesForm&observationTimeSeriesId=${ots.observationTimeSeriesId}" target="new">${i18nBundle.edit}</a></td>
+ </tr>
+ </#list>
+ </tbody>
+ </table>
+ <#else>
+ <p>${i18nBundle.noTimeSeries}</p>
+ </#if>
+ <h2>${i18nBundle.observations}</h2>
+ <#if observations?has_content>
+ <table class="table table-striped">
+ <thead>
+ <th>${i18nBundle.timeOfObservation}</th>
<th>${i18nBundle.cropOrganismId}</th>
<th>${i18nBundle.pestOrganismId}</th>
<th>${i18nBundle.observationHeading}</th>
<th></th>
- </thead>
- <tbody>
+ </thead>
+ <tbody>
<#list observations as observation>
- <tr>
- <td>${observation.timeOfObservation}</td>
- <td>${observation.cropOrganism.latinName}</td>
- <td>${observation.organism.latinName}</td>
- <td>${observation.observationHeading}</td>
- <td><a href="/observation?action=editObservationForm&observationId=${observation.observationId}" target="new">${i18nBundle.edit}</a></td>
- </tr>
+ <tr>
+ <td>${observation.timeOfObservation}</td>
+ <td>${observation.cropOrganism.latinName}</td>
+ <td>${observation.organism.latinName}</td>
+ <td>${observation.observationHeading}</td>
+ <td><a href="/observation?action=editObservationForm&observationId=${observation.observationId}" target="new">${i18nBundle.edit}</a></td>
+ </tr>
</#list>
- </tbody>
- </table>
+ </tbody>
+ </table>
+ <#else>
+ <p>${i18nBundle.noObservations}</p>
+ </#if>
+
<p>
<a href="${returnURL}" class="btn btn-default cancel" role="button">${i18nBundle.cancel}</a>
<button type="button" class="btn btn-danger" onclick="if(confirm('${i18nBundle.confirmDelete}')){window.location.href='/poi?action=deletePoi&pointOfInterestId=${poi.pointOfInterestId}';}">${i18nBundle.deletePoi}</button>