Skip to content
Browse files

Misc. UI Enhancements.

* implementing layer ordering buttons, recreating text annotations as children of a pin marker icon and adds hover/click functionality for annotations when svg is loaded as markup, fixing styling for select to show dropshadow over any item that would be selected when clicking, populating layers when loading an existing observation, adding ability to set default tool when a form is loaded instead of being hardcoded to path tool, fixing svg view size style update in firefox

* implementing negative viewbox origin, fixing move tool functionalitty, exclude background rect and ignored layers from highlighting, changing some flexbox behavior, addressing issue where <tspan>s get different x,y than parent <text>, removing clipMove behavior

* fixing pin placement to match clicked location

* fixing issue where svg.js adds multiple duplicate xmlns attributes causing server side validation failures
  • Loading branch information
kml27 authored and mks-d committed Nov 12, 2019
1 parent 6816752 commit b0d03c92412ce58405aa72047b653eef935055d2
@@ -16,28 +16,29 @@
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.openmrs.api.context.Context;
import org.openmrs.Concept;
import org.openmrs.Obs;
import org.openmrs.module.drawing.DrawingConstants;
import org.openmrs.api.context.Context;
import org.openmrs.module.drawing.AnnotatedImage;
import org.openmrs.module.drawing.DrawingConstants;
import org.openmrs.module.drawing.DrawingUtil;
import org.openmrs.module.htmlformentry.FormEntryContext;
import org.openmrs.module.htmlformentry.FormEntryContext.Mode;
import org.openmrs.module.htmlformentry.FormEntrySession;
import org.openmrs.module.htmlformentry.FormSubmissionActions;
import org.openmrs.module.htmlformentry.FormSubmissionError;
import org.openmrs.module.htmlformentry.HtmlFormEntryUtil;
import org.openmrs.module.htmlformentry.action.FormSubmissionControllerAction;
import org.openmrs.module.htmlformentry.element.HtmlGeneratorElement;
//import org.openmrs.module.htmlformentry.schema.ObsGroup;
//import org.openmrs.module.htmlformentry.action.ObsGroupAction;
import org.openmrs.obs.ComplexData;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

//public class DrawingSubmissionElement {
public class DrawingSubmissionElement implements HtmlGeneratorElement, FormSubmissionControllerAction {

private static final Log log = LogFactory.getLog(DrawingSubmissionElement.class);
@@ -46,7 +47,7 @@

private Concept questionConcept;

private Obs existingObs;
private Obs parentObs;

private enum DisplayMode {
@@ -66,6 +67,8 @@

private String fixedHeight;

private String defaultTool;

private Document svgWidgetTemplate;

public DrawingSubmissionElement(FormEntryContext context, Map<String, String> parameters)
@@ -185,9 +188,10 @@ public String apply(DisplayMode arg0) {

Map<Concept, List<Obs>> prevObs = context.getExistingObs();

if (prevObs != null && prevObs.size() > 0) {

existingObs = prevObs.get(questionConcept).get(0);
parentObs = prevObs.get(questionConcept).get(0);

//make sure the instance is loaded with complexData, according
//to getComplexData() comments it won't be guaranteed unless
@@ -199,7 +203,7 @@ public String apply(DisplayMode arg0) {
//(and thereby deletes the complex obs file)
//by not triggering the set of the dirty flag, saveObsNotDirty
//will be called in ObsServiceImpl.saveObs() instead
existingObs = Context.getObsService().getObs(existingObs.getId());
parentObs = Context.getObsService().getObs(parentObs.getId());

//TODO move to static (or does it require autowiring?)
@@ -208,6 +212,19 @@ public String apply(DisplayMode arg0) {
//(which over head is greater? clone or read from disk?)
svgWidgetTemplate = DrawingUtil.loadEditorMarkup(DrawingConstants.EDITOR_HTML_PATH);

//opening the form for view/edit should not remove the existing observation
//TODO evaluate supporting the same concept multiple times in the same form
//Obs o = context.removeExistingObs(questionConcept, (Concept) null);

defaultTool = parameters.get("defaultTool");

//if no mode was specified, or using signature mode,
//default to path tool, while it's not strictly required as default
//for annotation mode, it currently should be for signature mode
if (defaultTool == null || instancePurpose.equals(DisplayMode.Signature)) {
defaultTool = "path-tool";


@@ -225,6 +242,8 @@ public String apply(DisplayMode arg0) {
public void handleSubmission(FormEntrySession session, HttpServletRequest submission) {

FormSubmissionActions actions = session.getSubmissionActions();

String svgDOM = submission.getParameter("svgDOM");

//only create a "new" obs if this parameter is posted
@@ -237,7 +256,7 @@ public void handleSubmission(FormEntrySession session, HttpServletRequest submis

if (svgDOM == null || StringUtils.isBlank(svgDOM)) {

String markup = new String((byte[]) existingObs.getComplexData().getData());
String markup = new String((byte[]) parentObs.getComplexData().getData());

ai = new AnnotatedImage(markup);

@@ -248,23 +267,25 @@ public void handleSubmission(FormEntrySession session, HttpServletRequest submis
if (ai.getParsingError() != DrawingUtil.ErrorStatus.NONE)
throw new RuntimeException("Error parsing the SVG document" + ai.getParsingError());

byte[] svgByteArray = DrawingUtil.documentToString(ai.getImageDocument()).getBytes();

try {

byte[] svgByteArray = DrawingUtil.documentToString(ai.getImageDocument()).getBytes();

//TODO verify this update pipeline works, and behaviors between
//both update pipelines match
if (session.getContext().getMode() == Mode.EDIT && existingObs != null) {
session.getSubmissionActions().modifyObs(existingObs, questionConcept,
if (session.getContext().getMode() == Mode.EDIT && parentObs != null) {

actions.modifyObs(parentObs, questionConcept,
new ComplexData(DrawingConstants.BASE_COMPLEX_OBS_FILENAME, svgByteArray), null, null);

//should the modify codepath already handle setting previousObs?
//the new HFE code should handle this
Obs[] newObs = session.getSubmissionActions().getObsToCreate().parallelStream().filter(new Predicate<Obs>() {

public boolean test(Obs curObsToCreate) {
if (curObsToCreate.isComplex() && curObsToCreate.getComplexData() != null
&& svgByteArray.equals((byte[]) curObsToCreate.getComplexData().getData())) {
&& svgByteArray.equals(curObsToCreate.getComplexData().getData())) {
return true;

@@ -273,11 +294,18 @@ public boolean test(Obs curObsToCreate) {



} else {
new ComplexData(DrawingConstants.BASE_COMPLEX_OBS_FILENAME, svgByteArray), null, null);

//this is the first version of this module that supports versioning,
//so it will note this is the "1st version"... this version
//does not directly map to the module version and may not change with
//subsequent module versions, but could if more information is added, removed
//or how the information is stored changes

actions.createObs(questionConcept, new ComplexData(DrawingConstants.BASE_COMPLEX_OBS_FILENAME, svgByteArray),
null, null);

@@ -311,7 +339,7 @@ public String generateHtml(FormEntryContext context) {

//TODO for now, during testing if the file can't be loaded, return early
if (svgWidgetTemplate == null) {
return "";
return "<span style='color:red;'>Error loading drawing element</span>";

//always hide save button when using editor in HFE forms, the submit button will
@@ -336,13 +364,12 @@ public String generateHtml(FormEntryContext context) {

//TODO add translated messages, parse HTML/XML template or provide
//to JS?

//in both view and edit, load the existing svg if it exists
if ((ctxMode == Mode.VIEW || ctxMode == Mode.EDIT) && existingObs != null) {
if ((ctxMode == Mode.VIEW || ctxMode == Mode.EDIT) && parentObs != null) {

AnnotatedImage ai = (AnnotatedImage) new AnnotatedImage(
new String((byte[]) existingObs.getComplexData().getData()));
AnnotatedImage ai = new AnnotatedImage(new String((byte[]) parentObs.getComplexData().getData()));

//TODO after SVG DOM insertion is implemented, handle this section appropriately
Document svgDoc = ai.getImageDocument();
@@ -354,11 +381,13 @@ public String generateHtml(FormEntryContext context) {

//in view mode, disable interaction
if (ctxMode == Mode.VIEW) {

//in view mode only allow select
defaultTool = "select-tool";

//set disabled on all buttons

//set css pointer-events to none on root-svg
String rootClasses = rootSvg.getAttribute("class");
rootSvg.setAttribute("class", rootClasses + " disabledPointerEvents");

NodeList buttons = svgWidgetTemplate.getElementsByTagName("button");
//Element clearAllButton = svgWidgetTemplate.getElementById("");
@@ -367,7 +396,7 @@ public String generateHtml(FormEntryContext context) {

Element button = (Element) buttons.item(i);

addClasses(button, "hidden");
addClasses(button, "hidden disabled");


@@ -387,15 +416,18 @@ public String generateHtml(FormEntryContext context) {
Element imageNode = svgWidgetTemplate.createElement("image");

imageNode.setAttribute("href", base64preload);
imageNode.setAttribute("data-ignore-layer", "true");


//these elements will be used to set explicit heights or visibility for
rootSvg.setAttribute("data-default-tool", defaultTool);

//these elements will be used to set explicit heights or visiblity for
//different purposes
Element parentDiv = DrawingUtil.getElementById("view-layer-parent", svgWidgetTemplate);
Element layerParentDiv = DrawingUtil.getElementById("layer-list-div", svgWidgetTemplate);
Element layerParentDiv = DrawingUtil.getElementById("layer-div", svgWidgetTemplate);
Element layerArrangeDiv = DrawingUtil.getElementById("arrange", svgWidgetTemplate);

switch (instancePurpose) {
@@ -467,6 +499,7 @@ public String generateHtml(FormEntryContext context) {

throw new RuntimeException("Invalid <drawing> displayMode");


Element[] fixedHeightDivs = { parentDiv, layerParentDiv, layerArrangeDiv };
@@ -486,9 +519,9 @@ public String generateHtml(FormEntryContext context) {

css += elemCss + "}";

css += "</style>";


//add a hidden input to provide the SVG on submission
@@ -513,12 +546,10 @@ public String generateHtml(FormEntryContext context) {
widgetMarkup = "<span style='color:red;'>Error loading drawing element</span>";

//TODO use "correct" method to build the path to these js resources
String js = "<script src=\"../../moduleResources/drawing/svg.js\"></script>"
+ "<script src=\"../../moduleResources/drawing/svg.draw.js\"></script>"
+ "<script src=\"../../moduleResources/drawing/ui.js\"></script>";

return css + widgetMarkup + js;

@@ -611,7 +611,7 @@ public void testBlankFormHtml(String html) {
String[] ids = { "text-tool", "template-button", "load-image-button", "path-tool", "line-tool",
"circle-tool", "clear-all", "layer-info-vis-toggle", "edit-tool", "move-tool",
"send-to-front-tool", "move-forward-tool", "move-backward-tool", "send-to-back-tool",
"void-tool", "delete-tool" };
"delete-tool" };

for (String id : ids) {

0 comments on commit b0d03c9

Please sign in to comment.
You can’t perform that action at this time.