[![Correct location when moved from base point location to survey point][2]][2]
I'm in the process of writing a program to show navisworks clash points in Revit, however, i'm finding it difficult to place them at the exact location when the Base point is not (0,0,0). When i manually add the difference in location to the code, it works. How do I solve this programmatically? I understand there is probably a simple calculation to solve but i can't seem to figure it out. I go some ideas from googling around to be no avail. Any ideas how to go about this?
public static XYZ WorldToLocal(Document document, XYZ coordinate, bool millimeters)
{
ElementCategoryFilter filter = new ElementCategoryFilter(BuiltInCategory.OST_ProjectBasePoint);
FilteredElementCollector collector = new FilteredElementCollector(document);
IList<Element> oProjectBasePoints = collector.WherePasses(filter).ToElements();
Element oProjectBasePoint = null;
foreach (Element bp in oProjectBasePoints)
{
oProjectBasePoint = bp;
break;
}
double x = oProjectBasePoint.get_Parameter(BuiltInParameter.BASEPOINT_EASTWEST_PARAM).AsDouble();
double y = oProjectBasePoint.get_Parameter(BuiltInParameter.BASEPOINT_NORTHSOUTH_PARAM).AsDouble();
double z = oProjectBasePoint.get_Parameter(BuiltInParameter.BASEPOINT_ELEVATION_PARAM).AsDouble();
double r = oProjectBasePoint.get_Parameter(BuiltInParameter.BASEPOINT_ANGLETON_PARAM).AsDouble();
XYZ result = new XYZ(
coordinate.X * Math.Cos(r) - coordinate.Y * Math.Sin(r) ,
coordinate.X * Math.Sin(r) + coordinate.Y * Math.Cos(r),
coordinate.Z);
//Code that makes it work
XYZ newpostion = new XYZ(result.X - 21.943, result.Y +13.410, result.Z);
//ends here
if (millimeters)
{
return newpostion * 304.8;
}
return newpostion;
}
This returns the location of the survey point to the base point. Solving my question.
IEnumerable<BasePoint> points = new FilteredElementCollector(document)
.OfClass(typeof(BasePoint))
.Cast<BasePoint>();
XYZ surveypointXYZ = new XYZ();
foreach (BasePoint bp in points)
{
if (bp.IsShared)
{
BoundingBoxXYZ bb = bp.get_BoundingBox(null);
surveypointXYZ = bb.Min;
}
}
I'm going to draw a chart using c# in Windows Form Application. I need to draw a circle on line chart and show this data point value on a label which is nearest data point from the x-axis of the mouse pointer when mouse is moved over the chart area.
I write a code as follows......
private void Chart1_MouseMove(object sender, MouseEventArgs e)
{
HitTestResult result = Chart1.HitTest(e.X, e.Y);
DataPoint nearestPoint = null;
if (prevPosition!=null)
{
Chart1.Series[0].Points[prevPosition.PointIndex].MarkerStyle = MarkerStyle.None;
}
if (result.ChartElementType == ChartElementType.DataPoint)
{
string xValue = DateTime.FromOADate(Chart1.Series[0].Points[result.PointIndex].XValue).ToString("yyyy/MM/dd");
string yValue = Convert.ToString(Chart1.Series[0].Points[result.PointIndex].YValues[0]);
Chart1.Series[0].Points[result.PointIndex].MarkerStyle = MarkerStyle.Circle;
Chart1.Series[0].Points[result.PointIndex].MarkerSize = 7;
Chart1.Series[0].Points[result.PointIndex].MarkerColor = Color.Green;
label1.Text = "Date:" + xValue;
label2.Text = "Price:" + yValue;
prevPosition = result;
}
But this code shows the value and corresponding circle over line when mouse is moved near to the depicted line. When mouse is far away from line but within chart area it does not show the circle and value. I need to draw the circle over the line point nearest to the X-axis of the mouse pointer and show this data on a label
You can find the closest point measuring only the x-values or the y-values or measuring the absolute distances. Or you could simple output the values under the mouse cursor, no matter the points. For this last one see here!
For each of the 1st three options this should help:
Class level variables used to set and reset colors..:
DataPoint dpXaxis = null;
DataPoint dpYaxis = null;
DataPoint dpAbs = null;
And a list of points for keeping the pixel locations of all points:
List<Point> pixPoints = null;
The MouseMove event:
private void chart_MouseMove(object sender, MouseEventArgs e)
{
ChartArea ca = chart.ChartAreas[0];
Axis ax = ca.AxisX;
Axis ay = ca.AxisY;
Series s = chart.Series[0];
if (!s.Points.Any()) return; // no data, no action!
// option 1:
// the values at the mouse pointer:
double valx = ax.PixelPositionToValue(e.X);
double valy = ay.PixelPositionToValue(e.Y);
// the deltas on the x-axis (with index):
var ix = s.Points.Select((x, i) => new {delta = Math.Abs(x.XValue - valx), i})
.OrderBy(x => x.delta).First().i;
var dpx = s.Points[ix];
// option 2:
// the deltas on the y-axis (with index):
var iy = s.Points.Select((x, i) =>
new {delta = Math.Abs(x.YValues[0] - valy), i })
.OrderBy(x => x.delta).First().i;
var dpy = s.Points[iy];
// option 3:
// the absolute distances (with index):
var ind = pixPoints.Select((x, i) =>
new { delta = Math.Abs(x.X - e.X) + Math.Abs(x.Y - e.Y), i}).
OrderBy(x => x.delta).First().i;
// find index of smallest delta
var dpca = s.Points[ind];
// set/reset colors
if (dpXaxis != null) dpXaxis.Color = s.Color;
dpXaxis = dpx;
dpXaxis.Color = Color.LawnGreen;
// set/reset colors
if (dpYaxis != null) dpYaxis.Color = s.Color;
dpYaxis = dpy;
dpYaxis.Color = Color.Cyan;
if (dpAbs != null) dpAbs.Color = s.Color;
dpAbs = dpca;
dpAbs.Color = Color.Red;
}
To find the point closest in both directions you will need to either include the scales of the axes or, probably easier, create a List<PointF> from the DataPoints that hold the locations of the points in pixels. For this use the reverse axis functions. Then I calculate the deltas in a similar fashion as the Linq above.
The list gets filled/updated like so:
List<Point> getPixPoints(Series s, ChartArea ca)
{
List<Point> points = new List<Point>();
foreach (DataPoint dp in s.Points)
{
points.Add(new Point(
(int)ca.AxisX.ValueToPixelPosition(dp.XValue),
(int)ca.AxisY.ValueToPixelPosition(dp.YValues[0]) ));
}
return points;
}
Let's see it at work:
I have grid dimension 2000x2000. I have some points stored on grid and I want to connect some of them, but connection need to be closest path from one to another.
I have :
private static readonly Dictionary<double,Point> ExistingPoints = new Dictionary<double, Point>();
private static Point[,] gridMatrix = new Point[200, 200];
where ExistingPoints is dictionary of points that are placed on grid and gridMatrix contains same points from dictionary, but his row and column are x and y from point divided by 10 so it can be represented on grid.
So how can I find closest path from one point to another?
EDIT
Path can only go on grid, meaning it can go on lines only, it can't be straight line, so when I need to go up/ down/ left/ right it need to be on 90 degrees
Okey as we said in the comments I would do this:
List<Point> path = new List<Point>();
Point startingPoint = new Point(1000, 1000);
Point targetPoint = new Point(500, 500);
Point currentPos = startingPoint;
//Level in Y
while (currentPos.Y != targetPoint.Y)
{
path.Add(new Point(currentPos.X, currentPos.Y);
currentPos.Y += (currentPos.Y > targetPoint.Y ? -1 : 1);
}
//Level in X
while(currentPos.X != targetPoint.X)
{
path.Add(new Point(currentPos.X, currentPos.Y);
currentPos.X += (currentPos.X > targetPoint.X ? -1 : 1);
}
//Draw the lines in steps
for (int i = 0; i < path.Count()-1; i++)
{
DrawLineBetween(path[i], path[i + 1]);
}
DrawLineBetween(path.Last(), targetPoint);
I did not test the code but it compiles and should work in theory. The Method DrawLineBetweenstill has to be implemented of course but I guess you have something in place here.
You could, of course, combine the X and Y leveling in one loop and then go in a "stair" pattern.
I need to build a graphic train schedule visualisation tool in C#. Actually I have to rebuild this perfect tool in C#.
Marey's Trains
The graphs have to be zoomable, scrollable and printable/exportable to PDF with vector graphical elements.
Could you give me some tips? How should I start it? What sort of libraries should I use?
Is it worth to try using graphing libraries like OxyPlot? Maybe it's not the best because of the special axes and irregular grids - as I think. What's your opinion?
No matter which chart tool you use, once you need special types of display you will always have to add some extra coding.
Here is an example of using the MSChart control.
Do note that it has a limitation wrt to exporting vector formats:
It can export to various formats, including 3 EMF types; however only some application can actually use those. Not sure about the PDF libary you use..!
If you can't use the Emf formats you can get nice results by making the Chart control really big, exporting to Png and then making the Dpi resolution much larger the the default screen resolution it has after saving.. Setting it to 600 or 1200dpi should do for most pdf uses..
Now lets look at an example:
A few notes:
I have made my life easier in a number of ways. I have only coded for one direction and I have not reversed the rooster, so it goes only bottom to top.
I have not used real data but made them up.
I have not created one or more classes to hold the station data; instead I use a very simple Tuple.
I have not created a DataTable to hold the train data. Instead I make them up and add them to the chart on the fly..
I didn't test, but zooming and scrolling should work as well..
Here is the List<Tuple> that holds my station data:
// station name, distance, type: 0=terminal, 1=normal, 2=main station
List<Tuple<string, double, int>> TrainStops = null;
Here is how I set up the chart:
Setup24HoursAxis(chart1, DateTime.Today);
TrainStops = SetupTrainStops(17);
SetupTrainStopAxis(chart1);
for (int i = 0; i < 23 * 3; i++)
{
AddTrainStopSeries(chart1, DateTime.Today.Date.AddMinutes(i * 20),
17 - rnd.Next(4), i% 5 == 0 ? 1 : 0);
}
// this exports the image above:
chart1.SaveImage("D:\\trains.png", ChartImageFormat.Png);
This creates one train every 20 minutes with 14-17 stops and every 5th train a fast one.
Here are the routines I call:
Setting up the x-axis for hold one day's worth of data is straightforward.
public static void Setup24HoursAxis(Chart chart, DateTime dt)
{
chart.Legends[0].Enabled = false;
Axis ax = chart.ChartAreas[0].AxisX;
ax.IntervalType = DateTimeIntervalType.Hours;
ax.Interval = 1;
ax.Minimum = dt.ToOADate();
ax.Maximum = (dt.AddHours(24)).ToOADate();
ax.LabelStyle.Format = "H:mm";
}
Creating a List of stations with random distances is also very simple. I made the 1st and last ones terminals and every 5th a main station.
public List<Tuple<string, double, int>> SetupTrainStops(int count)
{
var stops = new List<Tuple<string, double, int>>();
Random rnd = new Random(count);
for (int i = 0; i < count; i++)
{
string n = (char)(i+(byte)'A') + "-Street";
double d = 1 + rnd.Next(3) + rnd.Next(4) + rnd.Next(5) / 10d;
if (d < 3) d = 3; // a minimum distance so the label won't touch
int t = (i == 0 | i == count-1) ? 0 : rnd.Next(5)==0 ? 2 : 1;
var ts = new Tuple<string, double, int>(n, d, t);
stops.Add(ts);
}
return stops;
}
Now that we have the train stops we can set up the y-axis:
public void SetupTrainStopAxis(Chart chart)
{
Axis ay = chart.ChartAreas[0].AxisY;
ay.LabelStyle.Font = new Font("Consolas", 8f);
double totalDist = 0;
for (int i = 0; i < TrainStops.Count; i++)
{
CustomLabel cl = new CustomLabel();
cl.Text = TrainStops[i].Item1;
cl.FromPosition = totalDist - 0.1d;
cl.ToPosition = totalDist + 0.1d;
totalDist += TrainStops[i].Item2;
cl.ForeColor = TrainStops[i].Item3 == 1 ? Color.DimGray : Color.Black;
ay.CustomLabels.Add(cl);
}
ay.Minimum = 0;
ay.Maximum = totalDist;
ay.MajorGrid.Enabled = false;
ay.MajorTickMark.Enabled = false;
}
A few notes are called for here:
As the values are quite dynamic we can't use normal Labels which would come with the fixed Interval spacing.
So we create CustomLabels instead.
For these we need two values to determine the space into which they shall be centered. So we create a small span by adding/subtracting 0.1d.
We have calculated the total distance and use it to set up the Maximum of the y-axis. Again: To mimick the schedule you show you will have to do some reversing here and there..
By adding CustomLabels the normal ones are turned off automatically. As we need MajorGridlines at the irregular intervals we also turn the normal ones off. Hence we must draw them ourselves. Not really hard as you can see..:
For this we code one of the xxxPaint events:
private void chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
Axis ay = chart1.ChartAreas[0].AxisY;
Axis ax = chart1.ChartAreas[0].AxisX;
int x0 = (int) ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int) ax.ValueToPixelPosition(ax.Maximum);
double totalDist = 0;
foreach (var ts in TrainStops)
{
int y = (int)ay.ValueToPixelPosition(totalDist);
totalDist += ts.Item2;
using (Pen p = new Pen(ts.Item3 == 1 ? Color.DarkGray : Color.Black,
ts.Item3 == 1 ? 0.5f : 1f))
e.ChartGraphics.Graphics.DrawLine(p, x0 + 1, y, x1, y);
}
// ** Insert marker drawing code (from update below) here !
}
Note the use of the ValueToPixelPosition conversion functions of the axes!
Now for the last part: How to add a Series of train data..:
public void AddTrainStopSeries(Chart chart, DateTime start, int count, int speed)
{
Series s = chart.Series.Add(start.ToShortTimeString());
s.ChartType = SeriesChartType.Line;
s.Color = speed == 0 ? Color.Black : Color.Brown;
s.MarkerStyle = MarkerStyle.Circle;
s.MarkerSize = 4;
double totalDist = 0;
DateTime ct = start;
for (int i = 0; i < count; i++)
{
var ts = TrainStops[i];
ct = ct.AddMinutes(ts.Item2 * (speed == 0 ? 1 : 1.1d));
DataPoint dp = new DataPoint( ct.ToOADate(), totalDist );
totalDist += TrainStops[i].Item2;
s.Points.Add(dp);
}
}
Note that since my data don't contain real arrival/departure times I calculated them from the distance and some speed factor. You, of course, would use your data!
Also note that I have used a Line chart with extra Marker circles.
Also note that each train series can easily be disabled/hidden or brought back again.
Let's show only the fast trains:
private void cbx_ShowOnlyFastTrains_CheckedChanged(object sender, EventArgs e)
{
foreach (Series s in chart1.Series)
s.Enabled = !cbx_ShowOnlyFastTrains.Checked || s.Color == Color.Brown;
}
Of course for a robust application you will not rely ona magic color ;-)
Instead you could add a Tag object to the Series to hold all sorts of train info.
Update: As you noticed the drawn GridLines cover the Markers. You can insert this piece of code here (**); it will owner-draw the Markers at the end of the xxxPaint event.
int w = chart1.Series[0].MarkerSize;
foreach(Series s in chart1.Series)
foreach(DataPoint dp in s.Points)
{
int x = (int) ax.ValueToPixelPosition(dp.XValue) - w / 2;
int y = (int) ay.ValueToPixelPosition(dp.YValues[0])- w / 2;
using (SolidBrush b = new SolidBrush(dp.Color))
e.ChartGraphics.Graphics.FillEllipse(b, x, y, w, w);
}
Close-up:
Intro
I noticed an issue while implementing clipping (see this).
It looks like UIElement.Clip still render invisible parts
Rendering relatively small geometry (lines to only fill 1920x1200 area ~ 2000 vertical lines) take a lot of time. When using Clip and moving that geometry offscreen (so that clipping should remove significant part of it) it is still take same time (around 1 sec).
Ok, I found what using Geometry.Combine will do a clip (render time is reduced proportionally to removed after clipping geometry). Perfect!
Problem
Geometry.Combine doesn't work with non-closed geometry properly. It produce closed geometry. And it looks ugly, connecting first and last point:
Question
How can I perform clipping (reducing amount of geometry to be rendered) for non-closed figures?
Edit
Here is geometry before (small peace of shown on picture)
{M0;50L0;50L1;53,1395259764657L2;56,2666616782152L3;59,3690657292862L4;62,4344943582427L5;65,4508497187474L6;68,4062276342339L7;71,2889645782536L8; ...
and after
{F1M54,9999923706055;34,5491371154785L53,9999885559082;37,5655174255371 53,0000114440918;40,6309471130371 52,0000076293945;43,7333335876465 ...
Notice change at beginning, was M 0;50 L ..., become F 1 M 55;34 L ...
F1 means NonZero filling
Rule that determines whether a point is in the fill region of the path by drawing a ray from that point to infinity in any direction and then examining the places where a segment of the shape crosses the ray. Starting with a count of zero, add one each time a segment crosses the ray from left to right and subtract one each time a path segment crosses the ray from right to left. After counting the crossings, if the result is zero then the point is outside the path. Otherwise, it is inside.
And I have absolutely no clue what that means. But maybe it is important?
Edit
I should have been looking at the end of strings. There is z at the end of Path.Data, which means figure is closed.
Strangely enough, trying to remove z (by using Geometry.ToString()/Geometry.Parse() combo) doesn't works. After some investigation I found what Combine produces physically enclosing figures (commands L x;y, where x;y is the leftmost point). And the worst thing is what it's not always the last point, so simply removing last L x;y before parsing doesn't works either. =(
Edit
Sample to demonstrate problem:
Xaml:
<Path x:Name="path" Stroke="Red"/>
Code:
var geometry1 = new RectangleGeometry(new Rect(100, 100, 100, 100));
var geometry2 = new PathGeometry(new[] { new PathFigure(new Point(0,0), new[] {
new LineSegment(new Point(300, 300), true),
new LineSegment(new Point(300, 0), true),
}, false) });
//path.Data = geometry1;
//path.Data = geometry2;
//path.Data = Geometry.Combine(geometry1, geometry2, GeometryCombineMode.Intersect, null);
Pictures of geometry1 and geometry2:
Resulting Combine:
As you can see 2 lines become 3 after clipping, debugging proves it:
{F1M100;100L200;100 200;200 100;100z}
Notice, it's not only z, but also 100;100 point at the end, connecting starting point.
I attempted to implement a clipping solutions for non closed geometry based on this line intersection algorithm
Code
public static PathGeometry ClipGeometry(PathGeometry geom, Rect clipRect)
{
PathGeometry clipped = new PathGeometry();
foreach (var fig in geom.Figures)
{
PathSegmentCollection segments = new PathSegmentCollection();
Point lastPoint = fig.StartPoint;
foreach (LineSegment seg in fig.Segments)
{
List<Point> points;
if (LineIntersectsRect(lastPoint, seg.Point, clipRect, out points))
{
LineSegment newSeg = new LineSegment(points[1], true);
PathFigure newFig = new PathFigure(points[0], new[] { newSeg }, false);
clipped.Figures.Add(newFig);
}
lastPoint = seg.Point;
}
}
return clipped;
}
static bool LineIntersectsRect(Point lineStart, Point lineEnd, Rect rect, out List<Point> points)
{
points = new List<Point>();
if (rect.Contains(lineStart) && rect.Contains(lineEnd))
{
points.Add(lineStart);
points.Add(lineEnd);
return true;
}
Point outPoint;
if (Intersects(lineStart, lineEnd, rect.TopLeft, rect.TopRight, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.BottomLeft, rect.BottomRight, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.TopLeft, rect.BottomLeft, out outPoint))
{
points.Add(outPoint);
}
if (Intersects(lineStart, lineEnd, rect.TopRight, rect.BottomRight, out outPoint))
{
points.Add(outPoint);
}
if (points.Count == 1)
{
if (rect.Contains(lineStart))
points.Add(lineStart);
else
points.Add(lineEnd);
}
return points.Count > 0;
}
static bool Intersects(Point a1, Point a2, Point b1, Point b2, out Point intersection)
{
intersection = new Point(0, 0);
Vector b = a2 - a1;
Vector d = b2 - b1;
double bDotDPerp = b.X * d.Y - b.Y * d.X;
if (bDotDPerp == 0)
return false;
Vector c = b1 - a1;
double t = (c.X * d.Y - c.Y * d.X) / bDotDPerp;
if (t < 0 || t > 1)
return false;
double u = (c.X * b.Y - c.Y * b.X) / bDotDPerp;
if (u < 0 || u > 1)
return false;
intersection = a1 + t * b;
return true;
}
currently solution works for line based geometry, other types perhaps need to be included if needed.
test xaml
<UniformGrid Columns="2"
Margin="250,250,0,0">
<Grid>
<Path x:Name="pathClip"
Fill="#22ff0000" />
<Path x:Name="path"
Stroke="Black" />
</Grid>
<Path x:Name="path2"
Margin="100,0,0,0"
Stroke="Black" />
</UniformGrid>
test code 1
void test()
{
var geometry = new PathGeometry(new[] { new PathFigure(new Point(0,0), new[] {
new LineSegment(new Point(300, 300), true),
new LineSegment(new Point(300, 0), true),
}, false) });
Rect clipRect= new Rect(10, 10, 180, 180);
path.Data = ClipGeometry(geometry, clipRect);
path2.Data = geometry;
pathClip.Data = new RectangleGeometry(clipRect);
}
result
test code 2
void test()
{
var radius = 1.0;
var figures = new List<LineSegment>();
for (int i = 0; i < 2000; i++, radius += 0.1)
{
var segment = new LineSegment(new Point(radius * Math.Sin(i), radius * Math.Cos(i)), true);
segment.Freeze();
figures.Add(segment);
}
var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
Rect clipRect= new Rect(10, 10, 180, 180);
path.Data = ClipGeometry(geometry, clipRect);
path2.Data = geometry;
pathClip.Data = new RectangleGeometry(clipRect);
}
result
give it a try and see how close it is.
If i am right your mainquestion is:"How do i improve the performance of drawing many shapes?"
To get this working you have to understand Geometry Math.
Geometric objects can only merged/combined if they connect or overlap. And there is a big difference between path-geometry and shape-geometry.
As example if two circle overlap you can combine them in WPF
to get the overlapping region: Intersect
to get the difference: Xor
to get the combined surface: Union
to get the difference of only one shape: Exclude
For path-geometry it's a little different, because a path has no surface a path cannot Intersect | Xor | Union | Exclude another path or shape.
But WPF thinks you just forgot to close the path and is doing that for you, which result in the given result in your question.
so to achieve a performanceboost you have to filter all the geometry first for shapes and paths.
foreach(Shape geometryObj in ControlsOrWhatEver)
{
if(geometryObj is Line || geometryObj is Path || geometryObj is Polypath)
{
pathList.Add(geometryObj);
}
else
{
shapeList.Add(geometryObj);
}
}
for the shapeList you can use Geometry.Combine, but for the pathList you have to do some other work. You have to check if the connect at some point, doesnt matter if beginningPoint, endPoint or somwhere inbetween.
If you have done that you can Merge not Combine the path by yourself like:
public Polyline mergePaths(Shape line1, Shape line2)
{
if(!checkLineType(line1) || !checkLineType(line2))
{
return null;
}
if(hitTest(line1, line2))
{
//here you have to do some math to determine the overlapping points
//on these points you can do something like this:
foreach(Point p in Overlapping Points)
{
//add the first line until p then add line2 and go on to add lin 1 until another p
}
}
else
{
return null;
}
}