diff --git a/pom.xml b/pom.xml index f47d0f176c8d12fb4fcf1a816044c878b88a91da..e372339da515df616f6d0ebcc41bc1d67186e204 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ <groupId>no.nibio.vips.</groupId> <artifactId>VIPSLogic</artifactId> <packaging>war</packaging> - <version>2024.2</version> + <version>2024.3</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/ForecastConfigurationController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/ForecastConfigurationController.java index b7277f97d8fac27234bf423a10618e03372d33e9..5685c07d82e41661b1d21ba65b153c9792cf3dac 100755 --- a/src/main/java/no/nibio/vips/logic/controller/servlet/ForecastConfigurationController.java +++ b/src/main/java/no/nibio/vips/logic/controller/servlet/ForecastConfigurationController.java @@ -18,6 +18,7 @@ package no.nibio.vips.logic.controller.servlet; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.util.Calendar; import java.io.IOException; import java.text.ParseException; @@ -35,14 +36,14 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; + +import org.slf4j.LoggerFactory; + import no.nibio.vips.logic.controller.session.ForecastBean; import no.nibio.vips.logic.controller.session.OrganismBean; +import no.nibio.vips.logic.controller.session.PointOfInterestBean; import no.nibio.vips.logic.controller.session.UserBean; -import no.nibio.vips.logic.entity.ForecastConfiguration; -import no.nibio.vips.logic.entity.ModelInformation; -import no.nibio.vips.logic.entity.Organization; -import no.nibio.vips.logic.entity.VipsLogicRole; -import no.nibio.vips.logic.entity.VipsLogicUser; +import no.nibio.vips.logic.entity.*; import no.nibio.vips.logic.i18n.SessionLocaleUtil; import no.nibio.vips.logic.util.Globals; import no.nibio.vips.logic.util.SystemTime; @@ -52,6 +53,9 @@ import no.nibio.web.forms.FormField; import no.nibio.web.forms.FormValidation; import no.nibio.web.forms.FormValidationException; import no.nibio.web.forms.FormValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.wololo.geojson.GeoJSON; /** * Handles form configuration actions @@ -59,7 +63,9 @@ import no.nibio.web.forms.FormValidator; * @author Tor-Einar Skog <tor-einar.skog@nibio.no> */ public class ForecastConfigurationController extends HttpServlet { - + + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ForecastConfigurationController.class); + @PersistenceContext(unitName="VIPSLogic-PU") EntityManager em; @@ -72,6 +78,9 @@ public class ForecastConfigurationController extends HttpServlet { @EJB OrganismBean organismBean; + @EJB + PointOfInterestBean pointOfInterestBean; + /** * Processes requests for both HTTP <code>GET</code> and <code>POST</code> * methods. @@ -203,7 +212,7 @@ public class ForecastConfigurationController extends HttpServlet { if(forecastConfiguration == null) { forecastConfiguration = new ForecastConfiguration(); - multipleNew = request.getParameter("multipleNew") != null + multipleNew = request.getParameter("multipleNew") != null && request.getParameter("multipleNew").equals("true"); } // Only superusers can view and edit forecasts from other organizations @@ -221,14 +230,22 @@ public class ForecastConfigurationController extends HttpServlet { // TODO: More intelligent selection of locations, weather stations and users if(user.isSuperUser()) { - request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findAll").getResultList()); - request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findAllByActivity").setParameter("active", Boolean.TRUE).getResultList()); + List<PointOfInterest> poiList = em.createNamedQuery("PointOfInterest.findAll").getResultList(); + request.setAttribute("locationPointOfInterests", poiList); + request.setAttribute("locationPointOfInterestsGeoJson", convertToGeoJson(poiList)); + List<PointOfInterest> weatherStationPoiList = em.createNamedQuery("PointOfInterestWeatherStation.findAllByActivity").setParameter("active", Boolean.TRUE).getResultList(); + request.setAttribute("weatherStationPointOfInterests", weatherStationPoiList); + request.setAttribute("weatherStationPointOfInterestsGeoJson", convertToGeoJson(weatherStationPoiList)); request.setAttribute("vipsLogicUsers", em.createNamedQuery("VipsLogicUser.findAll").getResultList()); } else { - request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findByOrganizationId").setParameter("organizationId", user.getOrganizationId()).getResultList()); - request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findByActivityAndOrganizationId").setParameter("active", Boolean.TRUE).setParameter("organizationId", user.getOrganizationId()).getResultList()); + List<PointOfInterest> poiList = em.createNamedQuery("PointOfInterest.findByOrganizationId").setParameter("organizationId", user.getOrganizationId()).getResultList(); + request.setAttribute("locationPointOfInterests", poiList); + request.setAttribute("locationPointOfInterestsGeoJson", convertToGeoJson(poiList)); + List<PointOfInterest> weatherStationPoiList = em.createNamedQuery("PointOfInterestWeatherStation.findByActivityAndOrganizationId").setParameter("active", Boolean.TRUE).setParameter("organizationId", user.getOrganizationId()).getResultList(); + request.setAttribute("weatherStationPointOfInterests", weatherStationPoiList); + request.setAttribute("weatherStationPointOfInterestsGeoJson", convertToGeoJson(weatherStationPoiList)); request.setAttribute("vipsLogicUsers", em.createNamedQuery("VipsLogicUser.findByOrganizationId").setParameter("organizationId", user.getOrganizationId()).getResultList()); } request.setAttribute("forecastConfiguration", forecastConfiguration); @@ -244,7 +261,7 @@ public class ForecastConfigurationController extends HttpServlet { request.setAttribute("modelInformations", forecastBean.getBatchableModels()); request.setAttribute("messageKey", request.getParameter("messageKey")); request.getRequestDispatcher("/forecastConfigurationForm.ftl").forward(request, response); - + } } catch(NullPointerException | NumberFormatException ex) @@ -264,13 +281,17 @@ public class ForecastConfigurationController extends HttpServlet { forecastConfiguration.setIsPrivate(Boolean.TRUE); } request.setAttribute("forecastConfiguration", forecastConfiguration); - request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findByOrganizationId").setParameter("organizationId", user.getOrganizationId()).getResultList()); - request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findByActivityAndOrganizationId").setParameter("active", Boolean.TRUE).setParameter("organizationId", user.getOrganizationId()).getResultList()); + List<PointOfInterest> poiList = em.createNamedQuery("PointOfInterest.findByOrganizationId").setParameter("organizationId", user.getOrganizationId()).getResultList(); + request.setAttribute("locationPointOfInterests", poiList); + request.setAttribute("locationPointOfInterestsGeoJson", convertToGeoJson(poiList)); + List<PointOfInterest> weatherStationPoiList = em.createNamedQuery("PointOfInterestWeatherStation.findByActivityAndOrganizationId").setParameter("active", Boolean.TRUE).setParameter("organizationId", user.getOrganizationId()).getResultList(); + request.setAttribute("weatherStationPointOfInterests", weatherStationPoiList); + request.setAttribute("weatherStationPointOfInterestsGeoJson", convertToGeoJson(weatherStationPoiList)); request.setAttribute("forecastConfiguration", forecastConfiguration); request.setAttribute("formId","forecastConfigurationForm"); request.getSession().setAttribute("availableTimeZones", SystemTime.getAvailableTimeZones()); request.getSession().setAttribute("defaultTimeZoneId", user.getOrganizationId().getDefaultTimeZone()); - + request.setAttribute("allCrops", em.createNamedQuery("Organism.findAllCrops").getResultList()); request.setAttribute("allPests", em.createNamedQuery("Organism.findAllPests").getResultList()); // Hierarchy categories @@ -279,13 +300,13 @@ public class ForecastConfigurationController extends HttpServlet { request.setAttribute("messageKey", request.getParameter("messageKey")); request.getRequestDispatcher("/forecastConfigurationForm.ftl").forward(request, response); } - else + else { response.sendError(403,"Access not authorized"); } } } - + // Store forecast configuration(s) // Authorization: SUPERUSERS and ORGANIZATION ADMINS else if(action.equals("forecastConfigurationFormSubmit")) @@ -310,14 +331,17 @@ public class ForecastConfigurationController extends HttpServlet { } else { - String formId = request.getParameter("multipleNew") != null && request.getParameter("multipleNew").equals("true") ? - "forecastConfigurationMultipleNewForm" + String formId = request.getParameter("multipleNew") != null && request.getParameter("multipleNew").equals("true") ? + "forecastConfigurationMultipleNewForm" :"forecastConfigurationForm"; FormValidation formValidation = FormValidator.validateForm(formId,request,getServletContext()); + LOGGER.debug("formValidation=" + formValidation.isValid()); // Also validation the model specific fields String modelId = formValidation.getFormField("modelId").getWebValue(); FormValidation modelFieldFormValidation = FormValidator.validateForm("models/" + modelId, request, getServletContext()); + // Additional input check: If the Grid data checkbox is not checked, a + if(formValidation.isValid() && modelFieldFormValidation.isValid()) { if(formId.equals("forecastConfigurationForm")) @@ -344,7 +368,7 @@ public class ForecastConfigurationController extends HttpServlet { for(String optionVal:formValidation.getFormField("weatherStationPointOfInterestIds").getWebValues()) { Integer weatherStationPointOfInterestId = Integer.valueOf(optionVal); - forecastBean.storeNewMultipleForecastConfiguration(weatherStationPointOfInterestId, formValidation.getFormFields(), modelFieldFormValidation.getFormFields()); + forecastBean.storeNewMultipleForecastConfiguration(weatherStationPointOfInterestId, formValidation.getFormFields(), modelFieldFormValidation.getFormFields()); } request.setAttribute("messageKey", request.getParameter("multipleForecastConfigurationsCreated")); response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://").append(ServletUtil.getServerName(request)).append("/forecastConfiguration?messageKey=").append("multipleForecastConfigurationsCreated").toString()); @@ -357,8 +381,12 @@ public class ForecastConfigurationController extends HttpServlet { // We must get date formats! Map<String, FormField> formFields = FormValidator.getFormFields("forecastConfigurationForm",getServletContext()); // TODO: More intelligent selection of locations, weather stations and users - request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findAll").getResultList()); - request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findAll").getResultList()); + List<PointOfInterest> poiList = em.createNamedQuery("PointOfInterest.findAll").getResultList(); + request.setAttribute("locationPointOfInterests", poiList); + request.setAttribute("locationPointOfInterestsGeoJson", convertToGeoJson(poiList)); + List<PointOfInterest> weatherStationPoiList = em.createNamedQuery("PointOfInterestWeatherStation.findAll").getResultList(); + request.setAttribute("weatherStationPointOfInterests", weatherStationPoiList); + request.setAttribute("weatherStationPointOfInterestsGeoJson", convertToGeoJson(weatherStationPoiList)); request.setAttribute("vipsLogicUsers", em.createNamedQuery("VipsLogicUser.findAll").getResultList()); request.setAttribute("dateStart_dateFormat", formFields.get("dateStart").getDateFormat()); request.setAttribute("dateEnd_dateFormat", formFields.get("dateEnd").getDateFormat()); @@ -403,13 +431,13 @@ public class ForecastConfigurationController extends HttpServlet { try { forecastBean.deleteForecastConfiguration(forecastConfigurationId); - response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://").append(ServletUtil.getServerName(request)).append("/forecastConfiguration?").append("&messageKey=").append("forecastConfigurationDeleted").toString()); + response.sendRedirect(new StringBuilder(Globals.PROTOCOL + "://").append(ServletUtil.getServerName(request)).append("/forecastConfiguration?").append("&messageKey=").append("forecastConfigurationDeleted").toString()); } catch(NullPointerException | NumberFormatException ex) { response.sendError(500, "Invalid forecast configurationId " + request.getParameter("forecastConfigurationId")); } - + } else { @@ -418,6 +446,11 @@ public class ForecastConfigurationController extends HttpServlet { } } + private GeoJSON convertToGeoJson(List<PointOfInterest> poiList) { + return pointOfInterestBean.getPoisAsGeoJson(poiList); + } + + // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code."> /** * Handles the HTTP <code>GET</code> method. diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ForecastBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ForecastBean.java index 6aa46f8bfcbd5603c43220f557c84de5832f1df5..9e6cdcb1923f18e7ab419615f8f21724dcda2ee1 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/ForecastBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/ForecastBean.java @@ -600,6 +600,7 @@ public class ForecastBean { forecastConfiguration.setCropOrganismId(em.find(Organism.class, formFields.get("cropOrganismId").getValueAsInteger())); forecastConfiguration.setPestOrganismId(em.find(Organism.class, formFields.get("pestOrganismId").getValueAsInteger())); forecastConfiguration.setIsPrivate(formFields.get("isPrivate").getWebValue() != null); + forecastConfiguration.setUseGridWeatherData(formFields.get("useGridWeatherData").getWebValue() != null); PointOfInterest locationPoi = em.find(PointOfInterest.class, formFields.get("locationPointOfInterestId").getValueAsInteger()); forecastConfiguration.setLocationPointOfInterestId(locationPoi); PointOfInterest weatherStationPoi = em.find(PointOfInterestWeatherStation.class, formFields.get("weatherStationPointOfInterestId").getValueAsInteger()); diff --git a/src/main/java/no/nibio/vips/logic/controller/session/SessionControllerGetter.java b/src/main/java/no/nibio/vips/logic/controller/session/SessionControllerGetter.java index 399bd43b255d37c6acd20106083a2fdec34849f6..be60d07964cf56c465790019eed7d95559208aa7 100644 --- a/src/main/java/no/nibio/vips/logic/controller/session/SessionControllerGetter.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/SessionControllerGetter.java @@ -34,7 +34,7 @@ public class SessionControllerGetter { // This obviously has to be changed when changing the application name in Maven // TODO: Refactor out to System properties (e.g. in standalone.xml in JBoss/WildFly) - public static final String JNDI_PATH = "java:global/VIPSLogic-2024.2/"; + public static final String JNDI_PATH = "java:global/VIPSLogic-2024.3/"; public static SchedulingBean getSchedulingBean() { diff --git a/src/main/java/no/nibio/vips/logic/entity/ForecastConfiguration.java b/src/main/java/no/nibio/vips/logic/entity/ForecastConfiguration.java index a17f2fbadaa35794dd3de13e10a6883d024776f7..735eebd16da0098531a68965e87403fe1994bbb1 100755 --- a/src/main/java/no/nibio/vips/logic/entity/ForecastConfiguration.java +++ b/src/main/java/no/nibio/vips/logic/entity/ForecastConfiguration.java @@ -50,7 +50,7 @@ import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.TypeDefs; /** - * @copyright 2014-2016 <a href="http://www.nibio.no/">NIBIO</a> + * @copyright 2014-2024 <a href="http://www.nibio.no/">NIBIO</a> * @author Tor-Einar Skog <tor-einar.skog@nibio.no> */ @Entity @@ -123,7 +123,17 @@ public class ForecastConfiguration implements Serializable, Comparable { private Organism pestOrganismId; @Column(name = "is_private") private Boolean isPrivate; + @Column(name = "use_grid_weather_data") + private Boolean useGridWeatherData; + public Boolean getUseGridWeatherData() { + return useGridWeatherData != null ? useGridWeatherData : Boolean.FALSE; + } + + public void setUseGridWeatherData(Boolean useGridWeatherData) { + this.useGridWeatherData = useGridWeatherData; + } + @Type(type = "IntegerArray") @Column(name = "grid_weather_station_point_of_interest_ids") private Integer[] gridWeatherStationPointOfInterestIds; diff --git a/src/main/java/no/nibio/vips/logic/service/POIService.java b/src/main/java/no/nibio/vips/logic/service/POIService.java index 19132a3b9146e71697b99567cb723a0acd7140b9..399cb26acf2078c0d0c4e9da7b48020e75605359 100644 --- a/src/main/java/no/nibio/vips/logic/service/POIService.java +++ b/src/main/java/no/nibio/vips/logic/service/POIService.java @@ -37,7 +37,9 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import com.fasterxml.jackson.core.JsonProcessingException; import no.nibio.vips.logic.controller.session.PointOfInterestBean; +import no.nibio.vips.logic.entity.helpers.PointOfInterestFactory; import org.jboss.resteasy.spi.HttpRequest; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Point; @@ -102,7 +104,70 @@ public class POIService { PointOfInterest retVal = SessionControllerGetter.getPointOfInterestBean().getPointOfInterest(pointOfInterestId); return Response.ok().entity(retVal).build(); } - + + @POST + @Consumes("application/json;charset=UTF-8") + @Produces("application/json;charset=UTF-8") + public Response postPoi(String poiJson) { + // TODO Fix authentication + ObjectMapper oM = new ObjectMapper(); + Map<Object, Object> poiMap; + try { + poiMap = oM.readValue(poiJson, new TypeReference<HashMap<Object, Object>>() {}); + } catch (JsonProcessingException e) { + LOGGER.error(e.getMessage(), e); + return Response.status(Status.BAD_REQUEST).entity("Unable to parse input").build(); + } + + Integer poiUserId = poiMap.get("userId") != null ? Integer.parseInt(poiMap.get("userId").toString()) : null; + + VipsLogicUser user = SessionControllerGetter.getUserBean().getVipsLogicUser(poiUserId); + if (user == null) { + LOGGER.error("No user found for userId={}", poiUserId); + return Response.status(Status.UNAUTHORIZED).build(); + } + LOGGER.error("Remember to check for roles as well, if necessary!"); + + PointOfInterestBean poiBean = SessionControllerGetter.getPointOfInterestBean(); + + Integer poiTypeId = poiMap.get("typeId") != null ? Integer.parseInt(poiMap.get("typeId").toString()) : null; + if(poiTypeId == null) { + return Response.status(Status.BAD_REQUEST).entity("Point of interest type is required").build(); + } + String poiName = poiMap.get("name") != null ? poiMap.get("name").toString() : null; + Double poiLongitude = poiMap.get("longitude") != null ? Double.valueOf(poiMap.get("longitude").toString()): null; + Double poiLatitude = poiMap.get("latitude") != null ? Double.valueOf(poiMap.get("latitude").toString()): null; + Double poiAltitude = poiMap.get("altitude") != null ? Double.valueOf(poiMap.get("altitude").toString()): null; + + PointOfInterest poiToSave = PointOfInterestFactory.getPointOfInterest(poiTypeId); + poiToSave.setName(poiName); + poiToSave.setLongitude(poiLongitude); + poiToSave.setLatitude(poiLatitude); + poiToSave.setAltitude(poiAltitude); + poiToSave.setLastEditedTime(new Date()); + poiToSave.setUser(user); + poiToSave.setCountryCode(user.getOrganizationId().getCountryCode()); + + if (poiLongitude != null && poiLatitude != null && poiAltitude != null) { + GISUtil gisUtil = new GISUtil(); + Coordinate coordinate = new Coordinate(poiLongitude, poiLatitude, poiAltitude); + Point p3d = gisUtil.createPointWGS84(coordinate); + poiToSave.setGisGeom(p3d); + } + + poiToSave = poiBean.storePoi(poiToSave); + + if (poiToSave != null) { + return Response.status(Response.Status.CREATED) + .entity(poiToSave) + .build(); + } else { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Failed to create PointOfInterest") + .build(); + } + } + /** * @param organizationId Id of the organization in question * @param poiTypesStr Comma separated list of poiTypes diff --git a/src/main/java/no/nibio/web/forms/FormValidator.java b/src/main/java/no/nibio/web/forms/FormValidator.java index 65214d7edbc41e02477cbba2e8a2532528797d02..d6d0ef0fdfeab610818d29eff8987ae1f08ab447 100755 --- a/src/main/java/no/nibio/web/forms/FormValidator.java +++ b/src/main/java/no/nibio/web/forms/FormValidator.java @@ -44,6 +44,7 @@ import no.nibio.vips.logic.controller.session.SessionControllerGetter; import no.nibio.vips.logic.controller.session.UserBean; import no.nibio.vips.logic.i18n.SessionLocaleUtil; import org.apache.commons.validator.routines.EmailValidator; +import org.slf4j.LoggerFactory; /** * Uses form configuration set in JSON files in [WARFILE]/formdefinitions/, or @@ -57,6 +58,8 @@ import org.apache.commons.validator.routines.EmailValidator; * @author Tor-Einar Skog <tor-einar.skog@nibio.no> */ public class FormValidator { + + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(FormValidator.class); @EJB UserBean userBean; @@ -340,6 +343,7 @@ public class FormValidator { { field.setValid(false); field.setValidationMessage(resourceBundle.getString("fieldIsRequired")); + LOGGER.debug(field.getName() + " with a value of " + field.getWebValue() + " is considered to be NULL"); } } diff --git a/src/main/resources/db/migration/V18__UseGridWeatherData.sql b/src/main/resources/db/migration/V18__UseGridWeatherData.sql new file mode 100644 index 0000000000000000000000000000000000000000..0775bd72877061f15cbcc5f4316a5dec35e79ef9 --- /dev/null +++ b/src/main/resources/db/migration/V18__UseGridWeatherData.sql @@ -0,0 +1,3 @@ +-- Adding this property when adding support for gridded weather datasources in VIPS +ALTER TABLE forecast_configuration +ADD COLUMN use_grid_weather_data BOOLEAN DEFAULT FALSE; \ No newline at end of file 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 61a9d67be0a612236834e7439c189538ad000d20..76a3c2201f800462755a96c228b09f58d07fffed 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties @@ -426,7 +426,7 @@ locationIsPrivate = Location is private locationIsPublic = Location is public -locationPointOfInterestId = Location Id +locationPointOfInterestId = Location logInterval = Log interval @@ -1056,6 +1056,8 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +useGridWeatherData=Use grid weather data +doNotUse=Do not use observationTimeSeriesId=Timeseries observationTimeSeriesLabel=Timeseries label observationId=Observation diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties index 87647c45dde3c281d5d68f2586191610a2faee80..b632979d4bef2c6f99f1b0a2908e54e7ceee5671 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties @@ -1046,3 +1046,5 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +useGridWeatherData=Use grid weather data +doNotUse=Do not use diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties index 351048a470187ba55766c9205a30a2a9403a2f17..286433a39e854e88ffbc69e3174ea1a676fa5e55 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties @@ -1044,3 +1044,5 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +useGridWeatherData=Use grid weather data +doNotUse=Do not use 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 966906932eae6e2f1e34720a20b75e2c4992b601..9c382a81b2a292d7ff50ce60a05113d37d169b86 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 @@ -419,11 +419,13 @@ listPests = Skadegj\u00f8rerliste listSelectedCropCategoryOnTop = List kulturer fra valgt gruppe \u00f8verst localName = Lokalt navn + location=Sted locationIsPrivate = Sted skal ikke offentliggj\u00f8res locationIsPublic = Sted kan vises offentlig + locationPointOfInterestId=Sted-Id logInterval = M\u00e5leintervall @@ -1054,6 +1056,8 @@ privacyStatement=Personvernerkl\u00e6ring privacyStatementFileName=Personvernerklaering_NIBIO-VIPS.pdf thresholdDSVMax=DSV-terskel for h\u00f8y infeksjonsrisiko thresholdDSVTempMin=Minimumstemperatur for beregning av DSV +useGridWeatherData=Bruk v\u00e6rdata fra rutenett +doNotUse=Ikke bruk observationTimeSeriesId=Tidsserie-Id observationTimeSeriesLabel=Tidsserie observationId=Observasjon-Id diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties index 1a791041100981ea65f2c5acf16d66132a05823d..044f7b23e28adf500929451368da17dfde0b3908 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties @@ -1046,3 +1046,5 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +useGridWeatherData=Use grid weather data +doNotUse=Do not use diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties index e118325c6d1829c0f06ba616f7f1a19c540cce12..85ad53c6c5179ff6f18fc03b386acc6ffc229acf 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties @@ -1043,3 +1043,5 @@ thresholdDSVTempMin=Minimum temperature for DSV calculation isBroadcast=Is broadcast yes=Yes no=No +useGridWeatherData=Use grid weather data +doNotUse=Do not use diff --git a/src/main/webapp/css/mapModal.css b/src/main/webapp/css/mapModal.css new file mode 100644 index 0000000000000000000000000000000000000000..dd9694b061060a316023b1f9ae9faabe4f3695be --- /dev/null +++ b/src/main/webapp/css/mapModal.css @@ -0,0 +1,84 @@ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: hidden; + background-color: rgba(0, 0, 0, 0.9); +} + +.modal-content { + position: relative; + height: 100%; + width: 100%; + background-color: #fefefe; +} + +#open-map-modal-icon { + font-size: 30px; + margin-left: 15px; + cursor: pointer; +} + +#selectedPointInfo { + font-family: Arial, sans-serif; + font-size: 12px; + box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); + width: 150px; + position: absolute; + bottom: 20px; + left: 20px; + background: white; + padding: 10px; + border-radius: 5px; + z-index: 1100; +} + +#zoomToMyLocationButton { + position: absolute; + top: 10px; + left: 50px; + z-index: 1100; +} + +#confirmButton { + margin-top: 10px; +} + +.leaflet-container { + cursor: default !important; /* Force the arrow cursor */ +} + +.leaflet-container { + font-size: 0.9rem; /* Make font-size of popup larger */ +} + +.close-button { + position: absolute; + top: 10px; + right: 15px; + font-size: 50px; + font-weight: bold; + text-shadow: + -1px -1px 0 #000, /* Top left shadow */ + 1px -1px 0 #000, /* Top right shadow */ + -1px 1px 0 #000, /* Bottom left shadow */ + 1px 1px 0 #000; + color: white; + background-color: transparent; + border: none; + cursor: pointer; + z-index: 1500; +} + +#newPointForm { + z-index: 1200; + position: absolute; +} + +#poiPopup { + width: 100px; +} \ No newline at end of file diff --git a/src/main/webapp/formdefinitions/forecastConfigurationForm.json b/src/main/webapp/formdefinitions/forecastConfigurationForm.json index 95c43c592f962e33cfe7900f5c63fd43a3058314..ca6fd66a4cf07d58acbed4e9a62d86c9c23b3769 100755 --- a/src/main/webapp/formdefinitions/forecastConfigurationForm.json +++ b/src/main/webapp/formdefinitions/forecastConfigurationForm.json @@ -57,6 +57,11 @@ "dataType" : "STRING", "required" : false }, + { + "name" : "useGridWeatherData", + "dataType" : "STRING", + "required" : false + }, { "name" : "locationPointOfInterestId", "dataType" : "INTEGER", diff --git a/src/main/webapp/js/mapModal.js b/src/main/webapp/js/mapModal.js new file mode 100644 index 0000000000000000000000000000000000000000..1e522eacf17b7b393b943accd3c2f79cc89755b7 --- /dev/null +++ b/src/main/webapp/js/mapModal.js @@ -0,0 +1,439 @@ +// mapModal.js +import { + map, + tileLayer, + geoJSON, + circleMarker, + marker, + GeoJSON, + DomEvent +} from 'https://unpkg.com/leaflet/dist/leaflet-src.esm.js'; + +/** + * Uses css classes from bootstrap 3.4.1 + * + */ +class MapModal { + + /** + * @param mapContainerId The id of the HTML element to which the map should be added + * @param typeNameMap A mapping from pointOfInterestTypeIds to their localized names + * @param geoJsonData GeoJson containing all features which should be displayed on the map + * @param allowNewPoints Whether or not the user should be allowed to add new points + * @param callbackOnPersistNew Callback function for persisting newly created point + * @param callbackOnClose Callback function to call when closing the modal + */ + constructor(mapContainerId, typeNameMap, geoJsonData, allowNewPoints = false, callbackOnPersistNew = null, callbackOnClose = null) { + this.mapContainerId = mapContainerId; + this.typeNameMap = typeNameMap; + this.geoJsonData = geoJsonData; + this.allowNewPoints = allowNewPoints; + this.callbackOnPersistNew = callbackOnPersistNew; + this.callbackOnClose = callbackOnClose; + + this.map = null; + this.isMapInitialized = false; + this.selectedPointLayer = null; + this.createdPointLayer = null; + this.createdPoints = []; + + // Colours for the available types of pois + this.typeColorMap = { + 0: "#5DADE2", // Bright Blue + 1: "#58D68D", // Vibrant Green + 2: "#AF7AC5", // Medium Lavender + 3: "#F5B041", // Warm Orange + 5: "#F7DC6F", // Bright Yellow + 6: "#DC7633", // Rich Brown + 7: "#FF33A6" // Vivid Magenta + }; + + } + + initMap() { + if (!this.isMapInitialized) { + // Initialize the map centered on Norway + this.map = map(this.mapContainerId).setView([63.4226, 10.3951], 5); + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19 + }).addTo(this.map); + + this.setUpSelectedPointInfoPanel(); + this.setUpZoomToCurrentLocation(); + + // Add points to the map + geoJSON(this.geoJsonData, { + filter: (feature) => feature.geometry.type === "Point", + pointToLayer: (feature, latlng) => { + const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB"; + return circleMarker(latlng, { + radius: 8, // Size of the marker + fillColor: color, + color: "#FFFFFF", // Border color + weight: 2, // Border thickness + opacity: 1, + fillOpacity: 0.8 + }); + }, + onEachFeature: (feature, layer) => { + layer.bindPopup(this.popupContent(feature)); + layer.on('click', () => this.selectPoint(feature, layer)); + } + }).addTo(this.map); + + // Enable adding new points if allowed + if (this.allowNewPoints) { + console.info("Enable point creation") + this.enablePointCreation(); + } + this.isMapInitialized = true; + } + } + + /** + * Create information panel for selected point, initially hidden. + */ + setUpSelectedPointInfoPanel() { + const selectedPointInfoHtml = ` + <div id="selectedPointInfo" style="display: none;"> + <div id="infoMessage"></div> + <button id="confirmButton" class="btn btn-primary">Velg sted</button> + </div>`; + document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', selectedPointInfoHtml); + } + + setUpZoomToCurrentLocation() { + const zoomButtonHtml = `<button id="zoomToMyLocationButton" class="btn btn-primary">Zoom til meg</button>`; + document.getElementById(this.mapContainerId).insertAdjacentHTML('beforeend', zoomButtonHtml); + + let zoomButton = document.getElementById('zoomToMyLocationButton') + DomEvent.disableClickPropagation(zoomButton); + zoomButton.addEventListener('click', () => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((position) => { + const latitude = position.coords.latitude; + const longitude = position.coords.longitude; + + this.map.setView([latitude, longitude], 13); + + // Add a marker at the user's location + const userLocationMarker = marker([latitude, longitude]).addTo(this.map); + }, (error) => { + console.error('Geolocation failed: ' + error.message); + alert('Unable to retrieve your location.'); + }); + } else { + alert('Geolocation is not supported by this browser.'); + } + }); + } + + /** + * Display the panel which contains information about the currently selected point, + * and a button to bring you back to the original page. + */ + displaySelectedPointInfo(feature) { + const name = feature.properties.pointOfInterestName; + console.info("Display selected point " + name); + const type = this.typeNameMap[feature.properties.pointOfInterestTypeId]; + const coordinates = feature.geometry.coordinates[1].toFixed(5) + ", " + feature.geometry.coordinates[0].toFixed(5); + + const selectedPointInfo = document.getElementById('selectedPointInfo'); + const infoMessage = document.getElementById('infoMessage'); + + infoMessage.innerHTML = `<strong>${name}</strong><br>${coordinates}<br>${type}`; + selectedPointInfo.style.display = 'block'; + + const confirmButton = document.getElementById('confirmButton'); + confirmButton.style.display = 'block'; + confirmButton.onclick = () => { + this.confirmSelection(feature); + }; + } + + selectPointById(pointOfInterestId) { + const selectedFeature = this.getFeatureById(pointOfInterestId); + const selectedLayer = this.getLayerById(pointOfInterestId); + this.selectPoint(selectedFeature, selectedLayer, true); + } + + getFeatureById(pointOfInterestId) { + return this.geoJsonData.features.find(feature => feature.properties.pointOfInterestId == pointOfInterestId); + } + + getLayerById(pointOfInterestId) { + let result = null; + this.map.eachLayer(layer => { + if (layer instanceof GeoJSON) { + layer.eachLayer(l => { + if (l.feature && l.feature.properties.pointOfInterestId == pointOfInterestId) { + result = l; + } + }); + } + }); + return result; + } + + selectPoint(feature, layer, zoomInToSelected = false) { + console.info("Select point", feature); + // Deselect previously selected point, if any + if (this.selectedPointLayer) { + const color = this.typeColorMap[feature.properties.pointOfInterestTypeId] || "#3498DB"; + this.selectedPointLayer.setStyle({ + radius: 8, // Size of the marker + fillColor: color, + color: "#FFFFFF", + weight: 2, + opacity: 1, + fillOpacity: 0.8 + }); + } + + // Highlight the new selected point + layer.setStyle({ + radius: 12, + fillColor: "#FF5733", + fillOpacity: 1, + }); + + if (zoomInToSelected) { + const latLng = layer.getLatLng(); + this.map.setView(latLng, 10); + } + + this.displaySelectedPointInfo(feature) + this.selectedPointLayer = layer; + } + + confirmSelection(feature) { + console.info("Confirm selection", feature); + let poiId = feature.properties.pointOfInterestId; + if (!poiId && typeof this.callbackOnPersistNew === 'function') { + const pointData = { + name: feature.properties.pointOfInterestName, + typeId: feature.properties.pointOfInterestTypeId, + longitude: feature.geometry.coordinates[0], + latitude: feature.geometry.coordinates[1] + }; + console.info("Persist new", pointData); + this.callbackOnPersistNew(pointData); + } + if (typeof this.callbackOnClose === 'function') { + this.callbackOnClose(poiId); + console.info("Goodbye from map modal!") + } + this.closeModal(); + } + + enablePointCreation() { + this.map.on('click', (e) => { + const latlng = e.latlng; + + // If a form already exists, remove it + this.closeNewPointFormIfOpen(); + + // Calculate the pixel position from the map's click event + const containerPoint = this.map.latLngToContainerPoint(latlng); + const newPointFormElement = this.addHtmlElementNewPointForm(containerPoint.x, containerPoint.y, latlng.lat, latlng.lng) + + // Click inside the form should not propagate to underlying map + DomEvent.disableClickPropagation(newPointFormElement); + // Add event listener to close the form if clicked outside + document.addEventListener('click', this.handleClickOutsidePointForm.bind(this), true); + + const closeButton = newPointFormElement.querySelector("#close-button"); + const nameInput = newPointFormElement.querySelector('#name'); + const latitudeInput = newPointFormElement.querySelector('#latitude'); + const longitudeInput = newPointFormElement.querySelector('#longitude'); + const typeInput = newPointFormElement.querySelector('#type'); + const submitButton = newPointFormElement.querySelector('#submit-button'); + + const validateInputs = () => { + const isValidLat = !isNaN(parseFloat(latitudeInput.value)) && isFinite(latitudeInput.value); + const isValidLng = !isNaN(parseFloat(longitudeInput.value)) && isFinite(longitudeInput.value); + submitButton.disabled = !(isValidLat && isValidLng); + }; + latitudeInput.addEventListener('input', validateInputs); + longitudeInput.addEventListener('input', validateInputs); + validateInputs(); + + closeButton.addEventListener('click', function() { + newPointFormElement.remove(); + }); + + submitButton.addEventListener('click', () => { + this.removeExistingNewPoint(); + this.setNewPointAsSelected(nameInput.value, parseFloat(latitudeInput.value), parseFloat(longitudeInput.value), parseInt(typeInput.value, 10)); + newPointFormElement.remove(); + }); + }); + } + + handleClickOutsidePointForm(event) { + const formElement = document.getElementById('newPointForm'); + + // If the clicked element is not inside the form, close the form + if (formElement && !formElement.contains(event.target)) { + this.closeNewPointFormIfOpen(); + } + } + + closeNewPointFormIfOpen() { + const formElement = document.getElementById('newPointForm'); + if (formElement) { + formElement.remove(); + } + + // Remove the event listener after closing the form + document.removeEventListener('click', this.handleClickOutsidePointForm.bind(this), true); + } + + createFeatureForPoint(longitude, latitude, name, type) { + console.info("Create feature for [" + longitude + "," + latitude + "," + name + "," + type + "]"); + return { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [longitude, latitude] + }, + "properties": { + "pointOfInterestName": name, + "pointOfInterestTypeId": type + } + }; + } + + addNewPointToMap(point) { + console.info("Add new point to map", point); + return geoJSON(point, { + pointToLayer: (feature, latlng) => { + return circleMarker(latlng, { + radius: 8, + fillColor: 'green', + color: '#000', + weight: 1, + opacity: 1, + fillOpacity: 0.8 + }); + }, + onEachFeature: (feature, layer) => { + layer.bindPopup(this.popupContent(feature)); + layer.on('click', () => this.selectPoint(feature, layer)); + } + }).addTo(this.map); + } + + setNewPointAsSelected(name, latitude, longitude, type) { + const feature = this.createFeatureForPoint(longitude, latitude, name, type); + this.createdPoints.push(feature); + this.createdPointLayer = this.addNewPointToMap(feature); + this.createdPointLayer.eachLayer((layer) => { + this.selectPoint(feature, layer); + }); + } + + removeExistingNewPoint(){ + if (this.createdPointLayer) { + this.map.removeLayer(this.createdPointLayer); + } + if (this.createdPoints.length > 0) { + this.createdPoints.pop(); + } + } + + popupContent(feature) { + const localizedTypeName = this.typeNameMap[feature.properties.pointOfInterestTypeId]; + const coordinates = feature.geometry.coordinates[1].toFixed(5) + ", " + feature.geometry.coordinates[0].toFixed(5); + return `<div id="poiPopup"> + <strong>${feature.properties.pointOfInterestName}</strong><br>${coordinates}<br>${localizedTypeName} + </div>`; + } + + /** + * Creates the HTML form for adding a new point, and add it to the map container. + * + * @param positionX Where to place the form on the x axis + * @param positionY Where to place the form on the y axis + * @param latitude Latitude of the point clicked + * @param longitude Longitude of the point clicked + * @returns {Element} + */ + addHtmlElementNewPointForm(positionX, positionY, latitude, longitude) { + const html = ` + <div id="newPointForm" class="panel panel-default" style="top: ${positionY}px; left: ${positionX}px;"> + <div class="panel-heading"> + <h3 class="panel-title">Opprett nytt sted</h3> + <span id="close-button" style="position: absolute; top: 5px; right: 10px; cursor: pointer; font-size: 18px;">×</span> + </div> + <div class="panel-body"> + <div class="form-group"> + <label for="name">Navn:</label> + <input type="text" class="form-control" id="name" name="name"> + </div> + <div class="form-group"> + <label for="latitude">Breddegrad:</label> + <input type="text" class="form-control" id="latitude" name="latitude" value="${latitude}"> + </div> + <div class="form-group"> + <label for="longitude">Lengdegrad:</label> + <input type="text" class="form-control" id="longitude" name="longitude" value="${longitude}"> + </div> + <div class="form-group"> + <label for="poiTypeSelect">Type:</label> + <select class="form-control" id="type" name="type"> + <option value="2">${this.typeNameMap[2]}</option> + <option value="3">${this.typeNameMap[3]}</option> + <option value="5">${this.typeNameMap[5]}</option> + </select> + </div> + <div class="form-group text-right"> + <button id="submit-button" class="btn btn-primary">OK</button> + </div> + </div> + </div>`; + const tmpContainer = document.createElement("div"); + tmpContainer.innerHTML = html; + const htmlElement = tmpContainer.querySelector('#newPointForm'); + document.getElementById(this.mapContainerId).appendChild(htmlElement); + return htmlElement; + } + + /** + * Function is called when newly created point of interest is successfully persisted to database. + * @param pointOfInterestId + */ + saveSuccess(pointOfInterestId) { + if (this.createdPoints.length < 1 || !this.createdPointLayer) { + console.error('No newly created points, unable to update with pointOfInterestId=' + pointOfInterestId); + return; + } + let latestCreatedFeature = this.createdPoints[0]; + latestCreatedFeature.properties.pointOfInterestId = pointOfInterestId; + this.geoJsonData.features.push(latestCreatedFeature); + this.createdPoints = []; + + this.map.removeLayer(this.createdPointLayer); + this.addNewPointToMap(latestCreatedFeature); + this.createdPointLayer = null; + + console.info("this.createdPoints", this.createdPoints); + console.info("this.createdPointLayer", this.createdPointLayer); + console.info("this.geoJsonData.features", this.geoJsonData.features); + } + + openModal(selectedPointOfInterestId) { + if(selectedPointOfInterestId) { + this.selectPointById(selectedPointOfInterestId); + } + document.getElementById('mapModal').style.display = 'block'; + this.initMap(); + } + + closeModal() { + document.getElementById('mapModal').style.display = 'none'; + } +} + +// Export the module +export default MapModal; \ No newline at end of file diff --git a/src/main/webapp/js/validateForm.js b/src/main/webapp/js/validateForm.js index 0e4a8e2199f33cc8708ad12f040b266889a1dd31..833dfe22f7c1995643b4943c6cac1863557ec37f 100755 --- a/src/main/webapp/js/validateForm.js +++ b/src/main/webapp/js/validateForm.js @@ -268,7 +268,8 @@ function validateFieldActual(fieldEl, theForm, formDefinitionKey) // Single select field - check for nullValue if(fieldDefinition.fieldType === fieldTypes.TYPE_SELECT_SINGLE) { - webValue = fieldEl.options[fieldEl.selectedIndex].value; + // Fallback if this is not a select list (could be a readonly list using a twin hidden field) + webValue = webValue = fieldEl.options != undefined ? fieldEl.options[fieldEl.selectedIndex].value : fieldEl.value; if(fieldDefinition.nullValue === webValue && fieldDefinition.required === true) { invalidizeField(fieldEl, theForm, getI18nMsg("fieldIsRequired",null)); @@ -471,7 +472,7 @@ function validateFieldActual(fieldEl, theForm, formDefinitionKey) } /** - * Recursive function to travers upwards in tree until we find the form + * Recursive function to traverse upwards in tree until we find the form * for the given element * @param {DOMElement} fieldEl * @returns {DOMelement} the form diff --git a/src/main/webapp/templates/forecastConfigurationForm.ftl b/src/main/webapp/templates/forecastConfigurationForm.ftl index c4656b5809c21741a48ddec2aac42bd9940a25c8..d1037a361da27fa37e7d337816b9d9f56b4ce0bd 100755 --- a/src/main/webapp/templates/forecastConfigurationForm.ftl +++ b/src/main/webapp/templates/forecastConfigurationForm.ftl @@ -1,6 +1,6 @@ -<#-- - Copyright (c) 2016 NIBIO <http://www.nibio.no/>. - +<#-- + Copyright (c) 2016 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 @@ -16,21 +16,108 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. --><#include "master.ftl"> <#macro page_head> - <title>${i18nBundle.viewForecastConfiguration}</title> + <title>${i18nBundle.viewForecastConfiguration}</title> </#macro> <#macro custom_js> - <script src="/js/resourcebundle.js"></script> - <script src="/js/forecastConfigurationForm.js"></script> - <script src="/js/validateForm.js"></script> - <script src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script> - <link href="//code.jquery.com/ui/1.10.3/themes/redmond/jquery-ui.css" rel="stylesheet" /> - <script type="text/javascript" src="/js/3rdparty/modernizr_custom.js"></script> - <script type="text/javascript" src="/js/3rdparty/moment.min.js"></script> + <script src="/js/resourcebundle.js"></script> + <script src="/js/forecastConfigurationForm.js"></script> + <script src="/js/validateForm.js"></script> + <script src="//code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script> + <link href="//code.jquery.com/ui/1.10.3/themes/redmond/jquery-ui.css" rel="stylesheet" /> + <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> + <link rel="stylesheet" href="/css/mapModal.css" /> + <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/environment.js"></script> <script type="text/javascript" src="/js/util.js"></script> <script type="text/javascript" src="/js/3rdparty/chosen.jquery.min.js"></script> <script type="text/javascript"> $(".chosen-select").chosen(); + </script> + <script type="module"> + import MapModal from '/js/mapModal.js'; + function callbackPersistNewPoint(pointData) { + const userId = ${user.userId}; + const params = { + 'name': pointData.name, + 'typeId': pointData.typeId, + 'longitude': pointData.longitude, + 'latitude': pointData.latitude, + 'altitude': '0', // default value - populate using a service for getting altitude for coordinates? + 'userId': userId, + } + $.ajax({ + url: "/rest/poi", + type: "POST", + contentType: "application/json", + data: JSON.stringify(params), + success: function(response) { + addOption(response.pointOfInterestId, response.name); + mapModalInstance.saveSuccess(response.pointOfInterestId); + console.info("Success:", response); + }, + error: function(jqXHR, textStatus, errorThrown) { + console.error("Error:", textStatus, errorThrown); + } + }); + } + function callbackUpdateLocationPointOfInterest(pointOfInterestId) { + const selectBox = document.querySelector('select[name="locationPointOfInterestId"]'); + if(pointOfInterestId) { + let optionFound = false; + for (let i = 0; i < selectBox.options.length; i++) { + if (selectBox.options[i].value == pointOfInterestId) { + selectBox.selectedIndex = i; // Select the matching option + optionFound = true; + break; + } + } + if (!optionFound) { + console.error("No matching option found for poi.id:", pointOfInterestId); + } + } + } + // TODO Ensure options are sorted alphabetically..? + function addOption(pointOfInterestId, name) { + let selectElement = document.querySelector('select[name="locationPointOfInterestId"]'); + let newOption = document.createElement("option"); + newOption.value = pointOfInterestId; + newOption.text = name; + selectElement.insertBefore(newOption, selectElement.firstChild); + selectElement.value = pointOfInterestId; + } + const typeNameMap = { + 0: "${i18nBundle["pointOfInterestType_0"]}", + 1: "${i18nBundle["pointOfInterestType_1"]}", + 2: "${i18nBundle["pointOfInterestType_2"]}", + 3: "${i18nBundle["pointOfInterestType_3"]}", + 5: "${i18nBundle["pointOfInterestType_5"]}", + 6: "${i18nBundle["pointOfInterestType_6"]}", + 7: "${i18nBundle["pointOfInterestType_7"]}" + }; + + const poiGeoJson = JSON.parse('${locationPointOfInterestsGeoJson?json_string}'); + const stationGeoJson = JSON.parse('${weatherStationPointOfInterestsGeoJson?json_string}') + const mapModalInstance = new MapModal('mapContainer', typeNameMap, poiGeoJson, true, callbackPersistNewPoint, callbackUpdateLocationPointOfInterest); + window.mapModalInstance = mapModalInstance; + + // If poi is selected, send id to map modal before opening + window.openModal = () => { + const selectElement = document.querySelector('select[name="locationPointOfInterestId"]'); + const selectedOption = selectElement.options[selectElement.selectedIndex]; + + let selectedPointOfInterestId; + const value = selectedOption.value; + if(value) { + const parsedValue = parseInt(value, 10); + if (!isNaN(parsedValue) && parsedValue > 0) { + selectedPointOfInterestId = parsedValue; + } + } + window.mapModalInstance.openModal(selectedPointOfInterestId); + }; + + window.closeModal = () => window.mapModalInstance && window.mapModalInstance.closeModal(); </script> <script type="text/javascript"> $(document).ready(function() { @@ -190,6 +277,47 @@ } } }; + var handleUseGridWeatherDataClicked = function(theCheckBox, weatherStationPointOfInterestId) { + weatherStationList = document.getElementById("weatherStationPointOfInterestId"); + weatherStationPointOfInterestIdHiddenField = document.getElementById("weatherStationPointOfInterestIdHidden"); + if(theCheckBox.checked) + { + // Select weatherStationId -2 + weatherStationList.selectedIndex = 0; + // Disable the weatherstation select list + weatherStationList.disabled=true; + weatherStationList.name="weatherStationPointOfInterestIdDisabled"; + + // Enable the hidden field + weatherStationPointOfInterestIdHiddenField.disabled=false + weatherStationPointOfInterestIdHiddenField.name="weatherStationPointOfInterestId"; + } + else + { + // Select weatherStationId -1 OR the optionally provided weatherStationPointOfInterestId + if(weatherStationPointOfInterestId == undefined || weatherStationPointOfInterestId == null) + { + weatherStationList.selectedIndex = 1; + } + else + { + for(let i=0;i<weatherStationList.options.length;i++) + { + weatherStationList.options[i].selected = parseInt(weatherStationList.options[i].value) == weatherStationPointOfInterestId; + } + } + // Enable the weather station select list + weatherStationList.disabled=false; + weatherStationList.name="weatherStationPointOfInterestId"; + // Disable the hidden field + weatherStationPointOfInterestIdHiddenField.disabled=true + weatherStationPointOfInterestIdHiddenField.name="weatherStationPointOfInterestIdDisabled"; + } + }; + + // Setting weather station select list state correct on page load + handleUseGridWeatherDataClicked(document.getElementById("useGridWeatherData")<#if forecastConfiguration.weatherStationPointOfInterestId?has_content>,${forecastConfiguration.weatherStationPointOfInterestId.pointOfInterestId}</#if>); + </script> </#macro> <#macro custom_css> @@ -253,6 +381,7 @@ <#if ! user.isSuperUser() && ! user.isOrganizationAdmin()> readonly="readonly" disabled="disabled"</#if>/> </label> ${i18nBundle.isPrivate} + <span class="help-block" id="${formId}_isPrivate_validation"></span> </div> </div> <div class="form-group"> @@ -272,24 +401,43 @@ <#if !multipleNew?has_content || !multipleNew> <div class="form-group"> <label for="locationPointOfInterestId">${i18nBundle.locationPointOfInterestId}</label> - <select class="form-control" name="locationPointOfInterestId" onblur="validateField(this);"> - <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.locationPointOfInterestId?lower_case}</option> - <#list locationPointOfInterests?sort_by("name") as poi> - <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.locationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.locationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> - </#list> - </select> + <div class="select-container" style="flex: 1; display: flex; align-items: center;"> + <select class="form-control" id="locationPointOfInterestId" name="locationPointOfInterestId" onblur="validateField(this);" style="width: calc(100% - 30px);"> + <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.locationPointOfInterestId?lower_case}</option> + <#list locationPointOfInterests?sort_by("name") as poi> + <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.locationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.locationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> + </#list> + </select> + <i id="open-map-modal-icon" class="fa fa-map-marker" onclick="openModal()"></i> + </div> + <div id="mapModal" class="modal"> + <div class="modal-content"> + <span class="close-button" onclick="closeModal()">×</span> + <div id="mapContainer" style="height: 100vh; width: 100%; position: relative;"></div> + </div> + </div> <span class="help-block" id="${formId}_locationPointOfInterestId_validation"></span> </div> <div class="form-group"> <label for="weatherStationPointOfInterestId">${i18nBundle.weatherStationPointOfInterestId}</label> - <select class="form-control" name="weatherStationPointOfInterestId" onblur="validateField(this);"> - <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.weatherStationPointOfInterestId?lower_case}</option> + <select class="form-control" id="weatherStationPointOfInterestId" name="weatherStationPointOfInterestId" onblur="if(!document.getElementById('useGridWeatherData').checked) {validateField(this);};"> + <option value="-2">${i18nBundle.doNotUse} ${i18nBundle.weatherStationPointOfInterestId?lower_case}</option> + <option value="-1"<#if !forecastConfiguration.weatherStationPointOfInterestId?has_content && !forecastConfiguration.useGridWeatherData> selected="selected"</#if>>${i18nBundle.pleaseSelect} ${i18nBundle.weatherStationPointOfInterestId?lower_case}</option> <#list weatherStationPointOfInterests?sort_by("name") as poi> <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.weatherStationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.weatherStationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> </#list> </select> <span class="help-block" id="${formId}_weatherStationPointOfInterestId_validation"></span> </div> + <input type="hidden" id="weatherStationPointOfInterestIdHidden" name="weatherStationPointOfInterestIdDisabled" value="-2" disabled="disabled"/> + <div class="form-group"> + <div class="checkbox"> + <label> + <input type="checkbox" id="useGridWeatherData" name="useGridWeatherData"<#if forecastConfiguration.useGridWeatherData?has_content && forecastConfiguration.useGridWeatherData == true> checked="checked"</#if> onclick="handleUseGridWeatherDataClicked(this);"/> + </label> + ${i18nBundle.useGridWeatherData} + <span class="help-block" id="${formId}_useGridWeatherData_validation"></span> + </div> <#else> <input type="hidden" name="multipleNew" value="true"/> <div class="form-group">