001/*
002 * (C) Copyright 2006-2008 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Alexandre Russel
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.platform.annotations.gwt.client.view;
021
022import java.util.ArrayList;
023import java.util.List;
024
025import org.nuxeo.ecm.platform.annotations.gwt.client.AnnotationConstant;
026import org.nuxeo.ecm.platform.annotations.gwt.client.controler.AnnotationController;
027import org.nuxeo.ecm.platform.annotations.gwt.client.model.Annotation;
028import org.nuxeo.ecm.platform.annotations.gwt.client.model.AnnotationChangeListener;
029import org.nuxeo.ecm.platform.annotations.gwt.client.model.AnnotationModel;
030import org.nuxeo.ecm.platform.annotations.gwt.client.util.CSSClassManager;
031import org.nuxeo.ecm.platform.annotations.gwt.client.util.ImageRangeXPointer;
032import org.nuxeo.ecm.platform.annotations.gwt.client.util.NullRangeXPointer;
033import org.nuxeo.ecm.platform.annotations.gwt.client.util.Point;
034import org.nuxeo.ecm.platform.annotations.gwt.client.util.StringRangeXPointer;
035import org.nuxeo.ecm.platform.annotations.gwt.client.util.Utils;
036import org.nuxeo.ecm.platform.annotations.gwt.client.util.Visitor;
037import org.nuxeo.ecm.platform.annotations.gwt.client.util.XPathUtil;
038import org.nuxeo.ecm.platform.annotations.gwt.client.util.XPointer;
039import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.DecoratorVisitor;
040import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.DecoratorVisitorFactory;
041import org.nuxeo.ecm.platform.annotations.gwt.client.view.decorator.ImageDecorator;
042
043import com.allen_sauer.gwt.log.client.Log;
044import com.google.gwt.dom.client.BodyElement;
045import com.google.gwt.dom.client.DivElement;
046import com.google.gwt.dom.client.Document;
047import com.google.gwt.dom.client.Element;
048import com.google.gwt.dom.client.ImageElement;
049import com.google.gwt.dom.client.NodeList;
050import com.google.gwt.user.client.Window;
051
052/**
053 * @author <a href="mailto:arussel@nuxeo.com">Alexandre Russel</a>
054 */
055public class AnnotatedDocument implements AnnotationChangeListener {
056    private List<Annotation> annotations = new ArrayList<Annotation>();
057
058    private List<Annotation> decoratedAnnotations = new ArrayList<Annotation>();
059
060    private static XPathUtil xPathUtil = new XPathUtil();
061
062    private final ImageDecorator decorator;
063
064    private AnnotationController controller;
065
066    public AnnotatedDocument(AnnotationController controller) {
067        this.controller = controller;
068        decorator = new ImageDecorator(controller);
069    }
070
071    public void onChange(AnnotationModel model, ChangeEvent ce) {
072        annotations = model.getAnnotations();
073        Log.debug("On change: annotations.empty? " + annotations.isEmpty());
074        if (annotations.isEmpty() || ce == ChangeEvent.annotation) {
075            return;
076        }
077
078        update();
079    }
080
081    public void update() {
082        update(false);
083    }
084
085    public void update(boolean forceDecorate) {
086        Log.debug("Update annotations - forceDecorate: " + forceDecorate);
087        if (annotations == null) {
088            return;
089        }
090
091        if (forceDecorate) {
092            decoratedAnnotations.clear();
093            removeAllAnnotatedAreas();
094        }
095
096        for (Annotation annotation : annotations) {
097            if (!decoratedAnnotations.contains(annotation)) {
098                Log.debug("Decorate annotation");
099                decorate(annotation);
100                decoratedAnnotations.add(annotation);
101            }
102        }
103
104        int selectedAnnotationIndex = getSelectedAnnotationIndex();
105        if (selectedAnnotationIndex > -1) {
106            updateSelectedAnnotation(selectedAnnotationIndex);
107        }
108
109        if (!isAnnotationsVisible()) {
110            Log.debug("Hide annotations!");
111            hideAnnotations();
112            // disable popup listeners in case we just added a new annotation
113            controller.disablePopupListeners();
114        }
115    }
116
117    public void preDecorateDocument() {
118        Document document = Document.get();
119        Log.debug("preDecorateDocument -- isMultiImage? " + controller.isMultiImage());
120        preDecorateDocument(document);
121    }
122
123    private static void preDecorateDocument(Document document) {
124        Log.debug("Predecorate document !");
125        NodeList<Element> elements = document.getElementsByTagName("img");
126        for (int x = 0; x < elements.getLength(); x++) {
127            Element element = elements.getItem(x);
128            DivElement divElement = document.createDivElement();
129            divElement.getStyle().setProperty("position", "relative");
130            divElement.setClassName(AnnotationConstant.IGNORED_ELEMENT);
131            String path = xPathUtil.getXPath(element);
132            path = XPathUtil.toIdableName(path);
133            divElement.setId(path);
134            Element nextSibling = element.getNextSiblingElement();
135            Element parent = element.getParentElement();
136            if (nextSibling == null) {
137                parent.appendChild(divElement);
138            } else {
139                parent.insertBefore(divElement, nextSibling);
140            }
141            divElement.appendChild(element);
142        }
143    }
144
145    public void decorate(Annotation annotation) {
146        XPointer xpointer = annotation.getXpointer();
147        if (xpointer instanceof StringRangeXPointer) {
148            decorateStringRange((StringRangeXPointer) xpointer, annotation);
149        } else if (xpointer instanceof ImageRangeXPointer) {
150            decorateImageRange((ImageRangeXPointer) xpointer, annotation);
151        }
152    }
153
154    private void decorateImageRange(ImageRangeXPointer xpointer, Annotation annotation) {
155        ImageElement img = xpointer.getImage(controller.isMultiImage());
156        if (img == null) {
157            return;
158        }
159        Point[] points = controller.filterAnnotation(xpointer.getTopLeft(), xpointer.getBottomRight());
160        if (points == null) {
161            return;
162        }
163        decorator.addAnnotatedArea(points[0].getX(), points[0].getY(), points[1].getX(), points[1].getY(), img,
164                annotation, controller);
165    }
166
167    private void decorateStringRange(StringRangeXPointer xpointer, Annotation annotation) {
168        DecoratorVisitor processor = DecoratorVisitorFactory.forAnnotation(annotation, controller);
169        Visitor visitor = new Visitor(processor);
170        visitor.process(xpointer.getOwnerDocument());
171    }
172
173    public void updateSelectedAnnotation(int index) {
174        Annotation annotation = annotations.get(index);
175        BodyElement bodyElement = Document.get().getBody();
176        if (!(annotation.getXpointer() instanceof NullRangeXPointer)) {
177            NodeList<Element> spans = bodyElement.getElementsByTagName("span");
178            NodeList<Element> as = bodyElement.getElementsByTagName("div");
179            int scrollTop = Integer.MAX_VALUE;
180            int scrollLeft = Integer.MAX_VALUE;
181            for (int x = 0; x < spans.getLength(); x++) {
182                Element element = spans.getItem(x);
183                if (processElement(annotation, element)) {
184                    int[] absTopLeft = Utils.getAbsoluteTopLeft(element, Document.get());
185                    if (absTopLeft[0] < scrollTop) {
186                        scrollTop = absTopLeft[0];
187                    }
188                    if (absTopLeft[1] < scrollLeft) {
189                        scrollLeft = absTopLeft[1];
190                    }
191                }
192            }
193            for (int x = 0; x < as.getLength(); x++) {
194                Element element = as.getItem(x);
195                if (processElement(annotation, element)) {
196                    int[] absTopLeft = Utils.getAbsoluteTopLeft(element, Document.get());
197                    if (absTopLeft[0] < scrollTop) {
198                        scrollTop = absTopLeft[0];
199                    }
200                    if (absTopLeft[1] < scrollLeft) {
201                        scrollLeft = absTopLeft[1];
202                    }
203                }
204            }
205
206            scrollLeft = scrollLeft == Integer.MAX_VALUE ? 0 : scrollLeft;
207            scrollTop = scrollTop == Integer.MAX_VALUE ? 0 : scrollTop;
208            Window.scrollTo(scrollLeft, scrollTop);
209        }
210    }
211
212    private boolean processElement(Annotation annotation, Element element) {
213        CSSClassManager manager = new CSSClassManager(element);
214        // remove old
215        manager.removeClass(AnnotationConstant.SELECTED_CLASS_NAME);
216        // set new
217        if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME + annotation.getId())) {
218            manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME);
219
220            return true;
221        }
222        return false;
223    }
224
225    private native int getSelectedAnnotationIndex() /*-{
226                                                    if (typeof top['selectedAnnotationIndex'] != "undefined") {
227                                                    return top['selectedAnnotationIndex'];
228                                                    } else {
229                                                    return -1;
230                                                    }
231                                                    }-*/;
232
233    public void hideAnnotations() {
234        BodyElement bodyElement = Document.get().getBody();
235        NodeList<Element> spans = bodyElement.getElementsByTagName("span");
236        NodeList<Element> divs = bodyElement.getElementsByTagName("div");
237
238        for (int x = 0; x < spans.getLength(); x++) {
239            Element element = spans.getItem(x);
240            CSSClassManager manager = new CSSClassManager(element);
241            if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME)) {
242                manager.removeClass(AnnotationConstant.DECORATE_CLASS_NAME);
243                manager.addClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME);
244            }
245        }
246
247        for (int x = 0; x < divs.getLength(); x++) {
248            Element element = divs.getItem(x);
249            CSSClassManager manager = new CSSClassManager(element);
250            if (manager.isClassPresent(AnnotationConstant.DECORATE_CLASS_NAME)) {
251                manager.removeClass(AnnotationConstant.DECORATE_CLASS_NAME);
252                manager.addClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME);
253            }
254        }
255        setAnnotationsShown(false);
256    }
257
258    private native void setAnnotationsShown(boolean annotationsShown) /*-{
259                                                                      top['annotationsShown'] = annotationsShown;
260                                                                      }-*/;
261
262    public void showAnnotations() {
263        BodyElement bodyElement = Document.get().getBody();
264        NodeList<Element> spans = bodyElement.getElementsByTagName("span");
265        NodeList<Element> divs = bodyElement.getElementsByTagName("div");
266
267        for (int x = 0; x < spans.getLength(); x++) {
268            Element element = spans.getItem(x);
269            CSSClassManager manager = new CSSClassManager(element);
270            if (manager.isClassPresent(AnnotationConstant.DECORATE_NOT_CLASS_NAME)) {
271                manager.removeClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME);
272                manager.addClass(AnnotationConstant.DECORATE_CLASS_NAME);
273            }
274            if (manager.isClassPresent(AnnotationConstant.SELECTED_NOT_CLASS_NAME)) {
275                manager.removeClass(AnnotationConstant.SELECTED_NOT_CLASS_NAME);
276                manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME);
277            }
278        }
279
280        for (int x = 0; x < divs.getLength(); x++) {
281            Element element = divs.getItem(x);
282            CSSClassManager manager = new CSSClassManager(element);
283            if (manager.isClassPresent(AnnotationConstant.DECORATE_NOT_CLASS_NAME)) {
284                manager.removeClass(AnnotationConstant.DECORATE_NOT_CLASS_NAME);
285                manager.addClass(AnnotationConstant.DECORATE_CLASS_NAME);
286            }
287            if (manager.isClassPresent(AnnotationConstant.SELECTED_NOT_CLASS_NAME)) {
288                manager.removeClass(AnnotationConstant.SELECTED_NOT_CLASS_NAME);
289                manager.addClass(AnnotationConstant.SELECTED_CLASS_NAME);
290            }
291        }
292        setAnnotationsShown(true);
293    }
294
295    public native boolean isAnnotationsVisible() /*-{
296                                                 if (typeof top['annotationsShown'] != "undefined") {
297                                                 return top['annotationsShown'];
298                                                 } else {
299                                                 return true;
300                                                 }
301                                                 }-*/;
302
303    private void removeAllAnnotatedAreas() {
304        String className = isAnnotationsVisible() ? AnnotationConstant.DECORATE_CLASS_NAME
305                : AnnotationConstant.DECORATE_NOT_CLASS_NAME;
306        BodyElement bodyElement = Document.get().getBody();
307        NodeList<Element> as = bodyElement.getElementsByTagName("div");
308        removeAnchorAreas(as, className);
309        removeSpanAreas(className);
310    }
311
312    private void removeAnchorAreas(NodeList<Element> nodes, String className) {
313        List<Element> elements = getElementsToRemove(nodes, className);
314        for (Element element : elements) {
315            element.getParentElement().removeChild(element);
316        }
317    }
318
319    private List<Element> getElementsToRemove(NodeList<Element> nodes, String className) {
320        List<Element> elementsToRemove = new ArrayList<Element>();
321        for (int i = 0; i < nodes.getLength(); ++i) {
322            Element element = nodes.getItem(i);
323            CSSClassManager manager = new CSSClassManager(element);
324            if (manager.isClassPresent(className)) {
325                elementsToRemove.add(element);
326            }
327        }
328        return elementsToRemove;
329    }
330
331    private void removeSpanAreas(String className) {
332        NodeList<Element> spans = Document.get().getBody().getElementsByTagName("span");
333        List<Element> elements = getElementsToRemove(spans, className);
334        while (!elements.isEmpty()) {
335            Element element = elements.get(0);
336            String elementHtml = element.getInnerHTML();
337            Element parent = element.getParentElement();
338            String parentHtml = parent.getInnerHTML();
339
340            String escapedClassName = element.getClassName().replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])",
341                    "\\\\$1");
342            String escapedElementHtml = elementHtml.replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])", "\\\\$1");
343
344            parentHtml = parentHtml.replaceFirst("<(span|SPAN) class=(\")?" + escapedClassName + "(\")?.*>"
345                    + escapedElementHtml + "</(span|SPAN)>", elementHtml);
346            parent.setInnerHTML(parentHtml);
347
348            spans = Document.get().getBody().getElementsByTagName("span");
349            elements = getElementsToRemove(spans, className);
350        }
351    }
352
353    public void decorateSelectedText(Annotation annotation) {
354        DecoratorVisitor processor = DecoratorVisitorFactory.forSelectedText(annotation);
355        Visitor visitor = new Visitor(processor);
356        StringRangeXPointer xpointer = (StringRangeXPointer) annotation.getXpointer();
357        visitor.process(xpointer.getOwnerDocument());
358    }
359
360    public void removeSelectedTextDecoration(Annotation annotation) {
361        String className = AnnotationConstant.SELECTED_TEXT_CLASS_NAME;
362        removeSpanAreas(className);
363    }
364
365}