unit Devices;
interface
uses
	SysUtils, Geomet, Projections, Graphics, Windows,
	Classes, Complex, GraphicUtils, RefObjects, Transformer;

var
	MonitorSize: record
		Width: Integer;
		Height: Integer;
	end = (Width: 0; Height: 0);

type
	TClipMode = (cmClipAlways, cmClipIfNeed);
	TDeviceInfo = class
	private
		//	Проекция отображения мировых координат в координаты виртуальной страницы
		FProjection: TProjection;

		//	Информация об устройстве отображения
		FResolution: TPoint;
		FMeterToDeviceScale: TDPoint;	{ множитель для преобразования метров в пикселы }
		FPageToDeviceScale: TDPoint;	{ множитель для преобразования в координаты устройства }
		FPageToMeterScale: TDPoint;

		FDeviceRect: TRect;	{	прямоугольник на устройстве вывода }
		FDeviceClipRect: TRect; { видимый прямоугольник на устройстве вывода }

		FPageRect: TDRect;	{ область, которую отображает окно устройства }
		FPageClipRect: TDRect;	{ видимый прямоугольник на виртуальной странице }
		FPageConvertable: TDRect; { область координат страницы, конвертируемых без искажений }

		procedure Changed;

		procedure SetProjection(aValue: TProjection);
		procedure SetDeviceRect(aValue: TRect);
		procedure SetDeviceClipRect(aValue: TRect);

		function GetPageOffset: TDPoint;
		procedure SetPageOffset(const aValue: TDPoint);
		procedure Recalculate;
	public
		OnChanged: TNotifyEvent;

		constructor Create;
		destructor Destroy; override;

		function IsValid: Boolean;
		procedure Assign(aResolution: TPoint; const aDeviceRect: TRect);
		function AddProjection(aProjection: TProjection): TProjection;
		procedure RemoveProjection(aProjection: TProjection);

		//	Преобразования Page <-> Device
		function SizePageToDevice(aValue: Double): Integer;
		function SizeDeviceToPage(aValue: Integer): Double;

		function PointPageToDevice(const aPoint: TDPoint): TPoint;
		function PointDeviceToPage(const aPoint: TPoint): TDPoint;

		function BoundsPageToDevice(const aBounds: TDRect): TRect;
		function BoundsDeviceToPage(const aBounds: TRect): TDRect;

		procedure EdgePageToDevice(const anEdge: TEdge; aPoints: TPointList;
			aPixelVarianceWidth: Integer = 1);
		function ContourPageToDevice(aContour: TContour; aWidth: Double;
			aDevPoints: TDevPoints): TDPoint;

		//	Conversations World <-> Device
		function SizeWorldToDevice(aValue: Double): Integer;
		function SizeDeviceToWorld(aValue: Integer): Double;

		function PointWorldToDevice(const aPoint: TDPoint): TPoint;
		function PointDeviceToWorld(aPoint: TPoint): TDPoint;

		function BoundsWorldToDevice(const aBounds: TDRect): TRect;
		function BoundsDeviceToWorld(const aBounds: TRect): TDRect;

		//	Service
		function GetTouchInfo(aPoints: TDPointList; anOptions: TTouchOptions;
			anOpaque: Boolean; aPoint: TPoint; anIndexPtr: PInteger): TTouchInfo;
		function GetNumberOfSegments(aRadius, aDelta: Double): Integer;

		//	Drawing
		procedure DrawPageContourEx(aCanvas: TCanvas; aWidth: Double; aContour: TContour;
			aClipMode: TClipMode; aPixelVarianceWidth: Integer = 1);

		procedure DrawPageContourWithAttrs(aCanvas: TCanvas; aContour: TContour;
			const aPen: TPenAttrsEx; const aBrush: TBrushAttrsEx; anUseAlphaBlend: Boolean);
		procedure DrawWorldContourWithAttrs(aCanvas: TCanvas; aContour: TContour;
			const aPen: TPenAttrsEx; const aBrush: TBrushAttrsEx; anUseAlphaBlend: Boolean);

		property Projection: TProjection read FProjection write SetProjection;
		property Resolution: TPoint read FResolution;

		property MeterToDeviceScale: TDPoint read FMeterToDeviceScale;
		property PageToDeviceScale: TDPoint read FPageToDeviceScale;
		property PageToMeterScale: TDPoint read FPageToMeterScale;

		property DeviceRect: TRect read FDeviceRect write SetDeviceRect;
		property DeviceClipRect: TRect read FDeviceClipRect write SetDeviceClipRect;

		property PageRect: TDRect read FPageRect;
		property PageOffset: TDPoint read GetPageOffset write SetPageOffset;
		property PageClipRect: TDRect read FPageClipRect;
		property PageConvertable: TDRect read FPageConvertable;
		property MeterToPixel: Double read FPageToDeviceScale.X;
	end;

	function SetGDIWorldTransform(const aDC: HDC; const aCoordSpace: TCoordSpace): Boolean;

	function GetLogicalCanvasResolution(aCanvas: TCanvas): TPoint;
	function GetLogicalDeviceResolution(aHandle: HDC): TPoint;
	function GetCanvasResolution(aCanvas: TCanvas): TPoint;
	function GetDeviceResolution(aHandle: HDC): TPoint;
	function GetDefResolution(aHandle: HDC; const aResolution: TPoint): TPoint;

implementation
uses
	InSysUtils, Math;


function SetGDIWorldTransform(const aDC: HDC; const aCoordSpace: TCoordSpace): Boolean;
var
	anXForm: TXForm;
begin
	with aCoordSpace.DXFormTo do begin
		anXForm.eM11 := a;
		anXForm.eM12 := d;
		anXForm.eM21 := b;
		anXForm.eM22 := e;
		anXForm.eDx := c;
		anXForm.eDy := f;
	end;
	Result := SetWorldTransform(aDC, anXForm);
end;

function GetLogicalCanvasResolution(aCanvas: TCanvas): TPoint;
begin
	Assert(Assigned(aCanvas));
	Result := GetLogicalDeviceResolution(aCanvas.Handle);
end;

function GetLogicalDeviceResolution(aHandle: HDC): TPoint;
begin
	Result.x := GetDeviceCaps(aHandle, LOGPIXELSX);
	Result.y := GetDeviceCaps(aHandle, LOGPIXELSY);
end;

function GetCanvasResolution(aCanvas: TCanvas): TPoint;
begin
	Assert(Assigned(aCanvas));
	Result := GetDeviceResolution(aCanvas.Handle);
end;

function GetDeviceResolution(aHandle: HDC): TPoint;

	function GetResolution(aHandle: HDC): TPoint;
	var
		aWidth, aHeight: Integer;
		anIsMonitor: Boolean;
	begin
		anIsMonitor := GetDeviceCaps(aHandle, TECHNOLOGY) = DT_RASDISPLAY;
		aWidth := MonitorSize.Width;
		if not anIsMonitor or  (aWidth = 0) then begin
			aWidth := GetDeviceCaps(aHandle, HORZSIZE);
			if aWidth = 0 then
				aWidth := 380;
		end;

		aHeight := MonitorSize.Height;
		if not anIsMonitor or (aHeight = 0) then begin
			aHeight := GetDeviceCaps(aHandle, VERTSIZE);
			if aHeight = 0 then
				aHeight := 300;
		end;

		Result.x := Round(25.4 * GetDeviceCaps(aHandle, HORZRES)/aWidth + 0.5);
		Result.y := Round(25.4 * GetDeviceCaps(aHandle, VERTRES)/aHeight + 0.5);
	end;

begin
	if aHandle = 0 then begin
		aHandle := GetDC(0);
		try
			Result := GetResolution(aHandle)
		finally
			ReleaseDC(0, aHandle);
		end;
	end
	else
		Result := GetResolution(aHandle)
end;

function GetDefResolution(aHandle: HDC; const aResolution: TPoint): TPoint;

	function GetDefResolutionX(aHandle, aResolutionX: Integer): Integer;
	begin
		if (aResolutionX = 0) or (aResolutionX = -1) then begin
			if aResolutionX = 0 then
				Result := GetDeviceResolution(aHandle).X
			else
				Result := GetLogicalDeviceResolution(aHandle).X;
		end
		else
			Result := aResolutionX;
	end;

	function GetDefResolutionY(aHandle, aResolutionY: Integer): Integer;
	begin
		if (aResolutionY = 0) or (aResolutionY = -1) then begin
			if aResolutionY = 0 then
				Result := GetDeviceResolution(aHandle).Y
			else
				Result := GetLogicalDeviceResolution(aHandle).Y;
		end
		else
			Result := aResolutionY;
	end;

begin
	Result := aResolution;

	if aHandle = 0 then begin
		aHandle := GetDC(0);
		try
			Result.X := GetDefResolutionX(aHandle, Result.X);
			Result.Y := GetDefResolutionY(aHandle, Result.Y);
		finally
			ReleaseDC(0, aHandle);
		end
	end
	else begin
		Result.X := GetDefResolutionX(aHandle, Result.X);
		Result.Y := GetDefResolutionY(aHandle, Result.Y);
	end;
end;


{  TDeviceInfo }

constructor TDeviceInfo.Create;
begin
	inherited Create;
	oInit(FProjection, TMatrixProjection.Create);
end;

destructor TDeviceInfo.Destroy;
begin
	FProjection.Release;
	inherited Destroy;
end;

procedure TDeviceInfo.Recalculate();
begin
	FPageClipRect := BoundsDeviceToPage(RectInflatedBy(FDeviceClipRect, 1, 1));
	FPageConvertable := FPageClipRect;
	FPageConvertable.Inflate(SizeDeviceToPage(8000), SizeDeviceToPage(8000));
end;

procedure TDeviceInfo.Changed;
begin
	if IsValid and Assigned(OnChanged) then
		OnChanged(Self);
end;

procedure TDeviceInfo.SetProjection(aValue: TProjection);
begin
	if aValue <> FProjection then begin
		oReplace(FProjection, aValue);
		Recalculate;
	end;
end;

procedure TDeviceInfo.SetDeviceRect(aValue: TRect);
begin
	Assign(FResolution, aValue);
end;

procedure TDeviceInfo.SetDeviceClipRect(aValue: TRect);
begin
	if not RectEqual(FDeviceClipRect, aValue) then begin
		FDeviceClipRect := aValue;
		Recalculate;
	end;
end;

function TDeviceInfo.GetPageOffset: TDPoint;
begin
	Result.X := FPageRect.X1;
	Result.Y := FPageRect.Y1;
end;

procedure TDeviceInfo.SetPageOffset(const aValue: TDPoint);
begin
	if (aValue.X <> FPageRect.X1) or (aValue.Y <> FPageRect.Y1) then begin
		with FPageRect do
			Offset(aValue.X - X1, aValue.Y - Y1);
		Recalculate;
		Changed;
	end;
end;

procedure TDeviceInfo.Assign(aResolution: TPoint; const aDeviceRect: TRect);
begin
	if not RectEqual(FDeviceRect, aDeviceRect) or
		(aResolution.x <> FResolution.x) or (aResolution.y <> FResolution.y)
	then begin
		FResolution := aResolution;
		FDeviceRect := aDeviceRect;

		FMeterToDeviceScale.X := aResolution.X / 0.0254;
		FMeterToDeviceScale.Y := aResolution.Y / 0.0254;
		if aDeviceRect.Top < aDeviceRect.Bottom then
			FMeterToDeviceScale.Y := -FMeterToDeviceScale.Y;

		{!!!
		with aDeviceRect, aPageRect do begin
			FPageToDeviceScale.X := DX / (Right - Left + 1);
			FPageToDeviceScale.Y := DY / -(Bottom - Top + 1);
		end;
		}
		FPageToDeviceScale := FMeterToDeviceScale;

		FPageToMeterScale.X := FPageToDeviceScale.X / FMeterToDeviceScale.X;
		FPageToMeterScale.Y := FPageToDeviceScale.Y / FMeterToDeviceScale.Y;

		if IsValid then begin
			with FPageRect do begin
				X1 := aDeviceRect.Left / FPageToDeviceScale.X;
				Y1 := aDeviceRect.Bottom / FPageToDeviceScale.Y;
				X2 :=	aDeviceRect.Right / FPageToDeviceScale.X;
				Y2 := aDeviceRect.Top / FPageToDeviceScale.Y;
			end;
			Recalculate;
		end;
		Changed;
	end;
end;

function TDeviceInfo.AddProjection(aProjection: TProjection): TProjection;
var
	aGroupProjection: TGroupProjection;
begin
	Result := Projection;
	if Assigned(Result) then begin
		Result.AddRef;
		try
			aGroupProjection := TGroupProjection.Create;
			aGroupProjection.Items.Add(aProjection);
			aGroupProjection.Items.Add(Result);
			Projection := aGroupProjection;
		finally
			Result.Release;
		end;
	end
	else
		Projection := aProjection;
end;

procedure TDeviceInfo.RemoveProjection(aProjection: TProjection);
begin
	if Projection is TGroupProjection then begin
		(Projection as TGroupProjection).Items.Remove(aProjection);
		if (Projection as TGroupProjection).Items.Count = 0 then
			Projection := TMatrixProjection.Create
		else if (Projection as TGroupProjection).Items.Count = 1 then
			Projection := (Projection as TGroupProjection).Items[0]

	end;
end;


function TDeviceInfo.IsValid: Boolean;
begin
	with FPageToDeviceScale do
		Result := (X <> 0) and (Abs(Y) <> 0);
end;

function DoubleToInt(d: Double): Integer;
begin
	if d < 0 then
		if d < -MaxInt then
			d := -MaxInt
		else
	else if d > MaxInt then
		d := MaxInt;
	Result := Round(d);
end;

//	Conversations Page <-> Device
function TDeviceInfo.SizePageToDevice(aValue: Double): Integer;
var
	aScale: Double;
begin
	with FPageToDeviceScale do
		if Abs(Y) = X then
			aScale := X
		else
			aScale := Sqrt(Abs(Y) * X);
	Result := DoubleToInt(aValue * aScale);
end;

function TDeviceInfo.SizeDeviceToPage(aValue: Integer): Double;
var
	aScale: Double;
begin
	with FPageToDeviceScale do
		if Abs(Y) = X then
			aScale := X
		else
			aScale := Sqrt(Abs(Y) * X);
	Result := aValue / aScale;
end;

function TDeviceInfo.PointPageToDevice(const aPoint: TDPoint): TPoint;
var
	aX, aY: Double;
begin
	aX := FDeviceRect.Left + (aPoint.X - FPageRect.X1) * FPageToDeviceScale.X;
	aY := FDeviceRect.Bottom + (aPoint.Y - FPageRect.Y1) * FPageToDeviceScale.Y;
	if aX < -(MaxInt - 100) then
		aX := -(MaxInt - 100)
	else if aX > (MaxInt - 100) then
		aX := (MaxInt - 100);
	if aY < -(MaxInt - 100) then
		aY := -(MaxInt - 100)
	else if aY > (MaxInt - 100) then
		aY := (MaxInt - 100);
	Result.x := Round(aX);
	Result.y := Round(aY);
end;

function TDeviceInfo.PointDeviceToPage(const aPoint: TPoint): TDPoint;
begin
	Result.x := (aPoint.x - FDeviceRect.Left) / FPageToDeviceScale.X + FPageRect.X1;
	Result.y := (aPoint.y - FDeviceRect.Bottom) / FPageToDeviceScale.Y + FPageRect.Y1;
end;

function TDeviceInfo.BoundsPageToDevice(const aBounds: TDRect): TRect;
begin
	with aBounds do begin
		with PointPageToDevice(DPoint(X1, Y1)) do SetRect(Result, X, Y, X, Y);
		ExpandRect(Result, PointPageToDevice(DPoint(X1, Y2)));
		ExpandRect(Result, PointPageToDevice(DPoint(X2, Y1)));
		ExpandRect(Result, PointPageToDevice(DPoint(X2, Y2)));
	end;
end;

function TDeviceInfo.BoundsDeviceToPage(const aBounds: TRect): TDRect;
begin
	with aBounds do begin
		with PointDeviceToPage(Point(Left, Bottom)) do Result.Assign(X, Y, X, Y);
		Result.Expand(PointDeviceToPage(Point(Left, Top)));
		Result.Expand(PointDeviceToPage(Point(Right, Bottom)));
		Result.Expand(PointDeviceToPage(Point(Right, Top)));
	end;
end;

function TDeviceInfo.SizeWorldToDevice(aValue: Double): Integer;
begin
	Result := SizePageToDevice(aValue * Projection.GetZoomScale);
end;

function TDeviceInfo.SizeDeviceToWorld(aValue: Integer): Double;
begin
	Result := SizeDeviceToPage(aValue) / Projection.GetZoomScale;
end;

function TDeviceInfo.PointWorldToDevice(const aPoint: TDPoint): TPoint;
begin
	Result := PointPageToDevice(Projection.ProjectPoint(aPoint));
end;

function TDeviceInfo.PointDeviceToWorld(aPoint: TPoint): TDPoint;
begin
	Result := Projection.UnprojectPoint(PointDeviceToPage(aPoint));
end;

function TDeviceInfo.BoundsWorldToDevice(const aBounds: TDRect): TRect;
begin
	Result := BoundsPageToDevice(Projection.ProjectBounds(aBounds));
end;

function TDeviceInfo.BoundsDeviceToWorld(const aBounds: TRect): TDRect;
begin
	Result := Projection.UnprojectBounds(BoundsDeviceToPage(aBounds));
end;


function TDeviceInfo.GetTouchInfo(aPoints: TDPointList; anOptions: TTouchOptions;
	anOpaque: Boolean; aPoint: TPoint; anIndexPtr: PInteger): TTouchInfo;
var
	aDPoint, aP: TDPoint;
	aDelta: Double;
	i: Integer;
begin
	Result := TouchNone;
	aDPoint := PointDeviceToWorld(aPoint);
	aDelta := SizeDeviceToWorld(8 div 2 + 1);
	{	Проверить вершины }
	if TouchVertex in anOptions then
		for i := 0 to aPoints.Count - 1 do begin
			aP := aPoints[i];
			if aP.CloselyTo(aDPoint, aDelta) then begin
				Result := TouchVertex;
				if Assigned(anIndexPtr) then
					anIndexPtr^ := i;
				Exit;
			end;
		end;
	{	Проверить ребра }
	if TouchEdge in anOptions then begin
		i := aPoints.EdgeAtPoint(aDPoint, aDelta);
		if i <> -1 then begin
			Result := TouchEdge;
			if Assigned(anIndexPtr) then
				anIndexPtr^ := i;
			Exit;
		end;
	end;
	{ Проверить внутри }
	if TouchInside in anOptions then
		if (aPoints.Closed and anOpaque) and aPoints.ContainsPoint(aDPoint) then begin
			Result := TouchInside;
			if Assigned(anIndexPtr) then
				anIndexPtr^ := -1;
			Exit;
		end;
end;

function TDeviceInfo.GetNumberOfSegments(aRadius, aDelta: Double): Integer;
var
	aCos: Double;
begin
	Result := 1;
	if aRadius > 0 then begin
		aCos := 1 - 0.5*SizeDeviceToPage(1)/aRadius;
		if (aCos >= -1) and (aCos < 1) then
			Result := Trunc(Abs(aDelta/ArcCos(aCos))) + 1
	end;
	if Result > 1024 then
		Result := 1024
end;

procedure TDeviceInfo.EdgePageToDevice(const anEdge: TEdge; aPoints: TPointList;
			aPixelVarianceWidth: Integer = 1);
var
	alfa: Double;
	aSin, aCos: Extended;
	i, aNumber: Integer;
begin
	with anEdge do begin
		if aPoints.PointsCount = 0 then
			aPoints.AddPoint(PointPageToDevice(P1), aPixelVarianceWidth);
		if IsArc then begin
			alfa := GetAngle(Center, P1);
			aNumber := GetNumberOfSegments(Radius, Delta);
			for i := 1 to aNumber - 1 do begin
				SinCos(alfa + i*Delta/aNumber, aSin, aCos);
				aPoints.AddPoint(PointPageToDevice(DPoint(Center.x + Radius*aCos,
					Center.y + Radius*aSin)), aPixelVarianceWidth);
			end
		end;
		aPoints.AddPoint(PointPageToDevice(P2), aPixelVarianceWidth)
	end;
end;


function TDeviceInfo.ContourPageToDevice(aContour: TContour; aWidth: Double;
	aDevPoints: TDevPoints): TDPoint;
var
	anEdge: TContourEdge;
	aSegment: TContourSegment;
	aBounds, anArea: TDRect;
begin
	aDevPoints.BeginUpdate;
	if aContour.GetBounds(aBounds) then begin
		Result := DPoint(aBounds.x1, aBounds.y1);

		anArea := FPageClipRect;
		anArea.Inflate(aWidth, aWidth);
		if DRectIntersectedWith(anArea, aBounds) then begin
			if not DRectContainsRect(PageConvertable, aBounds) then begin
				ClippingContour.Assign(aContour);
				MakeClipping(anArea, aBounds);
				aContour := ClippingContour;
			end;
			aSegment.Init(aContour, 0);
			while not aSegment.EndOfContour do begin
				aDevPoints.NewSegment;
				anEdge.Init(aContour, aSegment.FirstIndex, True);
				while anEdge.Index <= aSegment.LastIndex do begin
					EdgePageToDevice(anEdge, aDevPoints.Segment);
					anEdge.Next
				end;
				aDevPoints.Segment.Closed := aSegment.Closed;
				aSegment.Next
			end;
		end;
	end
	else begin
		Result.X := 0;
		Result.Y := 0;
	end;
	aDevPoints.EndUpdate
end;

procedure TDeviceInfo.DrawPageContourEx(aCanvas: TCanvas; aWidth: Double; aContour: TContour;
			aClipMode: TClipMode; aPixelVarianceWidth: Integer = 1);

	function ContourPageToDevice(aContour: TContour; aWidth: Double;
		aDevPoints: TDevPoints): TDPoint;
	var
		anEdge: TContourEdge;
		aSegment: TContourSegment;
		aBounds, anArea: TDRect;
		aClipRect: TDRect;
		aClip: Boolean;
	begin
		aDevPoints.BeginUpdate;
		if aContour.GetBounds(aBounds) then begin
			Result := DPoint(aBounds.x1, aBounds.y1);
			anArea := FPageClipRect;
			anArea.Inflate(aWidth, aWidth);
			if DRectIntersectedWith(anArea, aBounds) then begin
				if aClipMode = cmClipAlways then begin
					aClipRect := anArea;
					aClipRect.Inflate(aClipRect.DX/2, aClipRect.DY/2);
					aClip := not DRectContainsRect(aClipRect, aBounds);
				end
				else
					aClip := not DRectContainsRect(PageConvertable, aBounds);
				if aClip then begin
					ClippingContour.Assign(aContour);
					MakeClipping(anArea, aBounds);
					aContour := ClippingContour;
				end;
				aSegment.Init(aContour, 0);
				while not aSegment.EndOfContour do begin
					aDevPoints.NewSegment;
					anEdge.Init(aContour, aSegment.FirstIndex, True);
					while anEdge.Index <= aSegment.LastIndex do begin
						EdgePageToDevice(anEdge, aDevPoints.Segment, aPixelVarianceWidth);
						anEdge.Next
					end;
					aDevPoints.Segment.Closed := aSegment.Closed;
					aSegment.Next
				end;
			end;
		end;
		aDevPoints.EndUpdate
	end;

var
	aDevPoints: TDevPoints;
begin
	aDevPoints := GetDevPoints;
	try
		ContourPageToDevice(aContour, aWidth, aDevPoints);
		aDevPoints.Paint(aCanvas);
	finally
		aDevPoints.Free
	end;
end;

procedure TDeviceInfo.DrawPageContourWithAttrs(aCanvas: TCanvas; aContour: TContour;
	const aPen: TPenAttrsEx; const aBrush: TBrushAttrsEx; anUseAlphaBlend: Boolean);
var
	aDevPoints: TDevPoints;
begin
	if (aPen.Style = psClear) and (aBrush.Style = bsClear) then
		Exit;

	aDevPoints := GetDevPoints;
	try
		ContourPageToDevice(aContour, aPen.GetWidthInMeters(Projection.GetZoomScale), aDevPoints);
		aDevPoints.Paint(aCanvas, aPen, aBrush, Projection.GetZoomScale, PageToDeviceScale.X, anUseAlphaBlend);
	finally
		aDevPoints.Free
	end;
end;

procedure TDeviceInfo.DrawWorldContourWithAttrs(aCanvas: TCanvas; aContour: TContour;
	const aPen: TPenAttrsEx; const aBrush: TBrushAttrsEx; anUseAlphaBlend: Boolean);
var
	c: TContour;
begin
	c := ContourCache.GetCopy(aContour);
	try
		Projection.ProjectContour(c);
		DrawPageContourWithAttrs(aCanvas, c, aPen, aBrush, anUseAlphaBlend);
	finally
		ContourCache.Release(c)
	end;
end;



end.

