1 /*
2 * A class for handling shape interaction with libavoid.
3 *
4 * Authors:
5 * Michael Wybrow <mjwybrow@users.sourceforge.net>
6 * Abhishek Sharma
7 *
8 * Copyright (C) 2005 Michael Wybrow
9 *
10 * Released under GNU GPL, read the file 'COPYING' for more information
11 */
14 #include <cstring>
15 #include <string>
16 #include <iostream>
18 #include "sp-item.h"
19 #include "display/curve.h"
20 #include "2geom/line.h"
21 #include "2geom/crossing.h"
22 #include "2geom/convex-cover.h"
23 #include "helper/geom-curves.h"
24 #include "svg/stringstream.h"
25 #include "conn-avoid-ref.h"
26 #include "connection-points.h"
27 #include "sp-conn-end.h"
28 #include "sp-path.h"
29 #include "libavoid/router.h"
30 #include "libavoid/connector.h"
31 #include "libavoid/geomtypes.h"
32 #include "libavoid/shape.h"
33 #include "xml/node.h"
34 #include "document.h"
35 #include "desktop.h"
36 #include "desktop-handles.h"
37 #include "sp-namedview.h"
38 #include "sp-item-group.h"
39 #include "inkscape.h"
40 #include <glibmm/i18n.h>
42 using Inkscape::DocumentUndo;
44 using Avoid::Router;
46 static Avoid::Polygon avoid_item_poly(SPItem const *item);
49 SPAvoidRef::SPAvoidRef(SPItem *spitem)
50 : shapeRef(NULL)
51 , item(spitem)
52 , setting(false)
53 , new_setting(false)
54 , _transformed_connection()
55 {
56 }
59 SPAvoidRef::~SPAvoidRef()
60 {
61 _transformed_connection.disconnect();
63 // If the document is being destroyed then the router instance
64 // and the ShapeRefs will have been destroyed with it.
65 const bool routerInstanceExists = (item->document->router != NULL);
67 if (shapeRef && routerInstanceExists) {
68 // Deleting the shapeRef will remove it completely from
69 // an existing Router instance.
70 delete shapeRef;
71 }
72 shapeRef = NULL;
73 }
76 void SPAvoidRef::setAvoid(char const *value)
77 {
78 if (SP_OBJECT_IS_CLONED(item)) {
79 // Don't keep avoidance information for cloned objects.
80 return;
81 }
82 new_setting = false;
83 if (value && (strcmp(value, "true") == 0)) {
84 new_setting = true;
85 }
86 }
88 void print_connection_points(std::map<int, ConnectionPoint>& cp)
89 {
90 std::map<int, ConnectionPoint>::iterator i;
91 for (i=cp.begin(); i!=cp.end(); ++i)
92 {
93 const ConnectionPoint& p = i->second;
94 std::cout<<p.id<<" "<<p.type<<" "<<p.pos[Geom::X]<<" "<<p.pos[Geom::Y]<<std::endl;
95 }
96 }
98 void SPAvoidRef::setConnectionPoints(gchar const *value)
99 {
100 std::set<int> updates;
101 std::set<int> deletes;
102 std::set<int> seen;
104 if (value)
105 {
106 /* Rebuild the connection points list.
107 Update the connectors for which
108 the endpoint has changed.
109 */
111 gchar ** strarray = g_strsplit(value, "|", 0);
112 gchar ** iter = strarray;
114 while (*iter != NULL) {
115 ConnectionPoint cp;
116 Inkscape::SVGIStringStream is(*iter);
117 is>>cp;
118 cp.type = ConnPointUserDefined;
120 /* Mark this connection point as seen, so we can delete
121 the other ones.
122 */
123 seen.insert(cp.id);
124 if ( connection_points.find(cp.id) != connection_points.end() )
125 {
126 /* An already existing connection point.
127 Check to see if changed, and, if it is
128 the case, trigger connector update for
129 the connector attached to this connection
130 point. This is done by adding the
131 connection point to a list of connection
132 points to be updated.
133 */
134 if ( connection_points[cp.id] != cp )
135 // The connection point got updated.
136 // Put it in the update list.
137 updates.insert(cp.id);
138 }
139 connection_points[cp.id] = cp;
140 ++iter;
141 }
142 /* Delete the connection points that didn't appear
143 in the new connection point list.
144 */
145 std::map<int, ConnectionPoint>::iterator it;
147 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
148 if ( seen.find(it->first) == seen.end())
149 deletes.insert(it->first);
150 g_strfreev(strarray);
151 }
152 else
153 {
154 /* Delete all the user-defined connection points
155 Actually we do this by adding them to the list
156 of connection points to be deleted.
157 */
158 std::map<int, ConnectionPoint>::iterator it;
160 for (it=connection_points.begin(); it!=connection_points.end(); ++it)
161 deletes.insert(it->first);
162 }
163 /* Act upon updates and deletes.
164 */
165 if (deletes.empty() && updates.empty())
166 // Nothing to do, just return.
167 return;
168 // Get a list of attached connectors.
169 GSList* conns = getAttachedConnectors(Avoid::runningToAndFrom);
170 for (GSList *i = conns; i != NULL; i = i->next)
171 {
172 SPPath* path = SP_PATH(i->data);
173 SPConnEnd** connEnds = path->connEndPair.getConnEnds();
174 for (int ix=0; ix<2; ++ix) {
175 if (connEnds[ix]->type == ConnPointUserDefined) {
176 if (updates.find(connEnds[ix]->id) != updates.end()) {
177 if (path->connEndPair.isAutoRoutingConn()) {
178 path->connEndPair.tellLibavoidNewEndpoints();
179 } else {
180 }
181 }
182 else if (deletes.find(connEnds[ix]->id) != deletes.end()) {
183 sp_conn_end_detach(path, ix);
184 }
185 }
186 }
187 }
188 g_slist_free(conns);
189 // Remove all deleted connection points
190 if (deletes.size())
191 for (std::set<int>::iterator it = deletes.begin(); it != deletes.end(); ++it)
192 connection_points.erase(*it);
193 }
195 void SPAvoidRef::setConnectionPointsAttrUndoable(const gchar* value, const gchar* action)
196 {
197 SPDocument* doc = SP_OBJECT_DOCUMENT(item);
199 item->setAttribute( "inkscape:connection-points", value, 0 );
200 item->updateRepr();
201 doc->ensureUpToDate();
202 DocumentUndo::done(doc, SP_VERB_CONTEXT_CONNECTOR, action);
203 }
205 void SPAvoidRef::addConnectionPoint(ConnectionPoint &cp)
206 {
207 Inkscape::SVGOStringStream ostr;
208 bool first = true;
209 int newId = 1;
210 if ( connection_points.size() )
211 {
212 for (IdConnectionPointMap::iterator it = connection_points.begin(); ; )
213 {
214 if ( first )
215 {
216 first = false;
217 ostr<<it->second;
218 }
219 else
220 ostr<<'|'<<it->second;
221 IdConnectionPointMap::iterator prev_it = it;
222 ++it;
223 if ( it == connection_points.end() || prev_it->first + 1 != it->first )
224 {
225 newId = prev_it->first + 1;
226 break;
227 }
228 }
229 }
230 cp.id = newId;
231 if ( first )
232 {
233 first = false;
234 ostr<<cp;
235 }
236 else
237 ostr<<'|'<<cp;
239 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Add a new connection point") );
240 }
242 void SPAvoidRef::updateConnectionPoint(ConnectionPoint &cp)
243 {
244 Inkscape::SVGOStringStream ostr;
245 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
246 if ( cp_pos != connection_points.end() )
247 {
248 bool first = true;
249 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it)
250 {
251 ConnectionPoint* to_write;
252 if ( it != cp_pos )
253 to_write = &it->second;
254 else
255 to_write = &cp;
256 if ( first )
257 {
258 first = false;
259 ostr<<*to_write;
260 }
261 else
262 ostr<<'|'<<*to_write;
263 }
264 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Move a connection point") );
265 }
266 }
268 void SPAvoidRef::deleteConnectionPoint(ConnectionPoint &cp)
269 {
270 Inkscape::SVGOStringStream ostr;
271 IdConnectionPointMap::iterator cp_pos = connection_points.find( cp.id );
272 if ( cp_pos != connection_points.end() ) {
273 bool first = true;
274 for (IdConnectionPointMap::iterator it = connection_points.begin(); it != connection_points.end(); ++it) {
275 if ( it != cp_pos ) {
276 if ( first ) {
277 first = false;
278 ostr<<it->second;
279 } else {
280 ostr<<'|'<<it->second;
281 }
282 }
283 }
284 this->setConnectionPointsAttrUndoable( ostr.str().c_str(), _("Remove a connection point") );
285 }
286 }
288 void SPAvoidRef::handleSettingChange(void)
289 {
290 SPDesktop *desktop = inkscape_active_desktop();
291 if (desktop == NULL) {
292 return;
293 }
294 if (sp_desktop_document(desktop) != item->document) {
295 // We don't want to go any further if the active desktop's document
296 // isn't the same as the document that this item is part of. This
297 // case can happen if a new document is loaded from the file chooser
298 // or via the recent file menu. In this case, we can end up here
299 // as a rersult of a ensureUpToDate performed on a
300 // document not yet attached to the active desktop.
301 return;
302 }
304 if (new_setting == setting) {
305 // Don't need to make any changes
306 return;
307 }
308 setting = new_setting;
310 Router *router = item->document->router;
312 _transformed_connection.disconnect();
313 if (new_setting) {
314 Avoid::Polygon poly = avoid_item_poly(item);
315 if (poly.size() > 0) {
316 _transformed_connection = item->connectTransformed(
317 sigc::ptr_fun(&avoid_item_move));
319 char const *id = item->getAttribute("id");
320 g_assert(id != NULL);
322 // Get a unique ID for the item.
323 GQuark itemID = g_quark_from_string(id);
325 shapeRef = new Avoid::ShapeRef(router, poly, itemID);
327 router->addShape(shapeRef);
328 }
329 }
330 else
331 {
332 g_assert(shapeRef);
334 // Deleting the shapeRef will remove it completely from
335 // an existing Router instance.
336 delete shapeRef;
337 shapeRef = NULL;
338 }
339 }
342 GSList *SPAvoidRef::getAttachedShapes(const unsigned int type)
343 {
344 GSList *list = NULL;
346 Avoid::IntList shapes;
347 GQuark shapeId = g_quark_from_string(item->getId());
348 item->document->router->attachedShapes(shapes, shapeId, type);
350 Avoid::IntList::iterator finish = shapes.end();
351 for (Avoid::IntList::iterator i = shapes.begin(); i != finish; ++i) {
352 const gchar *connId = g_quark_to_string(*i);
353 SPObject *obj = item->document->getObjectById(connId);
354 if (obj == NULL) {
355 g_warning("getAttachedShapes: Object with id=\"%s\" is not "
356 "found. Skipping.", connId);
357 continue;
358 }
359 SPItem *shapeItem = SP_ITEM(obj);
360 list = g_slist_prepend(list, shapeItem);
361 }
362 return list;
363 }
366 GSList *SPAvoidRef::getAttachedConnectors(const unsigned int type)
367 {
368 GSList *list = NULL;
370 Avoid::IntList conns;
371 GQuark shapeId = g_quark_from_string(item->getId());
372 item->document->router->attachedConns(conns, shapeId, type);
374 Avoid::IntList::iterator finish = conns.end();
375 for (Avoid::IntList::iterator i = conns.begin(); i != finish; ++i) {
376 const gchar *connId = g_quark_to_string(*i);
377 SPObject *obj = item->document->getObjectById(connId);
378 if (obj == NULL) {
379 g_warning("getAttachedConnectors: Object with id=\"%s\" is not "
380 "found. Skipping.", connId);
381 continue;
382 }
383 SPItem *connItem = SP_ITEM(obj);
384 list = g_slist_prepend(list, connItem);
385 }
386 return list;
387 }
389 Geom::Point SPAvoidRef::getConnectionPointPos(const int type, const int id)
390 {
391 g_assert(item);
392 Geom::Point pos;
393 const Geom::Matrix& transform = item->i2doc_affine();
395 if ( type == ConnPointDefault )
396 {
397 // For now, just default to the centre of the item
398 Geom::OptRect bbox = item->getBounds(item->i2doc_affine());
399 pos = (bbox) ? bbox->midpoint() : Geom::Point(0, 0);
400 }
401 else
402 {
403 // Get coordinates from the list of connection points
404 // that are attached to the item
405 pos = connection_points[id].pos * transform;
406 }
408 return pos;
409 }
411 bool SPAvoidRef::isValidConnPointId( const int type, const int id )
412 {
413 if ( type < 0 || type > 1 )
414 return false;
415 else
416 {
417 if ( type == ConnPointDefault )
418 if ( id < 0 || id > 8 )
419 return false;
420 else
421 {
422 }
423 else
424 return connection_points.find( id ) != connection_points.end();
425 }
427 return true;
428 }
430 static std::vector<Geom::Point> approxCurveWithPoints(SPCurve *curve)
431 {
432 // The number of segments to use for not straight curves approximation
433 const unsigned NUM_SEGS = 4;
435 const Geom::PathVector& curve_pv = curve->get_pathvector();
437 // The structure to hold the output
438 std::vector<Geom::Point> poly_points;
440 // Iterate over all curves, adding the endpoints for linear curves and
441 // sampling the other curves
442 double seg_size = 1.0 / NUM_SEGS;
443 double at;
444 at = 0;
445 Geom::PathVector::const_iterator pit = curve_pv.begin();
446 while (pit != curve_pv.end())
447 {
448 Geom::Path::const_iterator cit = pit->begin();
449 while (cit != pit->end())
450 {
451 if (cit == pit->begin())
452 {
453 poly_points.push_back(cit->initialPoint());
454 }
456 if (dynamic_cast<Geom::CubicBezier const*>(&*cit))
457 {
458 at += seg_size;
459 if (at <= 1.0 )
460 poly_points.push_back(cit->pointAt(at));
461 else
462 {
463 at = 0.0;
464 ++cit;
465 }
466 }
467 else
468 {
469 poly_points.push_back(cit->finalPoint());
470 ++cit;
471 }
472 }
473 ++pit;
474 }
475 return poly_points;
476 }
478 static std::vector<Geom::Point> approxItemWithPoints(SPItem const *item, const Geom::Matrix& item_transform)
479 {
480 // The structure to hold the output
481 std::vector<Geom::Point> poly_points;
483 if (SP_IS_GROUP(item))
484 {
485 SPGroup* group = SP_GROUP(item);
486 // consider all first-order children
487 for (GSList const* i = sp_item_group_item_list(group); i != NULL; i = i->next) {
488 SPItem* child_item = SP_ITEM(i->data);
489 std::vector<Geom::Point> child_points = approxItemWithPoints(child_item, item_transform * child_item->transform);
490 poly_points.insert(poly_points.end(), child_points.begin(), child_points.end());
491 }
492 }
493 else if (SP_IS_SHAPE(item))
494 {
495 SPCurve* item_curve = SP_SHAPE(item)->getCurve();
496 // make sure it has an associated curve
497 if (item_curve)
498 {
499 // apply transformations (up to common ancestor)
500 item_curve->transform(item_transform);
501 std::vector<Geom::Point> curve_points = approxCurveWithPoints(item_curve);
502 poly_points.insert(poly_points.end(), curve_points.begin(), curve_points.end());
503 item_curve->unref();
504 }
505 }
507 return poly_points;
508 }
509 static Avoid::Polygon avoid_item_poly(SPItem const *item)
510 {
511 SPDesktop *desktop = inkscape_active_desktop();
512 g_assert(desktop != NULL);
513 double spacing = desktop->namedview->connector_spacing;
515 Geom::Matrix itd_mat = item->i2doc_affine();
516 std::vector<Geom::Point> hull_points;
517 hull_points = approxItemWithPoints(item, itd_mat);
519 // create convex hull from all sampled points
520 Geom::ConvexHull hull(hull_points);
522 // enlarge path by "desktop->namedview->connector_spacing"
523 // store expanded convex hull in Avoid::Polygn
524 Avoid::Polygon poly;
526 Geom::Line hull_edge(hull[-1], hull[0]);
527 Geom::Line prev_parallel_hull_edge;
528 prev_parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
529 prev_parallel_hull_edge.versor(hull_edge.versor());
530 int hull_size = hull.boundary.size();
531 for (int i = 0; i < hull_size; ++i)
532 {
533 hull_edge.setBy2Points(hull[i], hull[i+1]);
534 Geom::Line parallel_hull_edge;
535 parallel_hull_edge.origin(hull_edge.origin()+hull_edge.versor().ccw()*spacing);
536 parallel_hull_edge.versor(hull_edge.versor());
538 // determine the intersection point
540 Geom::OptCrossing int_pt = Geom::intersection(parallel_hull_edge, prev_parallel_hull_edge);
541 if (int_pt)
542 {
543 Avoid::Point avoid_pt((parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::X],
544 (parallel_hull_edge.origin()+parallel_hull_edge.versor()*int_pt->ta)[Geom::Y]);
545 poly.ps.push_back(avoid_pt);
546 }
547 else
548 {
549 // something went wrong...
550 std::cout<<"conn-avoid-ref.cpp: avoid_item_poly: Geom:intersection failed."<<std::endl;
551 }
552 prev_parallel_hull_edge = parallel_hull_edge;
553 }
554 return poly;
555 }
558 GSList *get_avoided_items(GSList *list, SPObject *from, SPDesktop *desktop,
559 bool initialised)
560 {
561 for (SPObject *child = from->firstChild() ; child != NULL; child = child->next ) {
562 if (SP_IS_ITEM(child) &&
563 !desktop->isLayer(SP_ITEM(child)) &&
564 !SP_ITEM(child)->isLocked() &&
565 !desktop->itemIsHidden(SP_ITEM(child)) &&
566 (!initialised || SP_ITEM(child)->avoidRef->shapeRef)
567 )
568 {
569 list = g_slist_prepend (list, SP_ITEM(child));
570 }
572 if (SP_IS_ITEM(child) && desktop->isLayer(SP_ITEM(child))) {
573 list = get_avoided_items(list, child, desktop, initialised);
574 }
575 }
577 return list;
578 }
581 void avoid_item_move(Geom::Matrix const */*mp*/, SPItem *moved_item)
582 {
583 Avoid::ShapeRef *shapeRef = moved_item->avoidRef->shapeRef;
584 g_assert(shapeRef);
586 Router *router = moved_item->document->router;
587 Avoid::Polygon poly = avoid_item_poly(moved_item);
588 if (!poly.empty()) {
589 router->moveShape(shapeRef, poly);
590 }
591 }
594 void init_avoided_shape_geometry(SPDesktop *desktop)
595 {
596 // Don't count this as changes to the document,
597 // it is basically just late initialisation.
598 SPDocument *document = sp_desktop_document(desktop);
599 bool saved = DocumentUndo::getUndoSensitive(document);
600 DocumentUndo::setUndoSensitive(document, false);
602 bool initialised = false;
603 GSList *items = get_avoided_items(NULL, desktop->currentRoot(), desktop,
604 initialised);
606 for ( GSList const *iter = items ; iter != NULL ; iter = iter->next ) {
607 SPItem *item = reinterpret_cast<SPItem *>(iter->data);
608 item->avoidRef->handleSettingChange();
609 }
611 if (items) {
612 g_slist_free(items);
613 }
614 DocumentUndo::setUndoSensitive(document, saved);
615 }
618 /*
619 Local Variables:
620 mode:c++
621 c-file-style:"stroustrup"
622 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
623 indent-tabs-mode:nil
624 fill-column:99
625 End:
626 */
627 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :