diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5864b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Map/* +Libs/* \ No newline at end of file diff --git a/Demo/MainUnit.dfm b/Demo/MainUnit.dfm new file mode 100644 index 0000000..3eab960 --- /dev/null +++ b/Demo/MainUnit.dfm @@ -0,0 +1,168 @@ +object MainForm: TMainForm + Left = 16 + Top = 116 + Caption = 'Test of OSM map control' + ClientHeight = 720 + ClientWidth = 1003 + Color = clBtnFace + DoubleBuffered = True + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -14 + Font.Name = 'MS Sans Serif' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + OnClose = FormClose + OnCreate = FormCreate + OnDestroy = FormDestroy + PixelsPerInch = 120 + TextHeight = 16 + object Splitter1: TSplitter + Left = 767 + Top = 0 + Width = 8 + Height = 576 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alRight + Beveled = True + end + object Panel1: TPanel + Left = 0 + Top = 0 + Width = 767 + Height = 576 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alClient + BevelOuter = bvNone + TabOrder = 0 + object mMap: TScrollBox + Left = 0 + Top = 0 + Width = 767 + Height = 576 + HorzScrollBar.Tracking = True + VertScrollBar.Smooth = True + VertScrollBar.Tracking = True + Align = alClient + AutoScroll = False + DoubleBuffered = True + DragCursor = crSizeAll + ParentDoubleBuffered = False + TabOrder = 0 + OnMouseMove = mMapMouseMove + end + end + object Panel2: TPanel + Left = 775 + Top = 0 + Width = 228 + Height = 576 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Align = alRight + BevelOuter = bvNone + TabOrder = 1 + object btnZoomIn: TSpeedButton + Left = 100 + Top = 13 + Width = 51 + Height = 36 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Glyph.Data = { + 66010000424D6601000000000000760000002800000014000000140000000100 + 040000000000F000000000000000000000001000000000000000000000000000 + 8000008000000080800080000000800080008080000080808000C0C0C0000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00EEEEEEEEEEEE + EEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEE + EEE000000EEEEEEE0000EEEEEE0FFFFFF0EEEEEE0000EEEEE0FFFFFFFF0EEEEE + 0000EEEE0FFFFFFFFFF0EEEE0000EEE0FFFFFCCFFFF0EEEE0000EEE0FFFFFCCF + FFFF0EEE0000EEE0FFFCCCCCCFFF0EEE0000EEE0FFFCCCCCCFFF0EEE0000EEE0 + FFFFFCCFFFFF0EEE0000EEE0FFFFFCCFFFF0EEEE0000EEEE0FFFFFFFFFF0EEEE + 0000EEEEE0FFFFFFFF0EEEEE0000EEEEEE0FFFFFF0EEEEEE0000EEEEEEE00000 + 0EEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEE + EEEEEEEEEEEEEEEE0000} + OnClick = btnZoomInClick + end + object btnZoomOut: TSpeedButton + Left = 164 + Top = 13 + Width = 51 + Height = 36 + Margins.Left = 4 + Margins.Top = 4 + Margins.Right = 4 + Margins.Bottom = 4 + Glyph.Data = { + 66010000424D6601000000000000760000002800000014000000140000000100 + 040000000000F000000000000000000000001000000000000000000000000000 + 8000008000000080800080000000800080008080000080808000C0C0C0000000 + FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00EEEEEEEEEEEE + EEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEE + EEE000000EEEEEEE0000EEEEEE0FFFFFF0EEEEEE0000EEEEE0FFFFFFFF0EEEEE + 0000EEEE0FFFFFFFFFF0EEEE0000EEE0FFFFFFFFFFF0EEEE0000EEE0FFFFFFFF + FFFF0EEE0000EEE0FFFCCCCCCFFF0EEE0000EEE0FFFCCCCCCFFF0EEE0000EEE0 + FFFFFFFFFFFF0EEE0000EEE0FFFFFFFFFFF0EEEE0000EEEE0FFFFFFFFFF0EEEE + 0000EEEEE0FFFFFFFF0EEEEE0000EEEEEE0FFFFFF0EEEEEE0000EEEEEEE00000 + 0EEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEEEEEEEEEEEEEEEEEE0000EEEE + EEEEEEEEEEEEEEEE0000} + OnClick = btnZoomOutClick + end + object Label1: TLabel + Left = 16 + Top = 488 + Width = 41 + Height = 16 + Caption = 'Label1' + end + object Label2: TLabel + Left = 16 + Top = 512 + Width = 41 + Height = 16 + Caption = 'Label2' + end + object lblZoom: TLabel + Left = 7 + Top = 13 + Width = 74 + Height = 36 + AutoSize = False + Font.Charset = RUSSIAN_CHARSET + Font.Color = clWindowText + Font.Height = -17 + Font.Name = 'Tahoma' + Font.Style = [fsBold] + ParentFont = False + Layout = tlCenter + end + object Button1: TButton + Left = 24 + Top = 256 + Width = 177 + Height = 33 + Caption = 'Save layer' + TabOrder = 0 + OnClick = Button1Click + end + end + object mLog: TMemo + Left = 0 + Top = 576 + Width = 1003 + Height = 144 + Align = alBottom + TabOrder = 2 + end +end diff --git a/Demo/MainUnit.pas b/Demo/MainUnit.pas new file mode 100644 index 0000000..6a1d433 --- /dev/null +++ b/Demo/MainUnit.pas @@ -0,0 +1,221 @@ +unit MainUnit; + +interface + +uses + Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, + Dialogs, ExtCtrls, Buttons, Vcl.StdCtrls, Math, Types, + OSM.SlippyMapUtils, OSM.MapControl, OSM.TileStorage, + OSM.NetworkRequest, {SynapseRequest,} WinInetRequest; + +const + MSG_GOTTILE = WM_APP + 200; + +type + // Nice trick to avoid registering TMapControl as design-time component + TScrollBox = class(TMapControl) + end; + + TGotTileData = record + Tile: TTile; + Ms: TMemoryStream; + Error: string; + end; + PGotTileData = ^TGotTileData; + + TMainForm = class(TForm) + Panel1: TPanel; + Panel2: TPanel; + Splitter1: TSplitter; + btnZoomIn: TSpeedButton; + btnZoomOut: TSpeedButton; + Button1: TButton; + mMap: TScrollBox; + mLog: TMemo; + Label1: TLabel; + Label2: TLabel; + lblZoom: TLabel; + procedure FormCreate(Sender: TObject); + procedure FormClose(Sender: TObject; var Action: TCloseAction); + procedure FormDestroy(Sender: TObject); + procedure btnZoomInClick(Sender: TObject); + procedure btnZoomOutClick(Sender: TObject); + procedure Button1Click(Sender: TObject); + procedure mMapMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); + + procedure MapGetTile(Sender: TMapControl; TileHorzNum, TileVertNum: Cardinal; out TileBmp: TBitmap); + procedure MsgGotTile(var Message: TMessage); message MSG_GOTTILE; + procedure NetReqGotTile(const Tile: TTile; Ms: TMemoryStream; const Error: string); + procedure mMapZoomChanged(Sender: TObject); + private + NetworkRequest: TNetworkRequestQueue; + TileStorage: TTileStorage; + procedure Log(const s: string); + end; + +var + MainForm: TMainForm; + +implementation + +{$R *.dfm} + +{ TMainForm } + +procedure TMainForm.FormCreate(Sender: TObject); +begin + // Memory/disc cache of tile images + // You probably won't need it if you have another fast storage (f.e. database) + TileStorage := TTileStorage.Create(30); + TileStorage.FileCacheBaseDir := ExpandFileName('..\Map\'); + // Queuer of tile image network requests + // You won't need it if you have another source (f.e. database) + NetworkRequest := TNetworkRequestQueue.Create(4, 3, {}{SynapseRequest.}WinInetRequest.NetworkRequest, NetReqGotTile); + + mMap.OnGetTile := MapGetTile; + mMap.OnZoomChanged := mMapZoomChanged; + mMap.SetZoom(1); +end; + +procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction); +begin + //... +end; + +procedure TMainForm.FormDestroy(Sender: TObject); +begin + FreeAndNil(NetworkRequest); + FreeAndNil(TileStorage); +end; + +procedure TMainForm.Log(const s: string); +begin + mLog.Lines.Add(DateTimeToStr(Now)+' '+s); + OutputDebugString(PChar(DateTimeToStr(Now)+' '+s)); +end; + +// Callback from map control to receive a tile image +procedure TMainForm.MapGetTile(Sender: TMapControl; TileHorzNum, TileVertNum: Cardinal; out TileBmp: TBitmap); +var + Tile: TTile; +begin + Tile.Zoom := Sender.Zoom; + Tile.ParameterX := TileHorzNum; + Tile.ParameterY := TileVertNum; + + // Query tile from storage + TileBmp := TileStorage.GetTile(Tile); + + // Tile image unavailable - queue network request + if TileBmp = nil then + begin + NetworkRequest.RequestTile(Tile); + Log(Format('Queued request from inet %s', [TileToStr(Tile)])); + end; +end; + +// Callback from a thread of network requester that request has been done +// To avoid thread access troubles, re-post all the data to form +procedure TMainForm.NetReqGotTile(const Tile: TTile; Ms: TMemoryStream; const Error: string); +var + pData: PGotTileData; +begin + New(pData); + pData.Tile := Tile; + pData.Ms := Ms; + pData.Error := Error; + if not PostMessage(Handle, MSG_GOTTILE, 0, LPARAM(pData)) then + begin + Dispose(pData); + FreeAndNil(Ms); + end; +end; + +procedure TMainForm.MsgGotTile(var Message: TMessage); +var + pData: PGotTileData; +begin + pData := PGotTileData(Message.LParam); + if pData.Error <> '' then + begin + Log(Format('Error getting tile %s: %s', [TileToStr(pData.Tile), pData.Error])); + end + else + begin + Log(Format('Got from inet %s', [TileToStr(pData.Tile)])); + TileStorage.StoreTile(pData.Tile, pData.Ms); + mMap.RefreshTile(pData.Tile.ParameterX, pData.Tile.ParameterY); + end; + FreeAndNil(pData.Ms); + Dispose(pData); +end; + +procedure TMainForm.btnZoomInClick(Sender: TObject); +begin + mMap.SetZoom(mMap.Zoom + 1); +end; + +procedure TMainForm.btnZoomOutClick(Sender: TObject); +begin + mMap.SetZoom(mMap.Zoom - 1); +end; + +procedure TMainForm.Button1Click(Sender: TObject); +var + bmp, bmTile: TBitmap; + col, row: Integer; + tile: TTile; + imgAbsent: Boolean; +begin + bmp := TBitmap.Create; + bmp.Height := TileCount(mMap.Zoom)*TILE_IMAGE_HEIGHT; + bmp.Width := TileCount(mMap.Zoom)*TILE_IMAGE_WIDTH; + + try + imgAbsent := False; + for col := 0 to TileCount(mMap.Zoom) - 1 do + for row := 0 to TileCount(mMap.Zoom) - 1 do + begin + tile.Zoom := mMap.Zoom; + tile.ParameterX := col; + tile.ParameterY := row; + bmTile := TileStorage.GetTile(tile); + if bmTile = nil then + begin + NetworkRequest.RequestTile(tile); + imgAbsent := True; + Continue; + end; + bmp.Canvas.Draw(col*TILE_IMAGE_WIDTH, row*TILE_IMAGE_HEIGHT, bmTile); + end; + + if imgAbsent then + begin + ShowMessage('Some images were absent'); + Exit; + end; + + bmp.SaveToFile('Map'+IntToStr(mMap.Zoom)+'.bmp'); + ShowMessage('Saved to Map'+IntToStr(mMap.Zoom)+'.bmp'); + finally + FreeAndNil(bmp); + end; +end; + +procedure TMainForm.mMapMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); +var + MapPt: TPoint; + GeoPt: TPointF; +begin + MapPt := mMap.ViewToMap(Point(X, Y)); + GeoPt := mMap.MapToGeoCoords(MapPt); + Label1.Caption := Format('%d : %d', [MapPt.X, MapPt.Y]); + Label2.Caption := Format('%.3f : %.3f', [GeoPt.X, GeoPt.Y]); +end; + +procedure TMainForm.mMapZoomChanged(Sender: TObject); +begin + lblZoom.Caption := Format('%d / %d', [TMapControl(Sender).Zoom, High(TMapZoomLevel)]); +end; + +end. diff --git a/Demo/OSMMapDemo.dpr b/Demo/OSMMapDemo.dpr new file mode 100644 index 0000000..108111d --- /dev/null +++ b/Demo/OSMMapDemo.dpr @@ -0,0 +1,15 @@ +program OSMMapDemo; + +uses + FastMM4, + Forms, + MainUnit in 'MainUnit.pas' {MainForm}; + +{$R *.res} + +begin + ReportMemoryLeaksOnShutdown := True; + Application.Initialize; + Application.CreateForm(TMainForm, MainForm); + Application.Run; +end. diff --git a/Demo/OSMMapDemo.dproj b/Demo/OSMMapDemo.dproj new file mode 100644 index 0000000..3279b58 --- /dev/null +++ b/Demo/OSMMapDemo.dproj @@ -0,0 +1,169 @@ + + + {C4875BA9-942C-4C88-84B0-673F38DD35AF} + OSMMapDemo.dpr + True + Debug + 1 + Application + VCL + 13.4 + Win32 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + Test_OSMTeilsMap_Icon.ico + None + ..\Libs\Synapse;..\;$(DCC_UnitSearchPath) + 1049 + false + false + false + 00400000 + false + false + Vcl;Vcl.Imaging;Vcl.Touch;Vcl.Samples;Vcl.Shell;System;Xml;Data;Datasnap;Web;Soap;Winapi;$(DCC_Namespace) + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments= + + + Test_OSMTeilsMap_Icon.ico + + + $(BDS)\bin\default_app.manifest + 1033 + true + System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + + + 0 + false + RELEASE;$(DCC_Define) + false + + + true + 1033 + $(BDS)\bin\default_app.manifest + + + true + true + true + DEBUG;$(DCC_Define) + false + + + $(BDS)\bin\default_app.manifest + true + 1033 + true + 65001 + true + true + 0 + 2 + true + true + false + true + true + + + + MainSource + + +
MainForm
+
+ + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + +
+ + Delphi.Personality.12 + + + + + OSMMapDemo.dpr + + + False + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 1049 + 1251 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + + False + True + + + 12 + + + +
diff --git a/Demo/Test_OSMTeilsMap_Icon.ico b/Demo/Test_OSMTeilsMap_Icon.ico new file mode 100644 index 0000000..1998c2d Binary files /dev/null and b/Demo/Test_OSMTeilsMap_Icon.ico differ diff --git a/OSM.MapControl.pas b/OSM.MapControl.pas new file mode 100644 index 0000000..0b5d939 --- /dev/null +++ b/OSM.MapControl.pas @@ -0,0 +1,729 @@ +{ + Visual control displaying a map. + Data for the map (tile images) must be supplied via callbacks. + See OSM.TileStorage unit +} +unit OSM.MapControl; + +interface + +uses + Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Math, Types, + OSM.SlippyMapUtils; + +const + // default W and H of cache image in number of tiles. + // Image's memory occupation: + // (4 bytes per pixel)*TilesH*TilesV*(65536 pixels in single tile) + // For value 8 it counts 16.7 Mb + CacheImageDefTilesH = 8; + CacheImageDefTilesV = 8; + // default W and H of cache image in pixels + CacheImageDefWidth = CacheImageDefTilesH*TILE_IMAGE_WIDTH; + CacheImageDefHeight = CacheImageDefTilesV*TILE_IMAGE_HEIGHT; + // margin that is added to cache image to hold view area, in number of tiles + CacheMarginSize = 2; + // size of margin for labels on map, in pixels + LabelMargin = 2; + +type + TMapOption = ( + moDontDrawCopyright, + moDontDrawScale + ); + + TMapOptions = set of TMapOption; + + TMapMark = record + {}// TODO + end; + PMapMark = ^TMapMark; + + TMapControl = class; + + // Callback to get bitmap of a single tile having number (TileHorzNum;TileVertNum) + // If TileBmp is returned nil, DrawTileLoading method is called for this tile + // Generally you must assign this callback only. + TOnGetTile = procedure (Sender: TMapControl; TileHorzNum, TileVertNum: Cardinal; + out TileBmp: TBitmap) of object; + + // Callback to draw bitmap of a single tile having number (TileHorzNum;TileVertNum) + // If OnDrawTile assigned, it means fully custom drawing process, f.ex. if user has + // fast tile sources that are not TBitmap-s, and it is user responsibility to indicate + // tiles that are loading at the moment. + // If OnDrawTileLoading assigned, the handler will be called only for empty tiles + // allowing a user to draw his own label + TOnDrawTile = procedure (Sender: TMapControl; TileHorzNum, TileVertNum: Cardinal; + const TopLeft: TPoint; DestBmp: TBitMap) of object; + + // Virtual control that doesn't hold any data and must be painted by callbacks + TMapControl = class(TScrollBox) + strict private + FMapSize: TSize; // current map dims in pixels + FCacheImage: TBitmap; // drawn tiles (it could be equal to or larger than view area!) + FCopyright, // lazily created cache images for + FScaleLine: TBitmap; // scale line and copyright + FZoom: Integer; // current zoom; integer for simpler operations + FCacheImageRect: TRect; // position of cache image on map in map coords + FMapOptions: TMapOptions; + FDragPos: TPoint; + FOnGetTile: TOnGetTile; + FOnDrawTile: TOnDrawTile; + FOnDrawTileLoading: TOnDrawTile; + FOnZoomChanged: TNotifyEvent; + protected + // overrides + procedure PaintWindow(DC: HDC); override; + procedure Resize; override; + procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override; + function MouseActivate(Button: TMouseButton; Shift: TShiftState; X, Y: Integer; HitTest: Integer): TMouseActivate; override; + function DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; override; + procedure DragOver(Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); override; + procedure WMHScroll(var Message: TWMHScroll); message WM_HSCROLL; + procedure WMVScroll(var Message: TWMVScroll); message WM_VSCROLL; + procedure WMPaint(var Message: TWMPaint); message WM_PAINT; + // main methods + function ViewInCache: Boolean; + procedure UpdateCache; + procedure MoveCache; + function SetCacheDimensions: Boolean; + function FindNextMapMark(const Pt: TPoint; PrevIndex: Integer = -1): Integer; + procedure DrawTileLoading(TileHorzNum, TileVertNum: Cardinal; const TopLeft: TPoint; DestBmp: TBitMap); + procedure DoDrawTile(TileHorzNum, TileVertNum: Cardinal; const TopLeft: TPoint; DestBmp: TBitMap); + // helpers + function ViewAreaRect: TRect; + + procedure SetNWPoint(const MapPt: TPoint); overload; + function GetCenterPoint: TPointF; + procedure SetCenterPoint(const Coords: TPointF); + function GetNWPoint: TPointF; + procedure SetNWPoint(const GeoCoords: TPointF); overload; + + class procedure DrawCopyright(const Text: string; DestBmp: TBitmap); + class procedure DrawScale(Zoom: TMapZoomLevel; DestBmp: TBitmap); + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + + procedure RefreshTile(TileHorzNum, TileVertNum: Cardinal); + + function MapToGeoCoords(const MapPt: TPoint): TPointF; + function GeoCoordsToMap(const GeoCoords: TPointF): TPoint; + function ViewToMap(const ViewPt: TPoint): TPoint; + function MapToView(const MapPt: TPoint): TPoint; + + procedure ScrollMapBy(DeltaHorz, DeltaVert: Integer); + procedure ScrollMapTo(Horz, Vert: Integer); + procedure SetZoom(Value: Integer; const ViewBindPoint: TPoint); overload; + procedure SetZoom(Value: Integer); overload; + + {} + { + add/remove map marks + MouseBox + } + property Zoom: Integer read FZoom; + property MapOptions: TMapOptions read FMapOptions write FMapOptions; + property CenterPoint: TPointF read GetCenterPoint write SetCenterPoint; + property NWPoint: TPointF read GetNWPoint write SetNWPoint; + property OnGetTile: TOnGetTile read FOnGetTile write FOnGetTile; + property OnDrawTile: TOnDrawTile read FOnDrawTile write FOnDrawTile; + property OnDrawTileLoading: TOnDrawTile read FOnDrawTileLoading write FOnDrawTileLoading; + property OnZoomChanged: TNotifyEvent read FOnZoomChanged write FOnZoomChanged; + end; + +function ToInnerCoords(const StartPt, Pt: TPoint): TPoint; overload; inline; +function ToOuterCoords(const StartPt, Pt: TPoint): TPoint; overload; inline; +function ToInnerCoords(const StartPt: TPoint; const Rect: TRect): TRect; overload; inline; +function ToOuterCoords(const StartPt: TPoint; const Rect: TRect): TRect; overload; inline; + +const + SLbl_Loading = 'Loading [%d : %d]...'; + +implementation + +// *** Utils *** + +// Like Client<=>Screen + +function ToInnerCoords(const StartPt, Pt: TPoint): TPoint; +begin + Result := Pt.Subtract(StartPt); +end; + +function ToOuterCoords(const StartPt, Pt: TPoint): TPoint; +begin + Result := Pt.Add(StartPt); +end; + +function ToInnerCoords(const StartPt: TPoint; const Rect: TRect): TRect; +begin + Result.TopLeft := ToInnerCoords(StartPt, Rect.TopLeft); + Result.BottomRight := ToInnerCoords(StartPt, Rect.BottomRight); +end; + +function ToOuterCoords(const StartPt: TPoint; const Rect: TRect): TRect; +begin + Result.TopLeft := ToOuterCoords(StartPt, Rect.TopLeft); + Result.BottomRight := ToOuterCoords(StartPt, Rect.BottomRight); +end; + +// Floor value to tile size + +function ToTileWidthLesser(Width: Cardinal): Cardinal; inline; +begin + Result := (Width div TILE_IMAGE_WIDTH)*TILE_IMAGE_WIDTH; +end; + +function ToTileHeightLesser(Height: Cardinal): Cardinal; inline; +begin + Result := (Height div TILE_IMAGE_HEIGHT)*TILE_IMAGE_HEIGHT; +end; + +// Ceil value to tile size + +function ToTileWidthGreater(Width: Cardinal): Cardinal; inline; +begin + Result := ToTileWidthLesser(Width); + if Width mod TILE_IMAGE_WIDTH > 0 then + Inc(Result, TILE_IMAGE_WIDTH); +end; + +function ToTileHeightGreater(Height: Cardinal): Cardinal; inline; +begin + Result := ToTileHeightLesser(Height); + if Height mod TILE_IMAGE_HEIGHT > 0 then + Inc(Result, TILE_IMAGE_HEIGHT); +end; + +{ TMapControl } + +constructor TMapControl.Create(AOwner: TComponent); +begin + inherited; + FCacheImage := TBitmap.Create; + + FZoom := Pred(Integer(Low(TMapZoomLevel))); + SetZoom(Low(TMapZoomLevel)); +end; + +destructor TMapControl.Destroy; +begin + FreeAndNil(FCacheImage); + FreeAndNil(FCopyright); + FreeAndNil(FScaleLine); + inherited; +end; + +// *** overrides - events *** + +// Main drawing routine +procedure TMapControl.PaintWindow(DC: HDC); +var + ViewRect: TRect; +begin + ViewRect := ViewAreaRect; + // if view area lays within cached image, no update required + if not FCacheImageRect.Contains(ViewRect) then + begin + MoveCache; + UpdateCache; + end; + + // convert ViewRect to CacheImage coords + ViewRect := ToInnerCoords(FCacheImageRect.TopLeft, ViewRect); + + // draw cache (map background) + // ! partial copying from source, TGraphic/TCanvas.Draw can't do that :( + BitBlt(DC, + 0, 0, ViewRect.Width, ViewRect.Height, + FCacheImage.Canvas.Handle, ViewRect.Left, ViewRect.Top, SRCCOPY); + + // init copyright bitmap if not inited yet and draw it + if not (moDontDrawCopyright in FMapOptions) then + begin + if FCopyright = nil then + begin + FCopyright := TBitmap.Create; + DrawCopyright(TilesCopyright, FCopyright); + end; + TransparentBlt(DC, + ClientWidth - FCopyright.Width - LabelMargin, + ClientHeight - FCopyright.Height - LabelMargin, + FCopyright.Width, + FCopyright.Height, + FCopyright.Canvas.Handle, + 0, 0, + FCopyright.Width, + FCopyright.Height, + clWhite); + end; + + // scaleline bitmap must've been inited already in SetZoom + if not (moDontDrawScale in FMapOptions) then + begin + BitBlt(DC, + LabelMargin, + ClientHeight - FScaleLine.Height - LabelMargin, + FScaleLine.Width, + FScaleLine.Height, + FScaleLine.Canvas.Handle, + 0, 0, SRCCOPY); + end; +end; + +// NB: painting on TWinControl is pretty tricky, doing it ordinary way leads +// to weird effects as DC's do not cover whole client area. +// Luckily this could be solved with Invalidate which fully redraws the control +procedure TMapControl.WMHScroll(var Message: TWMHScroll); +begin + Invalidate; + inherited; +end; + +procedure TMapControl.WMVScroll(var Message: TWMVScroll); +begin + Invalidate; + inherited; +end; + +// ! Only with csCustomPaint ControlState the call chain +// TWinControl.WMPaint > PaintHandler > PaintWindow will be executed. +procedure TMapControl.WMPaint(var Message: TWMPaint); +begin + ControlState := ControlState + [csCustomPaint]; + inherited; + ControlState := ControlState - [csCustomPaint]; +end; + +// Reposition cache +procedure TMapControl.Resize; +begin + if SetCacheDimensions then + UpdateCache; + Invalidate; + inherited; +end; + +// Start dragging on mouse press +procedure TMapControl.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); +begin + if FindNextMapMark(ViewToMap(Point(X, Y))) = -1 then + BeginDrag(False, -1); // < 0 - use the DragThreshold property of the global Mouse variable (c) help + inherited; +end; + +// Focus self on mouse press +function TMapControl.MouseActivate(Button: TMouseButton; Shift: TShiftState; X, Y, HitTest: Integer): TMouseActivate; +begin + SetFocus; + Result := inherited; +end; + +// Zoom in/out on mouse wheel +function TMapControl.DoMouseWheel(Shift: TShiftState; WheelDelta: Integer; MousePos: TPoint): Boolean; +begin + inherited; + SetZoom(Zoom + Sign(WheelDelta), ScreenToClient(MousePos)); + Result := True; +end; + +// Process dragging +procedure TMapControl.DragOver(Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); +begin + inherited; + + Accept := True; + + case State of + dsDragEnter: // drag started - save initial drag position + FDragPos := Point(X, Y); + dsDragMove: // dragging - move the map + begin + ScrollMapBy(FDragPos.X - X, FDragPos.Y - Y); + FDragPos := Point(X, Y); + end; + end; +end; + +// *** new methods *** + +// Set zoom level to Value and reposition to given point +// ViewBindPoint - point in view area's coords that must keep its position +procedure TMapControl.SetZoom(Value: Integer; const ViewBindPoint: TPoint); +var + CurrBindPt, NewViewNW: TPoint; + BindCoords: TPointF; +begin + if not (Value in [Low(TMapZoomLevel)..High(TMapZoomLevel)]) then Exit; + if Value = FZoom then Exit; + + // save bind point if zoom is valid (zoom value is used to calc geo coords) + if FZoom in [Low(TMapZoomLevel)..High(TMapZoomLevel)] + then BindCoords := MapToGeoCoords(ViewToMap(ViewBindPoint)) + else BindCoords := OSM.SlippyMapUtils.MapToGeoCoords(Point(0, 0), 0); + + FZoom := Value; + FMapSize.cx := TileCount(FZoom)*TILE_IMAGE_WIDTH; + FMapSize.cy := TileCount(FZoom)*TILE_IMAGE_HEIGHT; + + HorzScrollBar.Range := FMapSize.cx; + VertScrollBar.Range := FMapSize.cy; + + // init copyright bitmap if not inited yet and draw it + if not (moDontDrawScale in FMapOptions) then + begin + if FScaleLine = nil then + FScaleLine := TBitmap.Create; + DrawScale(FZoom, FScaleLine); + end; + + // move viewport + CurrBindPt := GeoCoordsToMap(BindCoords); // bind point in new map coords + NewViewNW := CurrBindPt.Subtract(ViewBindPoint); // view's top-left corner in new map coords + SetNWPoint(NewViewNW); + + SetCacheDimensions; + if not FCacheImageRect.Contains(ViewAreaRect) then + MoveCache; + UpdateCache; // zoom changed - update cache anyway + + Refresh; + + if Assigned(FOnZoomChanged) then + FOnZoomChanged(Self); +end; + +// Simple zoom change with binding to top-left corner +procedure TMapControl.SetZoom(Value: Integer); +begin + SetZoom(Value, Point(0,0)); +end; + +// Determines cache image size according to control and map size +// Returns true if size was changed +function TMapControl.SetCacheDimensions: Boolean; +var + CtrlSize, CacheSize: TSize; +begin + // dims of view area in pixels rounded to full tiles + CtrlSize.cx := ToTileWidthGreater(ClientWidth); + CtrlSize.cy := ToTileHeightGreater(ClientHeight); + + // cache dims = Max(control+margins, Min(map, default+margins)) + CacheSize.cx := Min(FMapSize.cx, CacheImageDefWidth + CacheMarginSize*TILE_IMAGE_WIDTH); + CacheSize.cy := Min(FMapSize.cy, CacheImageDefHeight + CacheMarginSize*TILE_IMAGE_HEIGHT); + + CacheSize.cx := Max(CacheSize.cx, CtrlSize.cx + CacheMarginSize*TILE_IMAGE_WIDTH); + CacheSize.cy := Max(CacheSize.cy, CtrlSize.cy + CacheMarginSize*TILE_IMAGE_HEIGHT); + + Result := (FCacheImageRect.Width <> CacheSize.cx) or (FCacheImageRect.Height <> CacheSize.cy); + if not Result then Exit; + FCacheImageRect.Size := CacheSize; + FCacheImage.SetSize(CacheSize.cx, CacheSize.cy); +end; + +// Recalc point in view area coords to map coords +function TMapControl.ViewToMap(const ViewPt: TPoint): TPoint; +begin + Result := ToOuterCoords(ViewAreaRect.TopLeft, ViewPt); +end; + +// Recalc point in map coords to view area coords +function TMapControl.MapToView(const MapPt: TPoint): TPoint; +begin + Result := ToInnerCoords(ViewAreaRect.TopLeft, MapPt); +end; + +// View area position and size in map coords +function TMapControl.ViewAreaRect: TRect; +begin + Result := ClientRect; + Result.Offset(Point(HorzScrollBar.Position, VertScrollBar.Position)); +end; + +// Whether view area is inside cache image +function TMapControl.ViewInCache: Boolean; +begin + Result := FCacheImageRect.Contains(ViewAreaRect); +end; + +// Fill the cache image +procedure TMapControl.UpdateCache; +var + CanvRect: TRect; + CacheHorzCount, CacheVertCount, horz, vert, CacheHorzNum, CacheVertNum: Cardinal; +begin + // Bounds of cache image in its own coords + CanvRect := FCacheImageRect; + CanvRect.SetLocation(0, 0); + // Clear the image + FCacheImage.Canvas.Brush.Color := Self.Color; + FCacheImage.Canvas.FillRect(CanvRect); + // Get dimensions of cache + CacheHorzCount := Min(FMapSize.cx - FCacheImageRect.Left, FCacheImageRect.Width) div TILE_IMAGE_WIDTH; + CacheVertCount := Min(FMapSize.cy - FCacheImageRect.Top, FCacheImageRect.Height) div TILE_IMAGE_HEIGHT; + // Get top-left of cache in tiles + CacheHorzNum := FCacheImageRect.Left div TILE_IMAGE_WIDTH; + CacheVertNum := FCacheImageRect.Top div TILE_IMAGE_HEIGHT; + // Draw cache tiles + for horz := 0 to CacheHorzCount - 1 do + for vert := 0 to CacheVertCount - 1 do + DoDrawTile(CacheHorzNum + horz, CacheVertNum + vert, Point(horz*TILE_IMAGE_WIDTH, vert*TILE_IMAGE_HEIGHT), FCacheImage); +end; + +// Calc new cache coords to cover current view area +procedure TMapControl.MoveCache; +var + ViewRect: TRect; + MarginH, MarginV: Cardinal; +begin + ViewRect := ViewAreaRect; + // move view rect to the border of tiles (to lesser value) + ViewRect.Left := ToTileWidthLesser(ViewRect.Left); + ViewRect.Top := ToTileHeightLesser(ViewRect.Top); + // resize view rect to the border of tiles (to greater value) + ViewRect.Right := ToTileWidthGreater(ViewRect.Right); + ViewRect.Bottom := ToTileHeightGreater(ViewRect.Bottom); + + // reposition new cache rect to cover tile-aligned view area + // calc margins + MarginH := FCacheImageRect.Width - ViewRect.Width; + MarginV := FCacheImageRect.Height - ViewRect.Height; + // margins on the both sides + if MarginH > TILE_IMAGE_WIDTH then + MarginH := MarginH div 2; + if MarginV > TILE_IMAGE_HEIGHT then + MarginV := MarginV div 2; + FCacheImageRect.SetLocation(ViewRect.TopLeft); + FCacheImageRect.TopLeft.Subtract(Point(MarginH, MarginV)); +end; + +// Draw single tile (TileHorzNum;TileVertNum) +procedure TMapControl.RefreshTile(TileHorzNum, TileVertNum: Cardinal); +var + TileTopLeft: TPoint; +begin + // calc tile rect in map coords + TileTopLeft := Point(TileHorzNum*TILE_IMAGE_WIDTH, TileVertNum*TILE_IMAGE_HEIGHT); + // the tile is not in cache + if not FCacheImageRect.Contains(TileTopLeft) then + Exit; + // convert tile to cache image coords + TileTopLeft.SetLocation(ToInnerCoords(FCacheImageRect.TopLeft, TileTopLeft)); + // draw to cache + DoDrawTile(TileHorzNum, TileVertNum, TileTopLeft, FCacheImage); + // redraw the view + Refresh; +end; + +// Draw single tile (TileHorzNum;TileVertNum) to bitmap DestBmp at point TopLeft +procedure TMapControl.DoDrawTile(TileHorzNum, TileVertNum: Cardinal; const TopLeft: TPoint; DestBmp: TBitMap); +var + TileBmp: TBitmap; +begin + // check if user wants custom draw + if Assigned(OnDrawTile) then + begin + OnDrawTile(Self, TileHorzNum, TileVertNum, TopLeft, DestBmp); + Exit; + end; + // request tile bitmap via callback + TileBmp := nil; + if Assigned(OnGetTile) then + OnGetTile(Self, TileHorzNum, TileVertNum, TileBmp); + // no such tile - draw "loading" + if TileBmp = nil then + begin + if Assigned(FOnDrawTileLoading) then + FOnDrawTileLoading(Self, TileHorzNum, TileVertNum, TopLeft, DestBmp) + else + DrawTileLoading(TileHorzNum, TileVertNum, TopLeft, DestBmp); + end + else + DestBmp.Canvas.Draw(TopLeft.X, TopLeft.Y, TileBmp); +end; + +// Draw single tile (TileHorzNum;TileVertNum) loading to bitmap DestBmp at point TopLeft +procedure TMapControl.DrawTileLoading(TileHorzNum, TileVertNum: Cardinal; const TopLeft: TPoint; DestBmp: TBitMap); +var + TileRect: TRect; + TextExt: TSize; + Canv: TCanvas; + txt: string; +begin + TileRect.TopLeft := TopLeft; + TileRect.Size := TSize.Create(TILE_IMAGE_WIDTH, TILE_IMAGE_HEIGHT); + + Canv := DestBmp.Canvas; + Canv.Brush.Color := Color; + Canv.Pen.Color := clDkGray; + Canv.Rectangle(TileRect); + + txt := Format(SLbl_Loading, [TileHorzNum, TileVertNum]); + TextExt := Canv.TextExtent(txt); + Canv.Font.Color := clGreen; + Canv.TextOut( + TileRect.Left + (TileRect.Width - TextExt.cx) div 2, + TileRect.Top + (TileRect.Height - TextExt.cy) div 2, + txt); +end; + +// Draw copyright label on bitmap and set its size. Happens only once. +class procedure TMapControl.DrawCopyright(const Text: string; DestBmp: TBitmap); +var + Canv: TCanvas; + TextExt: TSize; +begin + Canv := DestBmp.Canvas; + + Canv.Font.Name := 'Arial'; + Canv.Font.Size := 8; + Canv.Font.Style := []; + + TextExt := Canv.TextExtent(Text); + + DestBmp.SetSize(TextExt.cx, TextExt.cy); + + // Text + Canv.Font.Color := clGray; + Canv.TextOut(LabelMargin, LabelMargin, Text); +end; + +// Draw scale line on bitmap and set its size. Happens every zoom change. +class procedure TMapControl.DrawScale(Zoom: TMapZoomLevel; DestBmp: TBitmap); +var + Canv: TCanvas; + LetterWidth, ScalebarWidthPixel, ScalebarWidthMeter: Integer; + Text: string; + TextExt: TSize; + ScalebarRect: TRect; +begin + Canv := DestBmp.Canvas; + + GetScaleBarParams(Zoom, ScalebarWidthPixel, ScalebarWidthMeter, Text); + + Canv.Font.Name := 'Arial'; + Canv.Font.Size := 8; + Canv.Font.Style := []; + + TextExt := Canv.TextExtent(Text); + LetterWidth := Canv.TextWidth('W'); + + DestBmp.Width := LetterWidth + TextExt.cx + LetterWidth + ScalebarWidthPixel; // text, space, bar + DestBmp.Height := 2*LabelMargin + TextExt.cy; + + // Frame + Canv.Brush.Color := clWhite; + Canv.Pen.Color := clSilver; + Canv.Rectangle(0, 0, DestBmp.Width, DestBmp.Height); + + // Text + Canv.Font.Color := clBlack; + Canv.TextOut(LetterWidth div 2, LabelMargin, Text); + + // Scale-Bar + Canv.Brush.Color := clWhite; + Canv.Pen.Color := clBlack; + ScalebarRect.Left := LetterWidth div 2 + TextExt.cx + LetterWidth; + ScalebarRect.Top := (DestBmp.Height - TextExt.cy div 2) div 2; + ScalebarRect.Width := ScalebarWidthPixel; + ScalebarRect.Height := TextExt.cy div 2; + Canv.Rectangle(ScalebarRect); +end; + +// Pixels => degrees +function TMapControl.MapToGeoCoords(const MapPt: TPoint): TPointF; +begin + Result := OSM.SlippyMapUtils.MapToGeoCoords(MapPt, FZoom); +end; + +// Degrees => pixels +function TMapControl.GeoCoordsToMap(const GeoCoords: TPointF): TPoint; +begin + Result := OSM.SlippyMapUtils.GeoCoordsToMap(GeoCoords, FZoom); +end; + +// Delta move the view area +procedure TMapControl.ScrollMapBy(DeltaHorz, DeltaVert: Integer); +begin + Invalidate; // refresh the image + HorzScrollBar.Position := HorzScrollBar.Position + DeltaHorz; + VertScrollBar.Position := VertScrollBar.Position + DeltaVert; +end; + +// Absolutely move the view area +procedure TMapControl.ScrollMapTo(Horz, Vert: Integer); +begin + Invalidate; // refresh the image + HorzScrollBar.Position := Horz; + VertScrollBar.Position := Vert; +end; + +// Move the view area to new top-left point +procedure TMapControl.SetNWPoint(const MapPt: TPoint); +begin + ScrollMapTo(MapPt.X, MapPt.Y); +end; + +{}//? +function TMapControl.GetCenterPoint: TPointF; +begin + Result := MapToGeoCoords(ViewAreaRect.CenterPoint); +end; + +procedure TMapControl.SetCenterPoint(const Coords: TPointF); +var + ViewRect: TRect; + Pt: TPoint; +begin + // new center point in map coords + Pt := GeoCoordsToMap(Coords); + // new NW point + ViewRect := ViewAreaRect; + Pt.Offset(-ViewRect.Width div 2, -ViewRect.Height div 2); + // move + SetNWPoint(Pt); +end; + +// Get top-left point of the view area +function TMapControl.GetNWPoint: TPointF; +begin + Result := MapToGeoCoords(ViewAreaRect.TopLeft); +end; + +// Move the view area to new top-left point +procedure TMapControl.SetNWPoint(const GeoCoords: TPointF); +begin + SetNWPoint(GeoCoordsToMap(GeoCoords)); +end; + +// Find the next map mark that has specified coordinates. +// PrevIndex - index of previous found map mark in the list. -1 (default) to +// start from the 1st element. +// Returns: +// index of map mark in the list, -1 if not found. +// +// Samples: +// 1) Check if there's any map marks at this point +// if FindNextMapMark(Point) <> -1 then ... +// 2) Select all map marks at this point +// idx := -1; +// repeat +// idx := FindNextMapMark(Point, idx); +// if idx = -1 then Break; +// ... do something with MapMarks[idx] ... +// until False; +function TMapControl.FindNextMapMark(const Pt: TPoint; PrevIndex: Integer): Integer; +begin + { + if index = -1 - start searching + if no marks - return -1 + if index > -1 - continue from index + + } + + {} Result := -1; +end; + +end. diff --git a/OSM.NetworkRequest.pas b/OSM.NetworkRequest.pas new file mode 100644 index 0000000..af695ff --- /dev/null +++ b/OSM.NetworkRequest.pas @@ -0,0 +1,263 @@ +{ + Generic (no real network implementation) classes and declarations for + requesting OSM tile images from network. + Real network function from any network must be supplied to actually execute request. +} +unit OSM.NetworkRequest; + +interface + +uses + SysUtils, Classes, Contnrs, + OSM.SlippyMapUtils; + +type + THttpRequestType = (reqPost, reqGet); + + THttpRequestProps = record + RequestType: THttpRequestType; + URL: string; + POSTData: string; + HttpUserName: string; + HttpPassword: string; + HeaderLines: TStrings; + Additional: Pointer; + end; + + // Generic type of blocking network request function + // RequestProps - all details regarding a request + // ResponseStm - stream that accepts response data + // ErrMsg - error description if any. + // Returns: success flag + TBlockingNetworkRequestFunc = function (const RequestProps: THttpRequestProps; + const ResponseStm: TStream; out ErrMsg: string): Boolean; + + // Generic type of method to call when request is completed + // ! Called from the context of a background thread ! + TGotTileFromNetworkCallback = procedure (const Tile: TTile; Ms: TMemoryStream; const Error: string) of object; + + // Queuer of network requests + TNetworkRequestQueue = class + strict private + FTaskQueue: TQueue; // list of tiles to be requested + FThreads: TList; + FCurrentTasks: TList; // list of tiles that are requested but not yet received + FNotEmpty: Boolean; + + FMaxTasksPerThread: Cardinal; + FMaxThreads: Cardinal; + FGotTileCb: TGotTileFromNetworkCallback; + FRequestFunc: TBlockingNetworkRequestFunc; + procedure Lock; + procedure Unlock; + procedure AddThread; + private + // for access from TNetworkRequestThread + procedure DoRequestComplete(Sender: TThread; const Tile: TTile; Ms: TMemoryStream; const Error: string); + function PopTask: Pointer; + + property NotEmpty: Boolean read FNotEmpty; + property RequestFunc: TBlockingNetworkRequestFunc read FRequestFunc; + public + constructor Create(MaxTasksPerThread, MaxThreads: Cardinal; + RequestFunc: TBlockingNetworkRequestFunc; + GotTileCb: TGotTileFromNetworkCallback); + destructor Destroy; override; + + procedure RequestTile(const Tile: TTile); + end; + +implementation + +type + // Thread that consumes tasks from owner's queue and executes them + // When there are no tasks in the queue, it finishes and must be destroyed + TNetworkRequestThread = class(TThread) + strict private + FOwner: TNetworkRequestQueue; + public + constructor Create(Owner: TNetworkRequestQueue); + procedure Execute; override; + end; + +{ TNetworkRequestThread } + +constructor TNetworkRequestThread.Create(Owner: TNetworkRequestQueue); +begin + FOwner := Owner; + inherited Create(False); +end; + +procedure TNetworkRequestThread.Execute; +var + pT: PTile; + tile: TTile; + sURL, sErrMsg: string; + ms: TMemoryStream; + ReqProps: THttpRequestProps; +begin + ReqProps := Default(THttpRequestProps); + ReqProps.RequestType := reqGet; + + while not Terminated do + begin + pT := PTile(FOwner.PopTask); + if pT <> nil then + begin + tile := pT^; + sURL := TileToFullSlippyMapFileURL(tile); + ms := TMemoryStream.Create; + ReqProps.URL := sURL; + if not FOwner.RequestFunc(ReqProps, ms, sErrMsg) then + FreeAndNil(ms) + else + ms.Position := 0; + + FOwner.DoRequestComplete(Self, tile, ms, sErrMsg); + end; + end; +end; + +{ TNetworkRequestQueue } + +// MaxTasksPerThread - if TaskCount > MaxTasksPerThread*ThreadCount, add one more thread +// MaxThreads - limit the number of threads +// GotTileCb - method to call when request is completed +constructor TNetworkRequestQueue.Create(MaxTasksPerThread, MaxThreads: Cardinal; + RequestFunc: TBlockingNetworkRequestFunc; GotTileCb: TGotTileFromNetworkCallback); +begin + FTaskQueue := TQueue.Create; + FThreads := TList.Create; + FCurrentTasks := TList.Create; + FMaxTasksPerThread := MaxTasksPerThread; + FMaxThreads := MaxThreads; + FGotTileCb := GotTileCb; + FRequestFunc := RequestFunc; +end; + +destructor TNetworkRequestQueue.Destroy; +var i: Integer; +begin + // Command the threads to stop, wait and destroy them + for i := 0 to FThreads.Count - 1 do + TThread(FThreads[i]).Terminate; + for i := 0 to FThreads.Count - 1 do + TThread(FThreads[i]).WaitFor; + for i := 0 to FThreads.Count - 1 do + if TThread(FThreads[i]).Finished then + TThread(FThreads[i]).Free + else + raise Exception.Create('Thread was not finished'); + FreeAndNil(FThreads); + + // Data cleanup + while FTaskQueue.Count > 0 do + Dispose(PTile(FTaskQueue.Pop)); + FreeAndNil(FTaskQueue); + for i := 0 to FCurrentTasks.Count - 1 do + Dispose(PTile(FCurrentTasks[0])); + FreeAndNil(FCurrentTasks); +end; + +procedure TNetworkRequestQueue.Lock; +begin + System.TMonitor.Enter(Self); +end; + +procedure TNetworkRequestQueue.Unlock; +begin + System.TMonitor.Exit(Self); +end; + +function IndexOfTile(const Tile: TTile; List: TList): Integer; +begin + for Result := 0 to List.Count - 1 do + if TilesEqual(Tile, PTile(List[Result])^) then + Exit; + Result := -1; +end; + +type + TQueueHack = class(TQueue) end; + +procedure TNetworkRequestQueue.RequestTile(const Tile: TTile); +var + pT: PTile; +begin + Lock; + try + // check if tile already in process + if IndexOfTile(Tile, FCurrentTasks) <> -1 then + Exit; + // or in queue + if IndexOfTile(Tile, TQueueHack(FTaskQueue).List) <> -1 then + Exit; + + New(pT); + pT^ := Tile; + FTaskQueue.Push(pT); + FNotEmpty := True; + if (FTaskQueue.Count > FMaxTasksPerThread*FThreads.Count) and + (FThreads.Count < FMaxThreads) then + AddThread; + finally + Unlock; + end; +end; + +procedure TNetworkRequestQueue.AddThread; +begin + Lock; + try + FThreads.Add(TNetworkRequestThread.Create(Self)); + finally + Unlock; + end; +end; + +// Extract next item from queue +// ! Executed from bg threads +function TNetworkRequestQueue.PopTask: Pointer; +begin + // Fast check + if not NotEmpty then + Exit(nil); + + Lock; + try + if FTaskQueue.Count > 0 then + begin + Result := FTaskQueue.Pop; + FCurrentTasks.Add(Result); + end + else + Result := nil; + FNotEmpty := (FTaskQueue.Count > 0); + finally + Unlock; + end; +end; + +// Network request complete +// ! Executed from bg threads +procedure TNetworkRequestQueue.DoRequestComplete(Sender: TThread; const Tile: TTile; Ms: TMemoryStream; const Error: string); +var + idx: Integer; +begin + Lock; + try + idx := IndexOfTile(Tile, FCurrentTasks); + Dispose(PTile(FCurrentTasks[idx])); + FCurrentTasks.Delete(idx); + if Sender.Finished then + begin + Sender.Free; + FThreads.Delete(FThreads.IndexOf(Sender)); + end; + finally + Unlock; + end; + FGotTileCb(Tile, Ms, Error); +end; + +end. diff --git a/OSM.SlippyMapUtils.pas b/OSM.SlippyMapUtils.pas new file mode 100644 index 0000000..c9a143a --- /dev/null +++ b/OSM.SlippyMapUtils.pas @@ -0,0 +1,267 @@ +{ + OSM map types & functions. + Ref.: https://wiki.openstreetmap.org/wiki/Slippy_Map + + based on unit by Simon Kroik, 06.2018, kroiksm@gmx.de + which is based on UNIT openmap.pas + https://github.com/norayr/meridian23/blob/master/openmap/openmap.pas + New BSD License +} +unit OSM.SlippyMapUtils; + +interface + +uses Types, SysUtils, Math; + +type + TMapZoomLevel = 0..19; // 19 = Maximum zoom for Mapnik layer + + TTile = record + Zoom: TMapZoomLevel; + ParameterX: Integer; + ParameterY: Integer; + end; + PTile = ^TTile; + +const + TILE_IMAGE_WIDTH = 256; + TILE_IMAGE_HEIGHT = 256; + // https://wiki.openstreetmap.org/wiki/Zoom_levels + TileMetersPerPixelOnEquator: array [TMapZoomLevel] of Double = + ( + 156412, + 78206, + 39103, + 19551, + 9776, + 4888, + 2444, + 1222, + 610.984, + 305.492, + 152.746, + 76.373, + 38.187, + 19.093, + 9.547, + 4.773, + 2.387, + 1.193, + 0.596, + 0.298 + ); + +var // configurable + TilesCopyright: string = '(c) OpenStreetMap contributors'; + MapURLPrefix: string = 'http://tile.openstreetmap.org/'; + MapURLPostfix: string = ''; + +function TileCount(Zoom: TMapZoomLevel): Integer; inline; +function TileValid(const Tile: TTile): Boolean; inline; +function TileToStr(const Tile: TTile): string; +function TilesEqual(const Tile1, Tile2: TTile): Boolean; inline; + +function LongitudeToMapCoord(Longitude: Double; Zoom: TMapZoomLevel): Integer; +function LatitudeToMapCoord(Latitude: Double; Zoom: TMapZoomLevel): Integer; +function MapCoordToLongitude(X: Integer; Zoom: TMapZoomLevel): Double; +function MapCoordToLatitude(Y: Integer; Zoom: TMapZoomLevel): Double; +function MapToGeoCoords(const MapPt: TPoint; Zoom: TMapZoomLevel): TPointF; +function GeoCoordsToMap(const GeoCoords: TPointF; Zoom: TMapZoomLevel): TPoint; + +function CalcLinDistanceInMeter(const Coord1, Coord2: TPointF): Double; +procedure GetScaleBarParams(Zoom: TMapZoomLevel; + var ScalebarWidthInPixel: Integer; var ScalebarWidthInMeter: Integer; + var Text: string); + +function TileToSlippyMapFileSubURL(const Tile: TTile): string; +function TileToSlippyMapFileSubPath(const Tile: TTile): string; +function TileToFullSlippyMapFileURL(const Tile: TTile): string; + +implementation + +// Tile utils + +// Tile count on level is 2^Zoom +function TileCount(Zoom: TMapZoomLevel): Integer; +begin + Result := 1 shl Zoom; +end; + +// Check tile fields for validity +function TileValid(const Tile: TTile): Boolean; +begin + Result := + (Tile.Zoom in [Low(TMapZoomLevel)..High(TMapZoomLevel)]) and + (Tile.ParameterX >= 0) and (Tile.ParameterX < TileCount(Tile.Zoom)) and + (Tile.ParameterY >= 0) and (Tile.ParameterY < TileCount(Tile.Zoom)); +end; + +// Just a standartized string representation +function TileToStr(const Tile: TTile): string; +begin + Result := Format('%d * [%d : %d]', [Tile.Zoom, Tile.ParameterX, Tile.ParameterY]); +end; + +function TilesEqual(const Tile1, Tile2: TTile): Boolean; +begin + Result := + (Tile1.Zoom = Tile2.Zoom) and + (Tile1.ParameterX = Tile2.ParameterX) and + (Tile1.ParameterY = Tile2.ParameterY); +end; + +// Degrees to pixels + +function LongitudeToMapCoord(Longitude: Double; Zoom: TMapZoomLevel): Integer; +begin + Result := Floor((Longitude + 180.0) / 360.0 * TileCount(Zoom)*TILE_IMAGE_WIDTH); +end; + +function LatitudeToMapCoord(Latitude: Double; Zoom: TMapZoomLevel): Integer; +var + SavePi: Extended; + LatInRad: Extended; +begin + SavePi := Pi; + LatInRad := Latitude * SavePi / 180.0; + Result := Floor((1.0 - ln(Tan(LatInRad) + 1.0 / Cos(LatInRad)) / SavePi) / 2.0 * TileCount(Zoom)*TILE_IMAGE_HEIGHT); +end; + +function GeoCoordsToMap(const GeoCoords: TPointF; Zoom: TMapZoomLevel): TPoint; +begin + Result := Point( + LongitudeToMapCoord(GeoCoords.X, Zoom), + LatitudeToMapCoord(GeoCoords.Y, Zoom) + ); +end; + +// Pixels to degrees + +function MapCoordToLongitude(X: Integer; Zoom: TMapZoomLevel): Double; +begin + Result := X / (TileCount(Zoom)*TILE_IMAGE_WIDTH) * 360.0 - 180.0; +end; + +function MapCoordToLatitude(Y: Integer; Zoom: TMapZoomLevel): Double; +var + n: Extended; + SavePi: Extended; +begin + SavePi := Pi; + n := SavePi - 2.0 * SavePi * Y / (TileCount(Zoom)*TILE_IMAGE_HEIGHT); + + Result := 180.0 / SavePi * ArcTan(0.5 * (Exp(n) - Exp(-n))); +end; + +function MapToGeoCoords(const MapPt: TPoint; Zoom: TMapZoomLevel): TPointF; +begin + Result := PointF( + MapCoordToLongitude(MapPt.X, Zoom), + MapCoordToLatitude(MapPt.Y, Zoom) + ); +end; + +// Other + +function CalcLinDistanceInMeter(const Coord1, Coord2: TPointF): Double; +var + Phimean: Double; + dLambda: Double; + dPhi: Double; + Alpha: Double; + Rho: Double; + Nu: Double; + R: Double; + z: Double; + Temp: Double; +const + D2R: Double = 0.017453; + R2D: Double = 57.295781; + a: Double = 6378137.0; + b: Double = 6356752.314245; + e2: Double = 0.006739496742337; + f: Double = 0.003352810664747; +begin + dLambda := (Coord1.X - Coord2.X) * D2R; + dPhi := (Coord1.Y - Coord2.Y) * D2R; + Phimean := ((Coord1.Y + Coord2.Y) / 2.0) * D2R; + + Temp := 1 - e2 * Sqr(Sin(Phimean)); + Rho := (a * (1 - e2)) / Power(Temp, 1.5); + Nu := a / (Sqrt(1 - e2 * (Sin(Phimean) * Sin(Phimean)))); + + z := Sqrt(Sqr(Sin(dPhi / 2.0)) + Cos(Coord2.Y * D2R) * + Cos(Coord1.Y * D2R) * Sqr(Sin(dLambda / 2.0))); + + z := 2 * ArcSin(z); + + Alpha := Cos(Coord2.Y * D2R) * Sin(dLambda) * 1 / Sin(z); + Alpha := ArcSin(Alpha); + + R := (Rho * Nu) / (Rho * Sqr(Sin(Alpha)) + (Nu * Sqr(Cos(Alpha)))); + + Result := (z * R); +end; + +procedure GetScaleBarParams(Zoom: TMapZoomLevel; var ScalebarWidthInPixel, ScalebarWidthInMeter: Integer; var Text: string); +const + ScalebarWidthInKm: array [TMapZoomLevel] of Double = + ( + 10000, + 5000, + 3000, + 1000, + 500, + 300, + 200, + 100, + 50, + 30, + 10, + 5, + 3, + 1, + 0.500, + 0.300, + 0.200, + 0.100, + 0.050, + 0.020 + ); +var + dblScalebarWidthInMeter: Double; +begin + dblScalebarWidthInMeter := ScalebarWidthInKm[Zoom] * 1000; + ScalebarWidthInPixel := Round(dblScalebarWidthInMeter / TileMetersPerPixelOnEquator[Zoom]); + ScalebarWidthInMeter := Round(dblScalebarWidthInMeter); + + if ScalebarWidthInMeter < 1000 then + Text := IntToStr(ScalebarWidthInMeter) + ' m' + else + Text := IntToStr(ScalebarWidthInMeter div 1000) + ' km' +end; + +// Tile path + +function TileToSlippyMapFileSubURL(const Tile: TTile): string; +begin + Result := + IntToStr(Tile.Zoom) + '/' + + IntToStr(Tile.ParameterX) + '/' + + IntToStr(Tile.ParameterY) + '.png'; +end; + +function TileToSlippyMapFileSubPath(const Tile: TTile): string; +begin + Result := + IntToStr(Tile.Zoom) + PathDelim + + IntToStr(Tile.ParameterX) + PathDelim + + IntToStr(Tile.ParameterY) + '.png'; +end; + +function TileToFullSlippyMapFileURL(const Tile: TTile): string; +begin + Result := MapURLPrefix + TileToSlippyMapFileSubURL(Tile) + MapURLPostfix; +end; + +end. diff --git a/OSM.TileStorage.pas b/OSM.TileStorage.pas new file mode 100644 index 0000000..6a3ad70 --- /dev/null +++ b/OSM.TileStorage.pas @@ -0,0 +1,206 @@ +{ + OSM tile images cache. + Stores tile images for the map, could read/save them from/to local files but + doesn't request them from network. See OSM.NetworkRequest unit +} +unit OSM.TileStorage; + +interface + +uses + SysUtils, Classes, Graphics, PngImage, + OSM.SlippyMapUtils; + +const + // Amount of bytes that a single tile bitmap occupies in memory. + // Bitmap consumes ~4 byte per pixel. This constant could be used to + // determine acceptable cache size knowing acceptable memory usage. + TILE_BITMAP_SIZE = 4*TILE_IMAGE_WIDTH*TILE_IMAGE_HEIGHT; + +type + // List of cached tile bitmaps with fixed capacity organised as queue + TTileBitmapCache = class + strict private + type + TTileBitmapRec = record + Tile: TTile; + Bmp: TBitmap; + end; + PTileBitmapRec = ^TTileBitmapRec; + strict private + FCache: TList; + class function NewItem(const Tile: TTile; Bmp: TBitmap): PTileBitmapRec; + class procedure FreeItem(pItem: PTileBitmapRec); + public + constructor Create(Capacity: Integer); + destructor Destroy; override; + procedure Push(const Tile: TTile; Bmp: TBitmap); + function Find(const Tile: TTile): TBitmap; + end; + + TTileStorageOption = ( + tsoNoFileCache, // disable all file cache operations + tsoReadOnlyFileCache // disable write file cache operations + ); + TTileStorageOptions = set of TTileStorageOption; + + // Class that encapsulates memory and file cache of tile images + TTileStorage = class + strict private + FBmpCache: TTileBitmapCache; + FFileCacheBaseDir: string; + FOptions: TTileStorageOptions; + + function GetFromFileCache(const Tile: TTile): TBitmap; + procedure StoreInFileCache(const Tile: TTile; Ms: TMemoryStream); + public + constructor Create(CacheSize: Integer); + destructor Destroy; override; + function GetTile(const Tile: TTile): TBitmap; + procedure StoreTile(const Tile: TTile; Ms: TMemoryStream); + + property Options: TTileStorageOptions read FOptions write FOptions; + // Base path to images on disk: \ \ \ .png + property FileCacheBaseDir: string read FFileCacheBaseDir write FFileCacheBaseDir; + end; + +implementation + +{$REGION 'TTileBitmapCache'} + +class function TTileBitmapCache.NewItem(const Tile: TTile; Bmp: TBitmap): PTileBitmapRec; +begin + New(Result); + Result.Tile := Tile; + Result.Bmp := Bmp; +end; + +class procedure TTileBitmapCache.FreeItem(pItem: PTileBitmapRec); +begin + pItem.Bmp.Free; + Dispose(pItem); +end; + +constructor TTileBitmapCache.Create(Capacity: Integer); +begin + FCache := TList.Create; + FCache.Capacity := Capacity; +end; + +destructor TTileBitmapCache.Destroy; +begin + while FCache.Count > 0 do + begin + FreeItem(PTileBitmapRec(FCache[0])); + FCache.Delete(0); + end; + FreeAndNil(FCache); +end; + +procedure TTileBitmapCache.Push(const Tile: TTile; Bmp: TBitmap); +begin + if FCache.Count = FCache.Capacity then + begin + FreeItem(PTileBitmapRec(FCache[0])); + FCache.Delete(0); + end; + FCache.Add(NewItem(Tile, Bmp)); +end; + +function TTileBitmapCache.Find(const Tile: TTile): TBitmap; +var idx: Integer; +begin + for idx := 0 to FCache.Count - 1 do + if TilesEqual(Tile, PTileBitmapRec(FCache[idx]).Tile) then + Exit(PTileBitmapRec(FCache[idx]).Bmp); + Result := nil; +end; + +{$ENDREGION} + +{$REGION 'TTileStorage'} + +// CacheSize - capacity of image cache. +constructor TTileStorage.Create(CacheSize: Integer); +begin + FBmpCache := TTileBitmapCache.Create(CacheSize); +end; + +destructor TTileStorage.Destroy; +begin + FreeAndNil(FBmpCache); +end; + +function TTileStorage.GetFromFileCache(const Tile: TTile): TBitmap; +var + png: TPngImage; + Path: string; +begin + Result := nil; + Path := FFileCacheBaseDir + TileToSlippyMapFileSubPath(Tile); + if FileExists(Path) then + begin + png := TPngImage.Create; + png.LoadFromFile(Path); + Result := TBitmap.Create; + Result.Assign(png); + FreeAndNil(png); + end; +end; + +procedure TTileStorage.StoreInFileCache(const Tile: TTile; Ms: TMemoryStream); +var + Path: string; +begin + Path := FFileCacheBaseDir + TileToSlippyMapFileSubPath(Tile); + ForceDirectories(ExtractFileDir(Path)); + Ms.SaveToFile(Path); +end; + +// Try to get tile bitmap, return nil if not available locally. +// If bitmap has been loaded from file, store it in cache +function TTileStorage.GetTile(const Tile: TTile): TBitmap; +begin + // try to load from memory cache + Result := FBmpCache.Find(Tile); + if Result <> nil then + Exit; + + // try to load from disk cache + if not (tsoNoFileCache in FOptions) then + begin + Result := GetFromFileCache(Tile); + if Result <> nil then + FBmpCache.Push(Tile, Result); + end; +end; + +// Add tile PNG to memory and file cache +procedure TTileStorage.StoreTile(const Tile: TTile; Ms: TMemoryStream); +var + png: TPngImage; + bmp: TBitmap; + SavePos: Int64; +begin + png := nil; + try + SavePos := Ms.Position; + // Save to disk as PNG + if ([tsoNoFileCache, tsoReadOnlyFileCache] * FOptions = []) then + StoreInFileCache(Tile, Ms); + Ms.Position := SavePos; + // Convert to bitmap and store in memory cache + png := TPngImage.Create; + png.LoadFromStream(Ms); + Ms.Position := SavePos; + bmp := TBitmap.Create; + bmp.Assign(png); + FBmpCache.Push(Tile, Bmp); + finally + FreeAndNil(png); + end; +end; + +{$ENDREGION} + +end. diff --git a/README.md b/README.md new file mode 100644 index 0000000..045e05d --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Компонент для отображения карты OpenStreetMap и вспомогательные классы +Демо-проект включает механизм для скачивания карты с сети + +Совместимость: Delphi XE2+, VCL, Windows (на данный момент) + +!! Версия alpha, всё может меняться произвольным образом !! diff --git a/SynapseRequest.pas b/SynapseRequest.pas new file mode 100644 index 0000000..faf58ff --- /dev/null +++ b/SynapseRequest.pas @@ -0,0 +1,85 @@ +{ + Implements blocking HTTP request with Synapse framework. + + based on code by Simon Kroik, 06.2018, kroiksm@gmx.de +} +unit SynapseRequest; + +interface + +uses + SysUtils, Classes, + HTTPSend, SynaUtil, + OSM.NetworkRequest; + +// RequestProps.Additional: Boolean - SendAsMozilla flag +function NetworkRequest(const RequestProps: THttpRequestProps; + const ResponseStm: TStream; out ErrMsg: string): Boolean; + +implementation + +const + SEMsg_HTTPErr = 'HTTP error: %d %s'; + +procedure PrepareHTTPSendAsMozilla(AHTTP: THTTPSend); +begin + AHTTP.UserAgent:='Mozilla/5.0 (Windows NT 6.1; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0'; + AHTTP.Headers.Add('Accept-Language: de,en-US;q=0.7,en;q=0.3'); + AHTTP.Headers.Add('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'); +end; + +//------------------------------------------------------------------------------ +//based on Synapse +//For HTTPS-Support: +// 1) USES ssl_openssl; +// 2) copy libeay32.dll +// 3) copy ssleay32.dll +function NetworkRequest(const RequestProps: THttpRequestProps; + const ResponseStm: TStream; out ErrMsg: string): Boolean; +var + HTTP: THTTPSend; +begin + ErrMsg := ''; + + HTTP := THTTPSend.Create; + try + HTTP.UserName := RequestProps.HttpUserName; + HTTP.Password := RequestProps.HttpPassword; + + if RequestProps.RequestType = reqPost then + begin + WriteStrToStream(HTTP.Document, RawByteString(RequestProps.POSTData)); + HTTP.MimeType := 'application/x-www-form-urlencoded'; + end; + + if Boolean(RequestProps.Additional) then + PrepareHTTPSendAsMozilla(HTTP); + + if Assigned(RequestProps.HeaderLines) then + HTTP.Headers.AddStrings(RequestProps.HeaderLines); + + if RequestProps.RequestType = reqPost then + Result := HTTP.HTTPMethod('POST', RequestProps.URL) + else + Result := HTTP.HTTPMethod('GET', RequestProps.URL); + + // check network error + if not Result then + begin + ErrMsg := HTTP.Sock.LastErrorDesc; + Exit; + end; + // check HTTP error + if HTTP.ResultCode <> 200 then + begin + ErrMsg := Format(SEMsg_HTTPErr, [HTTP.ResultCode, HTTP.ResultString]); + Exit; + end; + // OK + ResponseStm.CopyFrom(HTTP.Document, 0); + finally + FreeAndNil(HTTP); + end; +end; + +end. diff --git a/WinInetRequest.pas b/WinInetRequest.pas new file mode 100644 index 0000000..cb03cee --- /dev/null +++ b/WinInetRequest.pas @@ -0,0 +1,65 @@ +{ + Implements blocking HTTP request with WinInet. +} +unit WinInetRequest; + +interface + +uses + SysUtils, Classes, Windows, WinInet, + OSM.NetworkRequest; + +// Only GET requests. No auth fields used. +function NetworkRequest(const RequestProps: THttpRequestProps; + const ResponseStm: TStream; out ErrMsg: string): Boolean; + +implementation + +const + SEMsg_UnsuppReqType = 'Only GET request type supported'; + +function NetworkRequest(const RequestProps: THttpRequestProps; + const ResponseStm: TStream; out ErrMsg: string): Boolean; +var + hInet: HINTERNET; + Headers: string; + Buf: array[0..1024-1] of Byte; + read: DWORD; + hFile: HINTERNET; +begin + ErrMsg := ''; Result := False; hInet := nil; hFile := nil; + + try try + if RequestProps.RequestType <> reqGet then + raise Exception.Create(SEMsg_UnsuppReqType); + // Init WinInet + hInet := InternetOpen('Foo', INTERNET_OPEN_TYPE_DIRECT, nil, nil, 0); + if hInet = nil then + raise Exception.Create(SysErrorMessage(GetLastError)); + // Open address + if RequestProps.HeaderLines <> nil then + Headers := RequestProps.HeaderLines.Text; + hFile := InternetOpenUrl(hInet, PChar(RequestProps.URL), PChar(Headers), 0, + INTERNET_FLAG_NO_CACHE_WRITE or INTERNET_FLAG_NO_COOKIES or INTERNET_FLAG_NO_UI, + 0); + if hFile = nil then + raise Exception.Create(SysErrorMessage(GetLastError)); + + // Read the URL + while InternetReadFile(hFile, @Buf, SizeOf(Buf), read) do + begin + if read = 0 then Break; + ResponseStm.Write(Buf, read); + end; + + Result := True; + except on E: Exception do + ErrMsg := E.Message; + end; + finally + InternetCloseHandle(hFile); + InternetCloseHandle(hInet); + end; +end; + +end.