diff --git a/client/src/main/scala/ch/wsl/box/client/RoutingRegistryDef.scala b/client/src/main/scala/ch/wsl/box/client/RoutingRegistryDef.scala index 6dd107c8..84171197 100755 --- a/client/src/main/scala/ch/wsl/box/client/RoutingRegistryDef.scala +++ b/client/src/main/scala/ch/wsl/box/client/RoutingRegistryDef.scala @@ -53,7 +53,9 @@ class RoutingRegistryDef extends RoutingRegistry[RoutingState] with Logging { case "/box" / "function" / exportFunction => DataState(DataKind.FUNCTION,exportFunction) case "/box" / kind / entity / "page" => FormPageState(kind,entity,"true",false,Layouts.std) case "/box" / kind / entity / "insert" => EntityFormState(kind,entity,"true",None,false,Layouts.std) + case "/box" / kind / entity / "insert" / layout => EntityFormState(kind,entity,"true",None,false,layout) case "/box" / kind / entity / "row" / write / id => EntityFormState(kind,entity,write,Some(id),false,Layouts.std) + case "/box" / kind / entity / "row" / write / id / layout => EntityFormState(kind,entity,write,Some(id),false,layout) case "/box" / kind / entity / "child" / childEntity => MasterChildState(kind,entity,childEntity) case "/box" / kind / entity => EntityTableState(kind,entity,None,false) case "/box" / kind / entity / "query" / query => EntityTableState(kind,entity,Some(query),false) diff --git a/client/src/main/scala/ch/wsl/box/client/geo/StandaloneMap.scala b/client/src/main/scala/ch/wsl/box/client/geo/StandaloneMap.scala index 6373a2b2..4e150389 100644 --- a/client/src/main/scala/ch/wsl/box/client/geo/StandaloneMap.scala +++ b/client/src/main/scala/ch/wsl/box/client/geo/StandaloneMap.scala @@ -131,6 +131,7 @@ abstract class StandaloneMap(_div:Div, metadata:MapMetadata,properties:ReadableP } } def redrawControl():Unit = { + logger.debug("drawControl") window.setTimeout(() => { controlsDiv.children.toSeq.foreach(controlsDiv.removeChild) controlsBottomDiv.children.toSeq.foreach(controlsBottomDiv.removeChild) @@ -215,9 +216,7 @@ abstract class StandaloneMap(_div:Div, metadata:MapMetadata,properties:ReadableP } case _ => None } - layersExtent.tail.foreach{ e => - extentMod.extend(layersExtent.head,e) - } + if (layersExtent.isEmpty) { None } else { @@ -324,18 +323,19 @@ abstract class StandaloneMap(_div:Div, metadata:MapMetadata,properties:ReadableP ready.set(false) - for { + {for { baseLayers <- Future.sequence(metadata.db.filter(_.autofocus).map(v => dbVectorLayer(v, d, None))) _ = addLayers(baseLayers.map(x => geomsToLayer(x._1, x._2)),true) extent = fit() extraLayers <- Future.sequence(metadata.db.filterNot(_.autofocus).map(v => dbVectorLayer(v, d, Some(extent)))) } yield { - + logger.debug("loaded layers") selectedLayerForEdit.set(selectedLayerForEdit.get, true) // retrigger layer listener addLayers(extraLayers.map(x => geomsToLayer(x._1, x._2)),false) redrawControl() ready.set(true) - } + logger.debug("ready") + }}.recover{ case t => t.printStackTrace()} } properties.listen({d => diff --git a/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala b/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala index 4f52f2b5..9a6f1605 100755 --- a/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala +++ b/client/src/main/scala/ch/wsl/box/client/views/EntityFormView.scala @@ -52,11 +52,11 @@ import scala.util.{Failure, Success, Try} */ case class EntityFormModel(name:String, kind:String, id:Option[String], metadata:Option[JSONMetadata], originalData:Json,data:Json, - error:String, children:Seq[JSONMetadata], navigation: Navigation, changed:Boolean, write:Boolean, public:Boolean, insert:Boolean, showActionPanelMobile: Boolean, localData:Boolean) + error:String, children:Seq[JSONMetadata], navigation: Navigation, changed:Boolean, write:Boolean, public:Boolean, insert:Boolean, showActionPanelMobile: Boolean, localData:Boolean, updateRight:Boolean) object EntityFormModel extends HasModelPropertyCreator[EntityFormModel] { - val empty = EntityFormModel("","",None,None,Json.Null,Json.Null,"",Seq(), Navigation.empty0,false, true, false, true, false,false) + val empty = EntityFormModel("","",None,None,Json.Null,Json.Null,"",Seq(), Navigation.empty0,false, true, false, true, false,false,false) implicit val blank: Blank[EntityFormModel] = Blank.Simple(empty) } @@ -146,7 +146,8 @@ case class EntityFormPresenter(model:ModelProperty[EntityFormModel]) extends Pre state.public, insert, false, - localData = record.local_version + localData = record.local_version, + updateRight = rowAccess ) model.set(stateModel) @@ -724,7 +725,10 @@ case class EntityFormView(model:ModelProperty[EntityFormModel], presenter:Entity } - if((action.updateOnly && _id.isDefined && !model.subProp(_.insert).get) || (action.insertOnly && model.subProp(_.insert).get) || (!action.insertOnly && !action.updateOnly)) { + if( + (!action.needUpdateRight || model.subProp(_.updateRight).get) && + ((action.updateOnly && _id.isDefined && !model.subProp(_.insert).get) || (action.insertOnly && model.subProp(_.insert).get) || (!action.insertOnly && !action.updateOnly)) + ) { val actionButton = button( id := TestHooks.actionButton(action.label), importance, diff --git a/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala b/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala index e620b71e..364aef7a 100755 --- a/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala +++ b/client/src/main/scala/ch/wsl/box/client/views/EntityTableView.scala @@ -16,7 +16,7 @@ import ch.wsl.box.client.views.components.ui.TwoPanelResize import ch.wsl.box.client.views.components.widget.DateTimeWidget import ch.wsl.box.client.views.components.{Debug, MapList, TableFieldsRenderer} import ch.wsl.box.client.views.elements.Offline -import ch.wsl.box.client.views.helpers.TableColumnDrag +import ch.wsl.box.client.views.helpers.{TableColumnDrag} import ch.wsl.box.model.shared.EntityKind.VIEW import ch.wsl.box.model.shared.GeoJson.Polygon import ch.wsl.box.model.shared.geo.GeoDataRequest @@ -372,7 +372,7 @@ case class EntityTablePresenter(model:ModelProperty[EntityTableModel], onSelect: (ta.target, new_window, url) match { case (_,_, None) => () case (Self, false, Some(url)) => Navigate.toUrl(url) - case (_,_, Some(url)) => window.open(url) + case (NewWindow,_, Some(url)) => window.open(url) } true } diff --git a/db/box_migrations/BOX_V68__form_action_rights.sql b/db/box_migrations/BOX_V68__form_action_rights.sql new file mode 100644 index 00000000..0f78ee18 --- /dev/null +++ b/db/box_migrations/BOX_V68__form_action_rights.sql @@ -0,0 +1,6 @@ + +alter table form_actions add need_update_right boolean default false not null; +alter table form_actions add need_delete_right boolean default false not null; + +alter table form_navigation_actions add need_update_right boolean default false not null; +alter table form_navigation_actions add need_delete_right boolean default false not null; \ No newline at end of file diff --git a/project/Settings.scala b/project/Settings.scala index cc486694..78e9b6cc 100755 --- a/project/Settings.scala +++ b/project/Settings.scala @@ -171,6 +171,9 @@ object Settings { "org.geotools" % "gt-shapefile" % "34.0", "org.geotools" % "gt-epsg-hsql" % "34.0", "org.geotools" % "gt-geopkg" % "34.0", + "org.geotools" % "gt-wms" % "34.0", + "org.geotools" % "gt-wmts" % "34.0", + "org.geotools.xsd" % "gt-xsd-sld" % "34.0", "com.google.zxing" % "core" % "3.5.0", "com.google.zxing" % "javase" % "3.5.0", "com.typesafe" %% "ssl-config-core" % "0.6.1", diff --git a/server/src/main/scala/ch/wsl/box/model/boxentities/BoxForm.scala b/server/src/main/scala/ch/wsl/box/model/boxentities/BoxForm.scala index 328adcb7..c3813655 100755 --- a/server/src/main/scala/ch/wsl/box/model/boxentities/BoxForm.scala +++ b/server/src/main/scala/ch/wsl/box/model/boxentities/BoxForm.scala @@ -120,12 +120,14 @@ object BoxForm { html_check:Boolean = true, view_enable:Boolean = true, edit_enable:Boolean = true, + need_update_right:Boolean = false, + need_delete_right:Boolean = false, target:Option[String] = None, enabled_roles:Option[List[String]] = None ) class BoxForm_actions(_tableTag: Tag) extends profile.api.Table[BoxForm_actions_row](_tableTag,schema, "form_actions") { - def * = (Rep.Some(uuid), form_uuid, action, importance, after_action_goto, label, update_only, insert_only, reload, action_order,confirm_text,execute_function,condition,html_check,view_enable,edit_enable,target,enabled_roles) <> (BoxForm_actions_row.tupled, BoxForm_actions_row.unapply) + def * = (Rep.Some(uuid), form_uuid, action, importance, after_action_goto, label, update_only, insert_only, reload, action_order,confirm_text,execute_function,condition,html_check,view_enable,edit_enable,need_update_right,need_delete_right,target,enabled_roles) <> (BoxForm_actions_row.tupled, BoxForm_actions_row.unapply) val uuid: Rep[java.util.UUID] = column[java.util.UUID]("uuid", O.AutoInc, O.PrimaryKey) val form_uuid: Rep[java.util.UUID] = column[java.util.UUID]("form_uuid") @@ -143,6 +145,8 @@ object BoxForm { val condition: Rep[Option[Json]] = column[Option[Json]]("condition") val view_enable: Rep[Boolean] = column[Boolean]("view_enable") val edit_enable: Rep[Boolean] = column[Boolean]("edit_enable") + val need_update_right: Rep[Boolean] = column[Boolean]("need_update_right") + val need_delete_right: Rep[Boolean] = column[Boolean]("need_delete_right") val html_check: Rep[Boolean] = column[Boolean]("html_check") val enabled_roles: Rep[Option[List[String]]] = column[Option[List[String]]]("enabled_roles") @@ -167,10 +171,13 @@ object BoxForm { view_enable:Boolean = true, edit_enable:Boolean = true, + + need_update_right:Boolean = false, + need_delete_right:Boolean = false, ) class BoxForm_navigation_actions(_tableTag: Tag) extends profile.api.Table[BoxForm_navigation_actions_row](_tableTag,schema, "form_navigation_actions") { - def * = (Rep.Some(uuid), form_uuid, action, importance, after_action_goto, label, update_only, insert_only, reload, action_order,confirm_text,execute_function,enabled_roles,view_enable,edit_enable) <> (BoxForm_navigation_actions_row.tupled, BoxForm_navigation_actions_row.unapply) + def * = (Rep.Some(uuid), form_uuid, action, importance, after_action_goto, label, update_only, insert_only, reload, action_order,confirm_text,execute_function,enabled_roles,view_enable,edit_enable,need_update_right,need_delete_right) <> (BoxForm_navigation_actions_row.tupled, BoxForm_navigation_actions_row.unapply) val uuid: Rep[java.util.UUID] = column[java.util.UUID]("uuid", O.AutoInc, O.PrimaryKey) val form_uuid: Rep[java.util.UUID] = column[java.util.UUID]("form_uuid") @@ -187,6 +194,8 @@ object BoxForm { val enabled_roles: Rep[Option[List[String]]] = column[Option[List[String]]]("enabled_roles") val view_enable: Rep[Boolean] = column[Boolean]("view_enable") val edit_enable: Rep[Boolean] = column[Boolean]("edit_enable") + val need_update_right: Rep[Boolean] = column[Boolean]("need_update_right") + val need_delete_right: Rep[Boolean] = column[Boolean]("need_delete_right") /** Foreign key referencing Field (database name fkey_field) */ lazy val fieldFk = foreignKey("fkey_form", form_uuid, BoxFormTable)(r => r.form_uuid, onUpdate=ForeignKeyAction.Cascade, onDelete=ForeignKeyAction.Cascade) diff --git a/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala index 522c0f79..fb3d05f9 100644 --- a/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala +++ b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeoPackageWriter.scala @@ -18,24 +18,7 @@ import scala.concurrent.{ExecutionContext, Future} object GeoPackageWriter { - def geomSchema(builder:SimpleFeatureTypeBuilder,name:String,geo:Seq[Option[GeoJson.Geometry]],srid:Int) = { - geo.toList.flatten.filterNot(_ == GeoJson.Empty).headOption match { - case Some(value) => { - value match { - case geometry: GeoJson.SingleGeometry => geometry match { - case GeoJson.Point(_, crs) => builder.add(name, classOf[Point], crs.srid) - case GeoJson.LineString(_, crs) => builder.add(name, classOf[LineString], crs.srid) - case GeoJson.Polygon(_, crs) => builder.add(name, classOf[Polygon], crs.srid) - } - case GeoJson.MultiPoint(_, crs) => builder.add(name, classOf[MultiPoint], crs.srid) - case GeoJson.MultiLineString(_, crs) => builder.add(name, classOf[MultiLineString], crs.srid) - case GeoJson.MultiPolygon(_, crs) => builder.add(name, classOf[MultiPolygon], crs.srid) - case GeoJson.GeometryCollection(_, crs) => builder.add(name, classOf[Geometry], crs.srid) - } - } - case None => builder.add(name, classOf[Geometry],srid) - } - } + private def fieldWriter(data:Json, typ: String, featureBuilder: SimpleFeatureBuilder) = { typ match { @@ -115,7 +98,7 @@ object GeoPackageWriter { t.typ match { case JSONFieldTypes.INTEGER => builder.add(n, classOf[Integer]) case JSONFieldTypes.NUMBER => builder.add(n, classOf[Double]) - case JSONFieldTypes.GEOMETRY if geomNameTrim(geomFieldName) == geomNameTrim(n) => geomSchema(builder, geomName, values.map(x => Some(x._1)), srid) + case JSONFieldTypes.GEOMETRY if geomNameTrim(geomFieldName) == geomNameTrim(n) => GeotoolUtils.geomSchema(builder, geomName, values.map(x => Some(x._1)), srid) case JSONFieldTypes.GEOMETRY => () case _ => builder.add(n, classOf[String]) } diff --git a/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeotoolUtils.scala b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeotoolUtils.scala new file mode 100644 index 00000000..09aeb6de --- /dev/null +++ b/server/src/main/scala/ch/wsl/box/rest/io/geotools/GeotoolUtils.scala @@ -0,0 +1,28 @@ +package ch.wsl.box.rest.io.geotools + +import ch.wsl.box.model.shared.GeoJson +import org.geotools.feature.simple.SimpleFeatureTypeBuilder +import org.locationtech.jts.geom.{Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon} + +object GeotoolUtils { + + def geomSchema(builder:SimpleFeatureTypeBuilder,name:String,geo:Seq[Option[GeoJson.Geometry]],srid:Int) = { + geo.toList.flatten.filterNot(_ == GeoJson.Empty).headOption match { + case Some(value) => { + value match { + case geometry: GeoJson.SingleGeometry => geometry match { + case GeoJson.Point(_, crs) => builder.add(name, classOf[Point], crs.srid) + case GeoJson.LineString(_, crs) => builder.add(name, classOf[LineString], crs.srid) + case GeoJson.Polygon(_, crs) => builder.add(name, classOf[Polygon], crs.srid) + } + case GeoJson.MultiPoint(_, crs) => builder.add(name, classOf[MultiPoint], crs.srid) + case GeoJson.MultiLineString(_, crs) => builder.add(name, classOf[MultiLineString], crs.srid) + case GeoJson.MultiPolygon(_, crs) => builder.add(name, classOf[MultiPolygon], crs.srid) + case GeoJson.GeometryCollection(_, crs) => builder.add(name, classOf[Geometry], crs.srid) + } + } + case None => builder.add(name, classOf[Geometry],srid) + } + } + +} diff --git a/server/src/main/scala/ch/wsl/box/rest/io/geotools/Map2Image.scala b/server/src/main/scala/ch/wsl/box/rest/io/geotools/Map2Image.scala new file mode 100644 index 00000000..7075ab80 --- /dev/null +++ b/server/src/main/scala/ch/wsl/box/rest/io/geotools/Map2Image.scala @@ -0,0 +1,170 @@ +package ch.wsl.box.rest.io.geotools + +import ch.wsl.box.model.shared.GeoJson.Geometry +import ch.wsl.box.model.shared.geo.Box2d +import org.geotools.api.data.SimpleFeatureSource +import org.geotools.api.referencing.crs.CoordinateReferenceSystem +import org.geotools.api.style.{NamedLayer, Style, StyledLayerDescriptor} +import org.geotools.data.collection.{CollectionFeatureSource, ListFeatureCollection} +import org.geotools.factory.CommonFactoryFinder +import org.geotools.feature.DefaultFeatureCollection +import org.geotools.feature.simple.{SimpleFeatureBuilder, SimpleFeatureTypeBuilder} +import org.geotools.geometry.jts.ReferencedEnvelope +import org.geotools.map.{FeatureLayer, MapContent, MessageDirectLayer} +import org.geotools.ows.wms.WebMapServer +import org.geotools.ows.wms.map.WMSLayer +import org.geotools.ows.wmts.WebMapTileServer +import org.geotools.ows.wmts.map.WMTSMapLayer +import org.geotools.ows.wmts.model.WMTSLayer +import org.geotools.renderer.lite.StreamingRenderer +import org.geotools.sld.v1_1.{SLD, SLDConfiguration} +import org.geotools.styling.BasicPolygonStyle +import org.geotools.xml.styling.SLDParser +import org.geotools.xsd.DOMParser +import org.w3c.dom.Document + +import java.awt.{Color, Rectangle, RenderingHints} +import java.awt.image.BufferedImage +import java.io.{ByteArrayOutputStream, StringReader} +import collection.JavaConverters._ +import java.net.URL +import javax.imageio.ImageIO +import javax.xml.parsers.DocumentBuilderFactory +import scala.concurrent.{ExecutionContext, Future} + +object Map2Image { + + private def renderMapToPNG(mapContent: MapContent, width: Int, height: Int,bbox:Box2d,crs:CoordinateReferenceSystem, copyright:String): Array[Byte] = { + val image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + val graphics = image.createGraphics + + val java2dHints = + new RenderingHints( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON + ) + + + val rendererHints = + new java.util.HashMap[AnyRef, Any]() + + + rendererHints.put( + StreamingRenderer.LINE_WIDTH_OPTIMIZATION_KEY, + java.lang.Boolean.FALSE + ) + + rendererHints.put( + StreamingRenderer.VECTOR_RENDERING_KEY, + java.lang.Boolean.TRUE + ) + + // Set the background color (optional) + graphics.setBackground(Color.WHITE) + graphics.clearRect(0, 0, width, height) + // Create a renderer + val renderer = new StreamingRenderer + renderer.setJava2DHints(java2dHints) + renderer.setRendererHints(rendererHints) + renderer.setMapContent(mapContent) + // Set up the rendering context + val paintArea = new Rectangle(0, 0, width, height) + + val envelope = new ReferencedEnvelope(bbox.xMin,bbox.xMax,bbox.yMin,bbox.yMax,crs) + + renderer.paint(graphics, paintArea, envelope) + + graphics.setColor(Color.white) + graphics.fillRect(0,height - 30,width,30) + graphics.setColor(Color.DARK_GRAY) + graphics.drawString(copyright, 0, height - 15) + + + + val byteArrayOutputStream = new ByteArrayOutputStream + + // Save the image to file + ImageIO.write(image, "png", byteArrayOutputStream) + // Clean up + graphics.dispose() + mapContent.dispose() + + byteArrayOutputStream.toByteArray() + } + + private def loadStyle(sld:String):Seq[Style] = { + val styleFactory = CommonFactoryFinder.getStyleFactory() + + // Create a DocumentBuilder to parse the XML String + val factory = DocumentBuilderFactory.newInstance + factory.setNamespaceAware(true) + val builder = factory.newDocumentBuilder + + val source = new org.xml.sax.InputSource(new StringReader(sld)) + + val document = builder.parse(source) + + val sldParser = new SLDParser(styleFactory) + + sldParser.readDOM(document) + + val parser = new DOMParser( + new SLDConfiguration(), + document + ) + + val sldParsed = parser.parse().asInstanceOf[StyledLayerDescriptor] + sldParsed.getStyledLayers().head.asInstanceOf[NamedLayer].getStyles + + + + } + + def renderPng(wmtsServer:String,layerName:String,width:Int,height:Int, bbox:Box2d,geoms:Seq[Geometry],sld:String)(implicit ex:ExecutionContext):Future[Array[Byte]] = Future{ + + val url = new URL(wmtsServer); + + val mapcontent = new MapContent + + val wmts = new WebMapTileServer(url) + val capabilities = wmts.getCapabilities() + + // gets all the layers in a flat list, in the order they appear in + // the capabilities document (so the rootLayer is at index 0) + val layer = capabilities.getLayer(layerName) + + val copyright = capabilities.getService.getContactInformation.getOrganisationName.toString() + + val crs = capabilities.getMatrixSets.asScala.head.getCoordinateReferenceSystem + + + mapcontent.setTitle(wmts.getCapabilities.getService.getTitle) + mapcontent.addLayer(new WMTSMapLayer(wmts, layer)) + + + // Create vector schema + val builder: SimpleFeatureTypeBuilder = new SimpleFeatureTypeBuilder + builder.setName("geom") + GeotoolUtils.geomSchema(builder,"geom",geoms.map(x => Some(x)),geoms.headOption.map(_.crs.srid).getOrElse(0)) + val schema = builder.buildFeatureType() + val collection = new DefaultFeatureCollection("geoms", schema) + val featureBuilder = new SimpleFeatureBuilder(schema) + + // Add geoms + geoms.flatMap(GeoJsonConverter.toJTS).foreach{ g => + featureBuilder.add(g) + collection.add(featureBuilder.buildFeature(null)) + } + val source = new CollectionFeatureSource(collection) + + + // Load SLD style + val styles = loadStyle(sld) + + // Add vector layer + mapcontent.addLayer(new FeatureLayer(source,styles.head)) + + renderMapToPNG(mapcontent,width,height,bbox,crs,copyright) + + } +} diff --git a/server/src/main/scala/ch/wsl/box/rest/io/geotools/SLD.scala b/server/src/main/scala/ch/wsl/box/rest/io/geotools/SLD.scala new file mode 100644 index 00000000..c66f68aa --- /dev/null +++ b/server/src/main/scala/ch/wsl/box/rest/io/geotools/SLD.scala @@ -0,0 +1,60 @@ +package ch.wsl.box.rest.io.geotools + +object SLD { + def defaultPoint = """ + | + | + | New scratch layer + | + | New scratch layer + | + | + | Single symbol + | + | + | + | circle + | + | #db1e2a + | + | + | #801119 + | 1 + | + | + | 14 + | + | + | + | + | + | + | + |""".stripMargin + + def defaultPolygon = s""" + | + | + | New scratch layer + | + | New scratch layer + | + | + | Single symbol + | + | + | #e41a1c + | 0.47 + | + | + | #800e10 + | 1 + | bevel + | + | + | + | + | + | + |""".stripMargin +} diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/functions/RuntimeFunction.scala b/server/src/main/scala/ch/wsl/box/rest/logic/functions/RuntimeFunction.scala index 4d2ebaeb..0530f80c 100644 --- a/server/src/main/scala/ch/wsl/box/rest/logic/functions/RuntimeFunction.scala +++ b/server/src/main/scala/ch/wsl/box/rest/logic/functions/RuntimeFunction.scala @@ -8,6 +8,7 @@ import io.circe.Json import scala.concurrent.{ExecutionContext, Future} import ch.wsl.box.jdbc.PostgresProfile.api._ +import ch.wsl.box.model.shared.GeoJson.Geometry import ch.wsl.box.model.shared.{DataResult, DataResultTable} import ch.wsl.box.services.Services @@ -25,6 +26,7 @@ trait RuntimePSQL{ trait RuntimeUtils{ def qrCode(url:String):String + def swissTopoMap(geometry:Option[Json], width:Int, height:Int, padding:Int = 500):String } case class Context(data:Json,ws:RuntimeWS,psql:RuntimePSQL,utils:RuntimeUtils) diff --git a/server/src/main/scala/ch/wsl/box/rest/logic/functions/UtilsImpl.scala b/server/src/main/scala/ch/wsl/box/rest/logic/functions/UtilsImpl.scala index f2822e69..8be1b4bc 100644 --- a/server/src/main/scala/ch/wsl/box/rest/logic/functions/UtilsImpl.scala +++ b/server/src/main/scala/ch/wsl/box/rest/logic/functions/UtilsImpl.scala @@ -1,4 +1,8 @@ package ch.wsl.box.rest.logic.functions +import ch.wsl.box.model.shared.GeoJson +import ch.wsl.box.model.shared.GeoJson.{CRS, Coordinates, Geometry, Point} +import ch.wsl.box.model.shared.geo.Box2d +import ch.wsl.box.rest.io.geotools.{Map2Image, SLD} import com.google.zxing.BarcodeFormat import com.google.zxing.client.j2se.MatrixToImageWriter import com.google.zxing.qrcode.QRCodeWriter @@ -8,8 +12,12 @@ import java.util.Base64 import javax.imageio.ImageIO import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import io.circe.Json import java.util +import scala.concurrent.Await +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration.DurationInt object UtilsImpl extends RuntimeUtils { override def qrCode(url: String): String = { @@ -27,4 +35,52 @@ object UtilsImpl extends RuntimeUtils { os.close() result } + + override def swissTopoMap(geomJS: Option[Json], width:Int, height:Int, padding:Int = 500): String = { + import Geometry._ + val geometry = geomJS.get.as[Geometry].toOption.get + val minX = geometry.allCoordinates.map(_.x).min + val maxX = geometry.allCoordinates.map(_.x).max + val minY = geometry.allCoordinates.map(_.y).min + val maxY = geometry.allCoordinates.map(_.y).max + val geomRatio = (maxX - minX + padding*2) / (maxY - minY + padding*2) + val reqRatio = width.toDouble / height + val ratio = geomRatio / reqRatio + val bbox = if(geomRatio == reqRatio) { + Box2d(minX - padding,minY - padding, maxX + padding, maxY + padding) + } else if(geomRatio > reqRatio) { + val adj = padding * ratio + Box2d(minX - padding,minY - adj, maxX + padding, maxY + adj) + } else { + val adj = padding / ratio + Box2d(minX - adj,minY - padding, maxX + adj, maxY + padding) + } + + val style = geometry match { + case geometry: GeoJson.SingleGeometry => geometry match { + case GeoJson.Empty => SLD.defaultPoint + case Point(coordinates, crs) => SLD.defaultPoint + case GeoJson.LineString(coordinates, crs) => SLD.defaultPoint + case GeoJson.Polygon(coordinates, crs) => SLD.defaultPolygon + } + case GeoJson.MultiPoint(coordinates, crs) => SLD.defaultPoint + case GeoJson.MultiLineString(coordinates, crs) => SLD.defaultPoint + case GeoJson.MultiPolygon(coordinates, crs) => SLD.defaultPolygon + case GeoJson.GeometryCollection(geometries, crs) => SLD.defaultPoint + } + + + val fut = Map2Image.renderPng( + "https://wmts.geo.admin.ch/EPSG/2056/1.0.0/WMTSCapabilities.xml", + "ch.swisstopo.pixelkarte-farbe", + width,height, + bbox, + Seq(geometry), + style + ) + val file = Await.result(fut,20.seconds) + Base64.getEncoder.encodeToString(file) + } + + } diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/BoxFormMetadataFactory.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/BoxFormMetadataFactory.scala index 10dc865b..9e6dec31 100644 --- a/server/src/main/scala/ch/wsl/box/rest/metadata/BoxFormMetadataFactory.scala +++ b/server/src/main/scala/ch/wsl/box/rest/metadata/BoxFormMetadataFactory.scala @@ -43,8 +43,8 @@ object BoxFormMetadataFactory extends Logging with MetadataFactory { FormUIDef.field_static(tablesAndViews,functions.map(_.name),fields,roles), FormUIDef.fieldI18n(services.config.langs,fields), FormUIDef.formI18n(viewsOnly,services.config.langs), - FormUIDef.form_actions(functions.map(_.name)), - FormUIDef.form_navigation_actions(functions.map(_.name)), + FormUIDef.form_actions(functions.map(_.name),roles), + FormUIDef.form_navigation_actions(functions.map(_.name),roles), FunctionUIDef.main, FunctionUIDef.field(tablesAndViews), FunctionUIDef.fieldI18n(services.config.langs), @@ -92,8 +92,8 @@ object BoxFormMetadataFactory extends Logging with MetadataFactory { FormUIDef.field_childs(forms,fields,roles), FormUIDef.fieldI18n(services.config.langs,fields), FormUIDef.formI18n(viewsOnly,services.config.langs), - FormUIDef.form_actions(functions.map(_.name)), - FormUIDef.form_navigation_actions(functions.map(_.name)) + FormUIDef.form_actions(functions.map(_.name),roles), + FormUIDef.form_navigation_actions(functions.map(_.name),roles) ) case f if f.objId == PAGE => Seq(FormUIDef.field_static(tablesAndViews,functions.map(_.name),fields,roles),FormUIDef.field_childs(forms,fields,roles),FormUIDef.fieldI18n(services.config.langs,fields),FormUIDef.formI18n(viewsOnly,services.config.langs)) case f if f.objId == FORM_FIELD => Seq(FormUIDef.fieldI18n(services.config.langs,fields)) diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala index 212441f1..04c2c8d7 100755 --- a/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala +++ b/server/src/main/scala/ch/wsl/box/rest/metadata/FormMetadataFactory.scala @@ -218,6 +218,8 @@ object FormMetadataFactory extends Logging with MetadataFactory{ enabledRoles = a.enabled_roles, view = a.view_enable, edit = a.edit_enable, + needUpdateRight = a.need_update_right, + needDeleteRight = a.need_delete_right, ) }.filter(services.config.localDb || _.action != SaveLocalAction), navigationActions = navigationActions.map{a => @@ -234,6 +236,8 @@ object FormMetadataFactory extends Logging with MetadataFactory{ enabledRoles = a.enabled_roles, view = a.view_enable, edit = a.edit_enable, + needUpdateRight = a.need_update_right, + needDeleteRight = a.need_delete_right, ) }, tableActions = if(tableActions.isEmpty && keys.nonEmpty) FormActionsMetadata.default.tableActions else { diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/box/CommonField.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/box/CommonField.scala index 939e1027..6b3358a1 100644 --- a/server/src/main/scala/ch/wsl/box/rest/metadata/box/CommonField.scala +++ b/server/src/main/scala/ch/wsl/box/rest/metadata/box/CommonField.scala @@ -94,7 +94,7 @@ object CommonField { )) ).asPopup - def roles(available_roles:Seq[String]) = JSONField(JSONFieldTypes.ARRAY_STRING,"roles",true, + def roles(available_roles:Seq[String], field:String = "roles") = JSONField(JSONFieldTypes.ARRAY_STRING,field,true, widget = Some(WidgetsNames.multipleLookup), lookup = Some(JSONFieldLookup.prefilled(available_roles.map(x => JSONLookup(Json.fromString(x),Seq(x))))) ) diff --git a/server/src/main/scala/ch/wsl/box/rest/metadata/box/FormUIDef.scala b/server/src/main/scala/ch/wsl/box/rest/metadata/box/FormUIDef.scala index e18a5c0c..9b459c01 100644 --- a/server/src/main/scala/ch/wsl/box/rest/metadata/box/FormUIDef.scala +++ b/server/src/main/scala/ch/wsl/box/rest/metadata/box/FormUIDef.scala @@ -614,7 +614,7 @@ object FormUIDef { action = FormActionsMetadata.default ) - def form_actions(functions:Seq[String]) = JSONMetadata( + def form_actions(functions:Seq[String],roles:Seq[String]) = JSONMetadata( objId = FORM_ACTION, kind = EntityKind.BOX_FORM.kind, name = "Form action", @@ -659,6 +659,11 @@ object FormUIDef { ), JSONField(JSONFieldTypes.BOOLEAN,"update_only",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), JSONField(JSONFieldTypes.BOOLEAN,"insert_only",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + JSONField(JSONFieldTypes.BOOLEAN,"view_enable",false,widget = Some(WidgetsNames.checkbox), default = Some("true")), + JSONField(JSONFieldTypes.BOOLEAN,"edit_enable",false,widget = Some(WidgetsNames.checkbox), default = Some("true")), + JSONField(JSONFieldTypes.BOOLEAN,"need_update_right",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + JSONField(JSONFieldTypes.BOOLEAN,"need_delete_right",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + CommonField.roles(roles,"enabled_roles"), JSONField(JSONFieldTypes.BOOLEAN,"reload",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), JSONField(JSONFieldTypes.BOOLEAN,"html_check",false,widget = Some(WidgetsNames.checkbox), default = Some("true")), JSONField(JSONFieldTypes.STRING,"confirm_text",true,label=Some("Confirm text"), @@ -692,6 +697,11 @@ object FormUIDef { "label", "update_only", "insert_only", + "view_enable", + "edit_enable", + "need_update_right", + "need_delete_right", + "enabled_roles", "reload", "confirm_text", "condition", @@ -712,7 +722,7 @@ object FormUIDef { ) - def form_navigation_actions(functions:Seq[String]) = JSONMetadata( + def form_navigation_actions(functions:Seq[String],roles:Seq[String]) = JSONMetadata( objId = FORM_NAVIGATION_ACTION, kind = EntityKind.BOX_FORM.kind, name = "Form navigation action", @@ -752,6 +762,11 @@ object FormUIDef { JSONField(JSONFieldTypes.BOOLEAN,"update_only",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), JSONField(JSONFieldTypes.BOOLEAN,"insert_only",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), JSONField(JSONFieldTypes.BOOLEAN,"reload",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + JSONField(JSONFieldTypes.BOOLEAN,"view_enable",false,widget = Some(WidgetsNames.checkbox), default = Some("true")), + JSONField(JSONFieldTypes.BOOLEAN,"edit_enable",false,widget = Some(WidgetsNames.checkbox), default = Some("true")), + JSONField(JSONFieldTypes.BOOLEAN,"need_update_right",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + JSONField(JSONFieldTypes.BOOLEAN,"need_delete_right",false,widget = Some(WidgetsNames.checkbox), default = Some("false")), + CommonField.roles(roles,"enabled_roles"), JSONField(JSONFieldTypes.STRING,"confirm_text",true,label=Some("Confirm text"), tooltip = Some("Before running action show a popup that ask for confirmation with the following text (translated with gobal transaltion table)"), widget = Some(WidgetsNames.input) @@ -777,6 +792,11 @@ object FormUIDef { "label", "update_only", "insert_only", + "view_enable", + "edit_enable", + "need_update_right", + "need_delete_right", + "enabled_roles", "reload", "confirm_text" )), diff --git a/server/src/test/scala/ch/wsl/box/rest/RuntimeFunctionSpec.scala b/server/src/test/scala/ch/wsl/box/rest/RuntimeFunctionSpec.scala index 79384af8..bb3316ce 100644 --- a/server/src/test/scala/ch/wsl/box/rest/RuntimeFunctionSpec.scala +++ b/server/src/test/scala/ch/wsl/box/rest/RuntimeFunctionSpec.scala @@ -2,7 +2,7 @@ package ch.wsl.box.rest import akka.actor.ActorSystem import akka.stream.{ActorMaterializer, Materializer} -import ch.wsl.box.model.shared.{DataResultTable, JSONQuery} +import ch.wsl.box.model.shared.{DataResultTable, GeoJson, JSONQuery} import ch.wsl.box.rest.logic._ import ch.wsl.box.rest.logic.functions.{Context, RuntimeFunction, RuntimePSQL, RuntimeUtils, RuntimeWS} import ch.wsl.box.rest.utils.{Lang, UserProfile} @@ -39,6 +39,8 @@ class RuntimeFunctionSpec extends BaseSpec { }, new RuntimeUtils { override def qrCode(url: String): String = ??? + + override def swissTopoMap(geometry:Option[Json], width:Int, height:Int, buffer:Int = 500): String = ??? } ) diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/FormActions.scala b/shared/src/main/scala/ch/wsl/box/model/shared/FormActions.scala index 3a8ac048..9a9a1599 100644 --- a/shared/src/main/scala/ch/wsl/box/model/shared/FormActions.scala +++ b/shared/src/main/scala/ch/wsl/box/model/shared/FormActions.scala @@ -53,15 +53,17 @@ object Importance { sealed trait Target case object Self extends Target case object NewWindow extends Target +case object Popup extends Target object Target { def fromString(s:String):Target = s match { case "Self" => Self case "NewWindow" => NewWindow + case "Popup" => Popup } - def all = Seq(Self,NewWindow) + def all = Seq(Self,NewWindow,Popup) } case class FormAction( diff --git a/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala b/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala index 3f7e3746..74025ca3 100644 --- a/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala +++ b/shared/src/main/scala/ch/wsl/box/model/shared/GeoJson.scala @@ -60,6 +60,7 @@ object GeoJson { object CRS { def default = wgs84 def wgs84 = CRS("EPSG:4326") + def epsg(epsg:Int) = CRS(s"EPSG:$epsg") }