Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/core/main.pas
Original file line number Diff line number Diff line change
Expand Up @@ -858,8 +858,12 @@ procedure TMainForm.InitFormContents(FileName: String; WebMapSource: Integer);
UpdateStatusBar(sbXY, '');

MainMenuFrame.InitMapThemes;
inifile.ReadLegendIntervals(ChangeFileExt(FileName, '.ini'));
MruMenuMgr.AddToRecent(FileName);
MapFrame.LoadBasemapFromWeb(WebMapSource, project.MapEPSG, project.MapUnits);
inifile.ReadBasemapStyle(ChangeFileExt(FileName, '.ini'));
MainMenuFrame.BasemapGrayscaleItem.Checked := MapFrame.Map.Basemap.Grayscale;
MainMenuFrame.BasemapLightenItem.Checked := (MapFrame.Map.Basemap.Brightness > 0);
MapFrame.DrawFullExtent;

end;
Expand Down
1 change: 1 addition & 0 deletions src/core/mapframe.pas
Original file line number Diff line number Diff line change
Expand Up @@ -1676,6 +1676,7 @@ function TMapFrame.WGS84Transform(Epsg: Integer; var Extent: TDoubleRect): Boole
if mapcoords.CanProjectionTransform(IntToStr(Epsg), '4326', Extent) then
begin
MainForm.Cursor := crHourGlass;
if not mapcoords.HasCachedNodeCoords then mapcoords.CacheNativeNodeCoords;
Transformed := mapcoords.DoProjectionTransform(IntToStr(Epsg),
'4326', Extent);
MainForm.Cursor := crDefault;
Expand Down
130 changes: 129 additions & 1 deletion src/map/mapcoords.pas
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ function FromWGS84ToWebMercator(LatLng: TDoublePoint): TDoublePoint;

function ManhattanDistance(P1, P2: TDoublePoint): Double;

// Caches each node's coordinates in the project's native (non-WGS84)
// coordinate system, taken BEFORE any transform to WGS84 for basemap
// display happens. This lets Save() write out the pristine original
// coordinates every time, instead of round-tripping through PROJ on
// every save (which accumulates rounding drift).
procedure CacheNativeNodeCoords;

function HasCachedNodeCoords: Boolean;

procedure GetCachedNodeCoord(Index: Integer; var X, Y: Double);

function GetCachedVertexCount(LinkIndex: Integer): Integer;

procedure GetCachedVertexCoord(LinkIndex, VertexIndex: Integer; var X, Y: Double);

procedure GetCachedLabelCoord(Index: Integer; var X, Y: Double);

implementation

uses
Expand All @@ -77,6 +94,14 @@ implementation
var
S1: TScalingInfo;
S2: TScalingInfo; // Used for scaling transform
CachedNodeX: array of Double; // Cached native-CRS node X coords
CachedNodeY: array of Double; // Cached native-CRS node Y coords
CachedVertexX: array of array of Double; // [LinkIndex][VertexIndex]
CachedVertexY: array of array of Double;
CachedVertexCount: array of Integer; // [LinkIndex]
CachedLabelX: array of Double;
CachedLabelY: array of Double;
NodeCoordsCached: Boolean = false;
Ax: Double; // Used for affine transform
Ay: Double;
Bx: Double;
Expand Down Expand Up @@ -209,8 +234,20 @@ function ApplyAffineTransform(X, Y: Double): TDoublePoint;
end;

function ApplyProjectionTransform(var X, Y: Double): TDoublePoint;
var
OrigX, OrigY: Double;
begin
ProjTrans.Transform(X,Y);
// Keep a copy of the original coordinates. PROJ's pj_transform can
// write HUGE_VAL (Infinity) directly into X/Y *even when it fails*,
// so we cannot trust X/Y after a failed call - restore the originals.
OrigX := X;
OrigY := Y;
if (not ProjTrans.Transform(X,Y))
or IsInfinite(X) or IsInfinite(Y) or IsNan(X) or IsNan(Y) then
begin
X := OrigX;
Y := OrigY;
end;
Result.X := X;
Result.Y := Y;
end;
Expand Down Expand Up @@ -497,4 +534,95 @@ function ManhattanDistance(P1, P2: TDoublePoint): Double;
Result := Abs(P2.X - P1.X) + Abs(P2.Y - P1.Y);
end;

procedure CacheNativeNodeCoords;
var
I, J: Integer;
N: Integer;
Vcount: Integer;
X, Y: Double;
begin
// Nodes
N := project.GetItemCount(ctNodes);
SetLength(CachedNodeX, N+1);
SetLength(CachedNodeY, N+1);
for I := 1 to N do
begin
if project.GetNodeCoord(I, X, Y) then
begin
CachedNodeX[I] := X;
CachedNodeY[I] := Y;
end;
end;

// Link vertices (interior bend points)
N := project.GetItemCount(ctLinks);
SetLength(CachedVertexX, N+1);
SetLength(CachedVertexY, N+1);
SetLength(CachedVertexCount, N+1);
for I := 1 to N do
begin
Vcount := project.GetVertexCount(I);
CachedVertexCount[I] := Vcount;
SetLength(CachedVertexX[I], Vcount+1);
SetLength(CachedVertexY[I], Vcount+1);
for J := 1 to Vcount do
project.GetVertexCoord(I, J, CachedVertexX[I][J], CachedVertexY[I][J]);
end;

// Map labels
N := project.GetItemCount(ctLabels);
SetLength(CachedLabelX, N+1);
SetLength(CachedLabelY, N+1);
for I := 1 to N do
begin
if project.GetLabelCoord(I, X, Y) then
begin
CachedLabelX[I] := X;
CachedLabelY[I] := Y;
end;
end;

NodeCoordsCached := true;
end;

function HasCachedNodeCoords: Boolean;
begin
Result := NodeCoordsCached and (Length(CachedNodeX) > 0);
end;

procedure GetCachedNodeCoord(Index: Integer; var X, Y: Double);
begin
if NodeCoordsCached and (Index >= 0) and (Index <= High(CachedNodeX)) then
begin
X := CachedNodeX[Index];
Y := CachedNodeY[Index];
end;
end;

function GetCachedVertexCount(LinkIndex: Integer): Integer;
begin
Result := 0;
if NodeCoordsCached and (LinkIndex >= 0) and (LinkIndex <= High(CachedVertexCount)) then
Result := CachedVertexCount[LinkIndex];
end;

procedure GetCachedVertexCoord(LinkIndex, VertexIndex: Integer; var X, Y: Double);
begin
if NodeCoordsCached and (LinkIndex >= 0) and (LinkIndex <= High(CachedVertexX))
and (VertexIndex >= 0) and (VertexIndex <= High(CachedVertexX[LinkIndex])) then
begin
X := CachedVertexX[LinkIndex][VertexIndex];
Y := CachedVertexY[LinkIndex][VertexIndex];
end;
end;

procedure GetCachedLabelCoord(Index: Integer; var X, Y: Double);
begin
if NodeCoordsCached and (Index >= 0) and (Index <= High(CachedLabelX)) then
begin
X := CachedLabelX[Index];
Y := CachedLabelY[Index];
end;
end;

end.
92 changes: 91 additions & 1 deletion src/project/inifile.pas
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ procedure WriteAppDefaults(FileName: string);
procedure ReadProjectDefaults(FileName: string; var WebMapSource: Integer);
procedure WriteProjectDefaults(FileName: string; WebMapSource: Integer);
procedure WriteProjectMapOptions(FileName: string; WebMapSource:Integer);
procedure ReadLegendIntervals(FileName: string);
procedure ReadBasemapStyle(FileName: string);

implementation

Expand Down Expand Up @@ -245,7 +247,7 @@ procedure WriteProjectDefaults(FileName: string; WebMapSource: Integer);

procedure WriteProjectMapOptions(FileName: string; WebMapSource:Integer);
var
I: Integer;
I, J: Integer;
Ini: TIniFile;
begin
Ini := TIniFile.Create(FileName);
Expand All @@ -263,16 +265,104 @@ procedure WriteProjectMapOptions(FileName: string; WebMapSource:Integer);
Ini.WriteString('MAP', 'BACKCOLOR', ColorToString(BackColor));
end;
Ini.WriteInteger('MAP', 'WEBMAPSOURCE', WebMapSource);
Ini.WriteBool('MAP', 'GRAYSCALE', MainForm.MapFrame.Map.Basemap.Grayscale);
Ini.WriteInteger('MAP', 'BRIGHTNESS', MainForm.MapFrame.Map.Basemap.Brightness);

for I := Low(mapthemes.NodeColors) to High(mapthemes.NodeColors) do
Ini.WriteString('LEGENDS', 'NODE' + IntToStr(I),
ColorToString(mapthemes.NodeColors[I]));
for I := Low(mapthemes.LinkColors) to High(mapthemes.LinkColors) do
Ini.WriteString('LEGENDS', 'LINK' + IntToStr(I),
ColorToString(mapthemes.LinkColors[I]));

// Legend interval thresholds (labels + values), one INI section per theme
for I := 0 to Length(mapthemes.NodeIntervals)-1 do
for J := 1 to mapthemes.MAXLEVELS do
begin
Ini.WriteString('NODE_INTERVALS_' + IntToStr(I),
'LABEL' + IntToStr(J), mapthemes.NodeIntervals[I].Labels[J]);
Ini.WriteFloat('NODE_INTERVALS_' + IntToStr(I),
'VALUE' + IntToStr(J), mapthemes.NodeIntervals[I].Values[J]);
end;
for I := 0 to Length(mapthemes.LinkIntervals)-1 do
for J := 1 to mapthemes.MAXLEVELS do
begin
Ini.WriteString('LINK_INTERVALS_' + IntToStr(I),
'LABEL' + IntToStr(J), mapthemes.LinkIntervals[I].Labels[J]);
Ini.WriteFloat('LINK_INTERVALS_' + IntToStr(I),
'VALUE' + IntToStr(J), mapthemes.LinkIntervals[I].Values[J]);
end;
except
end;

finally
Ini.Free;
end;
end;

procedure ReadLegendIntervals(FileName: string);
//
// Restores legend interval thresholds and basemap grayscale style.
// Must be called AFTER mapthemes.InitThemes has already populated
// NodeIntervals/LinkIntervals with their default values, since this
// procedure only overwrites entries that already exist in those arrays.
//
var
I, J: Integer;
Ini: TIniFile;
begin
if not FileExists(FileName) then exit;
Ini := TIniFile.Create(FileName);
try
try
for I := 0 to Length(mapthemes.NodeIntervals)-1 do
for J := 1 to mapthemes.MAXLEVELS do
begin
mapthemes.NodeIntervals[I].Labels[J] := Ini.ReadString(
'NODE_INTERVALS_' + IntToStr(I), 'LABEL' + IntToStr(J),
mapthemes.NodeIntervals[I].Labels[J]);
mapthemes.NodeIntervals[I].Values[J] := Ini.ReadFloat(
'NODE_INTERVALS_' + IntToStr(I), 'VALUE' + IntToStr(J),
mapthemes.NodeIntervals[I].Values[J]);
end;
for I := 0 to Length(mapthemes.LinkIntervals)-1 do
for J := 1 to mapthemes.MAXLEVELS do
begin
mapthemes.LinkIntervals[I].Labels[J] := Ini.ReadString(
'LINK_INTERVALS_' + IntToStr(I), 'LABEL' + IntToStr(J),
mapthemes.LinkIntervals[I].Labels[J]);
mapthemes.LinkIntervals[I].Values[J] := Ini.ReadFloat(
'LINK_INTERVALS_' + IntToStr(I), 'VALUE' + IntToStr(J),
mapthemes.LinkIntervals[I].Values[J]);
end;
except
end;
finally
Ini.Free;
end;
end;

procedure ReadBasemapStyle(FileName: string);
//
// Restores the basemap's grayscale/brightness display style.
// Must be called AFTER MapFrame.LoadBasemapFromWeb, since loading a
// basemap resets Grayscale to false and Brightness to 0 as part of
// its own initialization.
//
var
Ini: TIniFile;
begin
if not FileExists(FileName) then exit;
Ini := TIniFile.Create(FileName);
try
try
MainForm.MapFrame.Map.Basemap.Grayscale := Ini.ReadBool(
'MAP', 'GRAYSCALE', MainForm.MapFrame.Map.Basemap.Grayscale);
MainForm.MapFrame.Map.Basemap.Brightness := Ini.ReadInteger(
'MAP', 'BRIGHTNESS', MainForm.MapFrame.Map.Basemap.Brightness);
MainForm.MapFrame.Map.Basemap.NeedsRedraw := true;
except
end;
finally
Ini.Free;
end;
Expand Down
79 changes: 77 additions & 2 deletions src/project/project.pas
Original file line number Diff line number Diff line change
Expand Up @@ -1022,16 +1022,91 @@ function Load(FileName: string): Integer;
function Save(FileName: string): Boolean;
var
ErrCode: Integer;
I, J, N, NL, NLbl, Vcount: Integer;
X, Y: Double;
DisplayX, DisplayY: array of Double;
DisplayVertexX, DisplayVertexY: array of array of Double;
DisplayLabelX, DisplayLabelY: array of Double;
UseCache: Boolean;
begin
with MainForm.MapFrame do
if HasWebBasemap then UnloadWebBasemap;
UseCache := MainForm.MapFrame.HasWebBasemap and mapcoords.HasCachedNodeCoords;

if UseCache then
begin
// Temporarily swap in the pristine, never-transformed native
// coordinates for the file write, after remembering the currently
// displayed (WGS84) ones so the map can keep showing them.

// --- Nodes ---
N := project.GetItemCount(ctNodes);
SetLength(DisplayX, N+1);
SetLength(DisplayY, N+1);
for I := 1 to N do
begin
if project.GetNodeCoord(I, X, Y) then
begin
DisplayX[I] := X;
DisplayY[I] := Y;
end;
mapcoords.GetCachedNodeCoord(I, X, Y);
project.SetNodeCoord(I, X, Y);
end;

// --- Link vertices ---
NL := project.GetItemCount(ctLinks);
SetLength(DisplayVertexX, NL+1);
SetLength(DisplayVertexY, NL+1);
for I := 1 to NL do
begin
Vcount := project.GetVertexCount(I);
SetLength(DisplayVertexX[I], Vcount+1);
SetLength(DisplayVertexY[I], Vcount+1);
for J := 1 to Vcount do
begin
project.GetVertexCoord(I, J, X, Y);
DisplayVertexX[I][J] := X;
DisplayVertexY[I][J] := Y;
mapcoords.GetCachedVertexCoord(I, J, X, Y);
project.SetVertexCoord(I, J, X, Y);
end;
end;

// --- Map labels ---
NLbl := project.GetItemCount(ctLabels);
SetLength(DisplayLabelX, NLbl+1);
SetLength(DisplayLabelY, NLbl+1);
for I := 1 to NLbl do
begin
if project.GetLabelCoord(I, X, Y) then
begin
DisplayLabelX[I] := X;
DisplayLabelY[I] := Y;
end;
mapcoords.GetCachedLabelCoord(I, X, Y);
project.SetLabelCoord(I, X, Y);
end;
end;

ErrCode := epanet2.ENsaveinpfile(PAnsiChar(FileName));
Result := (ErrCode = 0);
if Result then
begin
projectmapdata.SaveMapData(FileName);
HasChanged := false;
end;

if UseCache then
begin
// Restore the WGS84 coordinates so the on-screen map is unaffected -
// no transform call involved, so no rounding drift is introduced.
for I := 1 to N do
project.SetNodeCoord(I, DisplayX[I], DisplayY[I]);
for I := 1 to NL do
for J := 1 to Length(DisplayVertexX[I])-1 do
project.SetVertexCoord(I, J, DisplayVertexX[I][J], DisplayVertexY[I][J]);
for I := 1 to NLbl do
project.SetLabelCoord(I, DisplayLabelX[I], DisplayLabelY[I]);
end;
end;

procedure SetTitle(Line1: string; Line2: string; Line3: string);
Expand Down