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>&nbsp;
+                        <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>