/// <summary>
        /// Return objects that are within maxDistance of the specified ray.
        /// </summary>
        /// <param name="ray">The ray.</param>
        /// <param name="maxDistance">Maximum distance from the ray to consider.</param>
        /// <param name="result">List result.</param>
        /// <returns>Objects within range.</returns>
        public void GetNearby(ref XYZ point, ref float maxDistance, List <T> result)
        {
            // Does the ray hit this node at all?
            // Note: Expanding the bounds is not exactly the same as a real distance check, but it's fast.
            // TODO: Does someone have a fast AND accurate formula to do this check?
            //bounds.ExpandToContain(new XYZ(maxDistance * 2, maxDistance * 2, maxDistance * 2));
            HLBoundingBoxXYZ d = new HLBoundingBoxXYZ(bounds.MidPoint, bounds.Size);

            d.ExpandToContain(new XYZ(bounds.Max.X + maxDistance, bounds.Max.Y + maxDistance, bounds.Max.Z + maxDistance));
            d.ExpandToContain(new XYZ(bounds.Min.X - maxDistance, bounds.Min.Y - maxDistance, bounds.Min.Z - maxDistance));

            bool intersected = d.Contains(point);

            d.Size = actualBoundsSize;
            if (!intersected)
            {
                return;
            }

            // Check against any objects in this node
            for (int i = 0; i < objects.Count; i++)
            {
                if (System.Math.Abs(objects[i].Pos.DistanceTo(point)) <= maxDistance)
                {
                    result.Add(objects[i].Obj);
                }
            }

            // Check children
            if (children != null)
            {
                for (int i = 0; i < 8; i++)
                {
                    children[i].GetNearby(ref point, ref maxDistance, result);
                }
            }
        }
        public void ParseFrom(Element elm, MEPRevitGraphWriter writer)
        {
            var space = elm as Autodesk.Revit.DB.Mechanical.Space;

            if (space == null)
            {
                return;
            }

            if (space.Volume < 0.5)
            {
                return;
            }

            var scannedElements = writer.Cache.ParsedElements;
            var cpTree          = writer.Cache.connectorsCache;
            var geoTree         = writer.Cache.geoCache;
            var maxDepth        = writer.Cache.MaxDepth;
            var graph           = writer.Graph;

            var lvl = space.Level;

            if (lvl != null)
            {
                graph.AddConnection(elm, lvl, MEPPathConnectionType.Proximity, Model.MEPEdgeTypes.IS_ON);
            }

            //get elements in the space


            //get areas with edges and add to walls

            var sbopt = new SpatialElementBoundaryOptions();

            sbopt.SpatialElementBoundaryLocation = SpatialElementBoundaryLocation.Finish;
            sbopt.StoreFreeBoundaryFaces         = true;

            //var geoOpt = new Options();
            //var spgeo = space.get_Geometry(geoOpt);

            //get nearby spaces by extending bb of space
            //foreach each face of space geometry
            //split into uv gripd
            //construct line from each point
            //get intersect with faces on nearby faces
            //if it hits increment area count for direction

            var doc = space.Document;
            SpatialElementGeometryCalculator sg = new SpatialElementGeometryCalculator(doc, sbopt);
            var spgets = sg.CalculateSpatialElementGeometry(space);

            var spgeo = spgets.GetGeometry();
            //var spbb = spgeo.GetBoundingBox();
            var spbb = elm.get_BoundingBox(null);
            //var spbbhl = new HLBoundingBoxXYZ(spbb);
            //spbbhl.Size = spbbhl.Size + new XYZ(2, 2, 4);


            //get faces
            var uvdensity   = 0.75;
            var maxDistance = 4D;
            //var nearbyFaces = nearbySpaces.OfType<SolidGeometrySegment>().Where(spc => spc.OriginatingElement != space.Id).SelectMany(sp => sp.Geometry.Faces.OfType<Face>());

            var rayIncidents = new HashSet <FaceIntersectRay>();


            //get all the faces in the geometry
            var spfaces  = spgeo.Faces.OfType <Face>().ToList();
            var spCenter = spgeo.ComputeCentroid();

            foreach (var gface in spfaces)
            {
                //todo: get edges from spaces and store these in the graph too

                //extract the faces which bound with other elements (Walls, floors, ceilings, windows etc)
                var sfaceInfos = spgets.GetBoundaryFaceInfo(gface);
                foreach (var sfaceInfo in sfaceInfos)
                {
                    //get the geo face and element of this bounding face
                    var sface = sfaceInfo.GetSubface();

                    var elmId = sfaceInfo.SpatialBoundaryElement;

                    var lelm     = GetElementFromLinkedElement(elmId, doc);
                    var docIdent = string.Empty;
                    //if (lelm == null) continue; //ignore this face if it doesn't resolve to a valid element

                    //find the bounding uv box so we can work out a grid of points
                    var fbb  = sface.GetBoundingBox();
                    var uExt = uvdensity; // (fbb.Max.U - fbb.Min.U) / uvdensity;
                    var vExt = uvdensity; // (fbb.Max.V - fbb.Min.V) / uvdensity;

                    var u = fbb.Min.U;
                    var v = fbb.Min.V;

                    //var sb = new GeoLib.C2DPolygon();

                    //construct grid for ray tracing
                    Stack <UV> gridPoints = new Stack <UV>();
                    while (u <= fbb.Max.U)
                    {
                        v = fbb.Min.V;
                        while (v <= fbb.Max.V)
                        {
                            var uvp = new UV(u, v);
                            v += uvdensity;
                            if (!sface.IsInside(uvp))
                            {
                                continue;                       //only include points that are actually on this face
                            }
                            gridPoints.Push(uvp);
                        }
                        u += uvdensity;
                    }

                    var nerbyCheckCats         = new int[] { };
                    IList <ElementId> hostElms = new List <ElementId>();

                    if (lelm != null && !(lelm is HostObject))
                    {
                        var n = lelm;
                    }

                    if (lelm != null)
                    {
                        docIdent = DocUtils.GetDocumentIdent(lelm.Document);
                        //get cutting elements if it's a wall so we can find door and windows
                        if (lelm is HostObject)
                        {
                            var whost = lelm as HostObject;
                            hostElms = whost.FindInserts(true, true, true, true);
                            //build oct tree of hostelems so we can quickly ray trace them later
                            foreach (var hostElm in hostElms)
                            {
                                //ignoring any link and transform for now
                                var ehl = whost.Document.GetElement(hostElm);
                                writer.Cache.geoCacheWriter.AddElement(ehl, true);
                            }

                            var lwall = lelm as Wall;
                            if (lwall != null && lwall.CurtainGrid != null)
                            {
                                foreach (var cwpanl in lwall.CurtainGrid.GetPanelIds())
                                {
                                    var ehl = lwall.Document.GetElement(cwpanl);
                                    //assume panels always from part of the host wall, so we'll exclude them
                                    if (ehl is Panel)
                                    {
                                        continue;
                                    }
                                    //but we want to include doors:
                                    //hostElms.Add(cwpanl);
                                    //writer.Cache.geoCacheWriter.AddElement(ehl, true);
                                }
                            }
                        }

                        //we need the nearby check to find the cut out elements
                        nerbyCheckCats = new int[] { (int)BuiltInCategory.OST_Doors, (int)BuiltInCategory.OST_Windows };
                    }
                    else
                    {
                        nerbyCheckCats = new int[] { (int)BuiltInCategory.OST_Doors, (int)BuiltInCategory.OST_Windows, (int)BuiltInCategory.OST_Floors, (int)BuiltInCategory.OST_Walls, (int)BuiltInCategory.OST_Roofs };

                        //we need the nearby check to find the bounding element
                        switch (sfaceInfo.SubfaceType)
                        {
                        case SubfaceType.Bottom:
                            nerbyCheckCats = new int[] { (int)BuiltInCategory.OST_Floors };
                            break;

                        case SubfaceType.Top:
                            nerbyCheckCats = new int[] { (int)BuiltInCategory.OST_Roofs, (int)BuiltInCategory.OST_Floors, (int)BuiltInCategory.OST_Windows };
                            break;

                        case SubfaceType.Side:
                            nerbyCheckCats = new int[] { (int)BuiltInCategory.OST_Doors, (int)BuiltInCategory.OST_Windows, (int)BuiltInCategory.OST_Walls };
                            break;
                        }
                    }

                    //option 1 - brute force ray trace
                    //option 2 - construct 2d polygon from edges of each face, translate each face into the same plane, then boolean intersect, get area of each intersect
                    //calc space boundaries at midpoint?
                    Face optLastIntermeidateFace = null;
                    SolidGeometrySegment optLastIntermeidateSegment = null;
                    Face optLastHitFace = null;
                    SolidGeometrySegment optLastHitSegment = null;
                    var arWeight = sface.Area / gridPoints.Count;
                    while (gridPoints.Count > 0)
                    {
                        var pt          = gridPoints.Pop();
                        var rayIncident = new FaceIntersectRay();

                        var nv = sface.ComputeNormal(pt).Normalize();
                        //var mx = sface.ComputeSecondDerivatives(pt).MixedDerivative.Normalize();
                        rayIncident.SourceElement = space;
                        rayIncident.SourceFace    = sface;
                        rayIncident.SourceUV      = pt;

                        rayIncident.RayVecotor          = nv;
                        rayIncident.IntermediatDocIdent = docIdent;
                        rayIncident.IntermeidateElement = lelm != null ? lelm.Id : ElementId.InvalidElementId;
                        rayIncident.AreaWeight          = arWeight;
                        rayIncident.SubFaceType         = sfaceInfo.SubfaceType;
                        rayIncidents.Add(rayIncident);

                        var sp = sface.Evaluate(pt);
                        rayIncident.SourceXYZ = sp;
                        //var ray = Line.CreateBound(sp, sp + nv * 4);
                        var rayBB = new HLBoundingBoxXYZ(sp, (sp + nv * 4), true);
                        //rayBB.Size = rayBB.Size + new XYZ(0.5, 0.5, 0.5);
                        //Plane geoPlane = Plane.c(sp, sp + nv * 5, sp + nv * 5 + (mx * 0.2));
                        //SketchPlane skPlane = SketchPlane.Create(doc, geoPlane);
                        //doc.Create.NewModelCurve(ray, skPlane);

                        //check cache for hit on otherside, if there is one nearby on this face we can ignore it as we're not including both sides
                        //var nearbyrayHits = writer.Cache.rayhitCache.GetNearby(ray, 0.4F);
                        //var validSimilarHit = nearbyrayHits.FirstOrDefault(rh => rh.HittingSegment != null && rh.HittingSegment.OriginatingElement == space.Id && rh.IntermeidateElement == rayIncident.IntermeidateElement);
                        //if (validSimilarHit != null && sface.IsInside(validSimilarHit.HittingUV))
                        //{
                        //    rayIncident.Ignore = true;
                        //    log.Info("Got hit on other side, ignoring");
                        //    continue;
                        // }

                        if (optLastIntermeidateFace != null)
                        {
                            IntersectionResultArray issRes = null;
                            var issGeoHit = getIntersect(pt, optLastIntermeidateFace, sface, maxDistance, out issRes, out double distance, nv, doc);
                            if (issGeoHit)
                            {
                                rayIncident.IntermediatDocIdent = optLastIntermeidateSegment.OriginatingDocIdent;
                                rayIncident.IntermeidateElement = optLastIntermeidateSegment.OriginatingElement;
                            }
                            else
                            {
                                optLastIntermeidateFace    = null;
                                optLastIntermeidateSegment = null;
                            }
                        }

                        GeometrySegment[] nearbyElements = null;

                        if (optLastIntermeidateFace == null)
                        {
                            nearbyElements = geoTree.GetColliding(rayBB);

                            var nearbyCutoutElements = nearbyElements.Where(iel =>
                                                                            (hostElms.Count == 0 || hostElms.Contains(iel.OriginatingElement)) &&
                                                                            nerbyCheckCats.Contains(iel.OriginatingElementCategory.IntegerValue))
                                                       .OfType <SolidGeometrySegment>();


                            IntersectionResultArray isRes = null;
                            bool isGeoHit = false;
                            foreach (var extSegment in nearbyCutoutElements)
                            {
                                foreach (var extFace in extSegment.Faces)
                                {
                                    isGeoHit = getIntersect(pt, extFace, sface, maxDistance, out isRes, out double distance, nv, doc);
                                    if (isGeoHit)
                                    {
                                        rayIncident.IntermediatDocIdent = extSegment.OriginatingDocIdent;
                                        rayIncident.IntermeidateElement = extSegment.OriginatingElement;
                                        optLastIntermeidateFace         = extFace;
                                        optLastIntermeidateSegment      = extSegment;
                                        break;
                                    }
                                }
                                if (isGeoHit)
                                {
                                    break;
                                }
                            }
                        }



                        if (optLastHitFace != null)
                        {
                            var isHit = getIntersect(pt, optLastHitFace, sface, maxDistance, out var isRe, out double distance, nv, doc);
                            var isRes = isRe;
                            //project point onto other face instead?
                            //var srcXYZ = sface.Evaluate(pt);
                            //var otXYZRes = optLastHitFace.Project(srcXYZ);

                            if (isHit)
                            {
                                var itx = isRes.OfType <IntersectionResult>().FirstOrDefault();
                                rayIncident.HittingFace    = optLastHitFace;
                                rayIncident.HittingSegment = optLastHitSegment;
                                rayIncident.HittingUV      = itx.UVPoint;
                                rayIncident.HittingXYZ     = itx.XYZPoint;
                                rayIncident.Distance       = distance;
                                nv = sface.ComputeNormal(pt).Normalize();
                                continue; //shortcut if we find a hit on the same face again
                            }
                            else
                            {
                                optLastHitFace    = null;
                                optLastHitSegment = null;
                            }
                        }


                        if (nearbyElements == null)
                        {
                            nearbyElements = geoTree.GetColliding(rayBB, (ob) => { return(ob.OriginatingElementCategory.IntegerValue == (int)BuiltInCategory.OST_MEPSpaces); });
                        }

                        //BoundingBoxIntersectsFilter bif = new BoundingBoxIntersectsFilter(new Outline(sp - new XYZ(0.1, 0.1, 0.1), (sp + nv * 2) + new XYZ(0.1, 0.1, 0.1)));
                        //var sfv = new FilteredElementCollector(space.Document);
                        //var sepl = sfv.WherePasses(bif).ToElements();

                        var nearbySpaces = nearbyElements.Where(ne => ne.OriginatingElementCategory.IntegerValue == (int)BuiltInCategory.OST_MEPSpaces).OfType <SolidGeometrySegment>().Distinct().ToList();

                        //find the extents of this face which face faces on other nearby faces (whaaat?)
                        //check each face of each nearby space for intersect with ray
                        foreach (var nearSpace in nearbySpaces)
                        {
                            var isHit = false;
                            foreach (var otFace in nearSpace.Faces)
                            {
                                isHit = getIntersect(pt, otFace, sface, maxDistance, out var isRe, out double distance, nv, doc);
                                var isRes = isRe;
                                //project point onto other face instead?
                                //var srcXYZ = sface.Evaluate(pt);
                                //var otXYZRes = otFace.Project(srcXYZ);

                                if (isHit)
                                {
                                    if (nearSpace.OriginatingElement.IntegerValue != elm.Id.IntegerValue)
                                    {
                                        var itx = isRes.OfType <IntersectionResult>().FirstOrDefault();
                                        rayIncident.HittingFace    = otFace;
                                        rayIncident.HittingSegment = nearSpace;
                                        rayIncident.HittingUV      = itx.UVPoint;
                                        rayIncident.HittingXYZ     = itx.XYZPoint;
                                        rayIncident.Distance       = distance;

                                        nv = sface.ComputeNormal(pt).Normalize();

                                        //optimization: check this face again first for the next ray check, since it's likely to be another hit
                                        optLastHitFace    = otFace;
                                        optLastHitSegment = nearSpace;

                                        rayIncident.Ignore = false;
                                    }
                                    else
                                    {
                                        if (distance < 0.1)
                                        {
                                            isHit = false;                 //looks like we hit our own face, ouch!
                                        }
                                        rayIncident.Ignore = true;
                                    }

                                    break;
                                }
                            }
                            if (isHit)
                            {
                                break;
                            }
                        }
                    }
                }

                /*
                 * space
                 * face
                 *    intermediate element (Wall/window/door)
                 *        face (space)
                 * face
                 *
                 *
                 * add connection
                 * this space -> section -> other space
                 * wall ------------^
                 */
            }

            //var ec = new elmComparer();
            var srcNode = graph.AddElement(space);

            srcNode.AsAbstractNode.ExtendedProperties.Add("Center", spCenter.ToBuildingGraph());
            srcNode.AsElementNode.ExtendedProperties.Add("Center", spCenter.ToBuildingGraph());
            double           minIncluedArea = 4;
            VectorBucketiser vbw            = new VectorBucketiser(8, 5);

            var includeRays = rayIncidents.Where(r => !r.Ignore);

            var outsideNode = graph.Nodes.FirstOrDefault(n => n.Name == "Outside");
            var groundNode  = graph.Nodes.FirstOrDefault(n => n.Name == "Ground");

            //group by the intermediate element (wall/floor/etc)
            foreach (var docGroup in includeRays.GroupBy(ri => ri.IntermediatDocIdent))//, ec))
            {
                var sdoc = DocUtils.GetDocument(docGroup.Key, elm.Document.Application);

                foreach (var intermediateElemGroup in docGroup.GroupBy(ri => ri.IntermeidateElement.IntegerValue))//, ec))
                {
                    var     selmid = new ElementId(intermediateElemGroup.Key);
                    Element selm   = sdoc != null?sdoc.GetElement(selmid) : null;

                    //group similar vectors into buckets
                    foreach (var rayVectorBuckets in intermediateElemGroup.GroupBy(ri => vbw.GetBucket(ri.RayVecotor)))
                    {
                        var rg = rayVectorBuckets.ToList();

                        var gs = rg.GroupBy(vr => vr.HittingSegment == null ? null : vr.HittingSegment.OriginatingElement).ToList();
                        //group each vector and intermediate element by the element it hits
                        foreach (var orgElmGroup in gs)
                        {
                            //find a section already matching this section
                            //actually easier to treat each path as separate sections
                            //var edNodes = graph.GetEdges(intermediateElemGroup.Key).Where(ed => ed.NextNode.AsAbstractNode.Name == "Surface"
                            //&& ed.NextNode.Connections.Any(cn => cn.NextNode.OriginId == spNode.OriginId)).Where(ed => ed.;

                            var apporxIntersect = orgElmGroup.Sum(et => et.AreaWeight);
                            var vector          = orgElmGroup.First().RayVecotor;
                            if (apporxIntersect < minIncluedArea)
                            {
                                continue;
                            }

                            var direction = VectorBucketiser.GetZeroClamppedPoint(orgElmGroup.First().RayVecotor); //should be rayVectorGroup.Key.AverageVector, but not yet implemented;

                            MEPRevitNode spNode = null;
                            if (orgElmGroup.Key != null)
                            {
                                var otherSpace = doc.GetElement(orgElmGroup.Key);
                                spNode = graph.AddElement(otherSpace);
                            }
                            else
                            {
                                var isExternal = false;
                                if (selm is Wall)
                                {
                                    var wallType      = selm.Document.GetElement(selm.GetTypeId());
                                    var functionParam = wallType.get_Parameter(BuiltInParameter.FUNCTION_PARAM);
                                    isExternal = functionParam != null?functionParam.AsInteger() == 1 : false;
                                }

                                if (isExternal || selm != null && (selm.Name.ToLower().Contains("exterior") || selm is Autodesk.Revit.DB.Opening || selm.Name.ToLower().Contains("window") || selm is Autodesk.Revit.DB.RoofBase))
                                {
                                    if (outsideNode == null)
                                    {
                                        outsideNode = new MEPRevitNode("Outside", "External", "Outside", new Model.Environment());
                                    }
                                    spNode = outsideNode;
                                }
                                else if (selm != null && (selm.Name.ToLower().Contains("floor") || selm is Autodesk.Revit.DB.Floor))
                                {
                                    if (groundNode == null)
                                    {
                                        groundNode = new MEPRevitNode("Ground", "External", "Ground", new Model.Environment());
                                    }
                                    spNode = groundNode;
                                }
                                else
                                {
                                    spNode = new MEPRevitNode("Void", "External", "Void", new Model.VoidVolume());
                                    continue; //ignore void boundaries for now
                                }
                            }


                            var sectionN = graph.NewSection(selm, Model.MEPEdgeTypes.IS_ON);
                            sectionN.AsAbstractNode.Labels.Add("Surface");
                            sectionN.AsElementNode.Labels.Add("Surface");


                            if (selm == null)
                            {
                                var emptyBondary = new MEPRevitNode("OpenBoundary");
                                emptyBondary.AsAbstractNode.Name = "OpenBoundary";
                                var cl = graph.AddConnection(emptyBondary, sectionN, MEPPathConnectionType.SectionOf, Model.MEPEdgeTypes.IS_ON);
                                cl.AsNodeEdge.ExtendedProperties.Add("rvid", intermediateElemGroup.Key);
                            }


                            var edgesf = graph.AddConnection(srcNode, sectionN, MEPPathConnectionType.Analytical, Model.MEPEdgeTypes.BOUNDED_BY);

                            //total up intersecting area
                            var sampleIntersect = orgElmGroup.First();

                            edgesf.SetWeight("Area", apporxIntersect);
                            edgesf.SetWeight("Faceing", new Model.Types.Point3D(direction.X, direction.Y, direction.Z));
                            edgesf.SetWeight("SubFaceType", sampleIntersect.SubFaceType.ToString());

                            sectionN.AsAbstractNode.ExtendedProperties.Add("Area", apporxIntersect);
                            sectionN.AsAbstractNode.ExtendedProperties.Add("SubFaceType", sampleIntersect.SubFaceType.ToString());


                            HLBoundingBoxXYZ bb = new HLBoundingBoxXYZ();
                            foreach (var et in orgElmGroup)
                            {
                                if (et.HittingXYZ != null)
                                {
                                    bb.ExpandToContain(et.HittingXYZ);
                                }
                            }

                            if (!bb.IsInvalid)
                            {
                                sectionN.BoundingBox = bb;
                                var avgCenterPoint = bb.MidPoint;
                                var size           = bb.Size;
                                sectionN.AsAbstractNode.ExtendedProperties.Add("Center", avgCenterPoint.ToBuildingGraph());
                                sectionN.AsAbstractNode.ExtendedProperties.Add("Size", size.ToBuildingGraph());
                            }



                            var edgest     = graph.AddConnection(sectionN, spNode, MEPPathConnectionType.Analytical, Model.MEPEdgeTypes.BOUNDED_BY);
                            var directionn = direction.Negate();
                            edgest.SetWeight("Area", apporxIntersect);
                            edgest.SetWeight("Faceing", directionn.ToBuildingGraph());
                            edgest.SetWeight("SubFaceType", (int)sampleIntersect.SubFaceType);
                        }
                    }
                }
            }
        }