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