diff --git a/src/core/main.pas b/src/core/main.pas index 65d80b6..a91244c 100644 --- a/src/core/main.pas +++ b/src/core/main.pas @@ -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; diff --git a/src/core/mapframe.pas b/src/core/mapframe.pas index 35b5d00..18347be 100644 --- a/src/core/mapframe.pas +++ b/src/core/mapframe.pas @@ -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; diff --git a/src/map/mapcoords.pas b/src/map/mapcoords.pas index 375e103..fa0ede6 100644 --- a/src/map/mapcoords.pas +++ b/src/map/mapcoords.pas @@ -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 @@ -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; @@ -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; @@ -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. diff --git a/src/project/inifile.pas b/src/project/inifile.pas index 7c16323..db8d376 100644 --- a/src/project/inifile.pas +++ b/src/project/inifile.pas @@ -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 @@ -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); @@ -263,6 +265,8 @@ 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), @@ -270,9 +274,95 @@ procedure WriteProjectMapOptions(FileName: string; WebMapSource:Integer); 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; diff --git a/src/project/project.pas b/src/project/project.pas index 2296e6d..a4bdb5f 100644 --- a/src/project/project.pas +++ b/src/project/project.pas @@ -1022,9 +1022,71 @@ 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 @@ -1032,6 +1094,19 @@ function Save(FileName: string): Boolean; 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);