ABOUT ME

-

Total
-
  • Go: 카카오맵 API 정적 지도 이미지에 여러 마커들 추가하기
    컴퓨터/Go language 2024. 4. 11. 16:45
    728x90
    반응형

    pdf 파일 안 정적 지도 이미지

    오프라인 지도 저장 API를 만들어 보고 싶어서, 카카오맵 정적 지도 문서를 봤다.

    하지만, 하나의 마커만 지원한다.

    https://apis.map.kakao.com/web/sample/staticMapWithMarker/

     

    사진 렌더링하는 js 코드를 보면, CX와 CY (마커의 WCONGNAMUL 값)을 

    하나 밖에 안 받고 렌더링해서 이 부분을 고쳐보고 싶었으나 다른 방식을 택했다.

    c.Va = function() {
            var a = this.Gd
              , b = [];
            b.push("IW=" + String(this.b.m() | 0) + "&IH=" + String(this.b.i() | 0));
            b.push(sf(this));
            b.push("SCALE=" + String(0.3125 * (1 << this.b.k())));
            /png|gif|bmp/i.test(this.fg) && b.push("FORMAT=" + String(this.fg).toUpperCase());
            var c, f = [], e = gd(this.gb) ? this.gb : [this.gb];
            bc(e, function(a, b) {
                var c = K(b.position || tc(this)), d;
                f.push("CX=" + String(c.e() | 0) + "&CY=" + String(c.c() | 0));
                (d = b.text) && f.push("TX=%x&TY=%y&TEXT=%text".replace(/%x/, String(c.e() | 0)).replace(/%y/, String(20.625 * (1 << this.b.k()) + c.c() | 0)).replace(/%text/, encodeURI(String(d))))
            }, this);
            (c = f.join("&")) && b.push(c);
            b.push("service=open");
            c = "imageservice?";
            if (2 == this.ya || 3 == this.ya)
                c = "skyview" + c;
            3 == this.ya && (c += "RDR=HybridRender&");
            a.T.src = Ef + c + b.join("&");
        
      ...
      }

     

    Workaround

    어떻게 하면 여러 마커를 만들어서 사진을 저장할 수 있을까?

    프론트엔드에서 버튼을 다 숨기는 UI를 만들고 인쇄 기능을 이용해도 되겠지만, pdf를 컨트롤 해보고 싶었다.

     

    정적 지도 이미지마다 하나의 마커를 지원하니까,

    중앙 좌표 값에서 500m 반경 (1280x1080 크기로 사진 저장 기준) DB에 저장된 마커들을 보여주기 위해

    정적 지도 이미지 마커 * n개 (n = 500m 반경 마커 리스트) + 1 (마커 하나도 없는 기본 이미지) 를 다운로드하였다.

    n+1 개의 정적 지도 이미지를 다운로드한 이유는

     

    이미지 차이 방식을 이용했기 때문에 base 사진에서 각 마커 1개씩 있는 사진마다 차이점을 찾아서

    결과.png 에 넣으면 마커들을 저장할 수 있다.

    이미지 차이

     

    Python 에서는 PIL이나 다른 이미지 처리 프로세싱 라이브러리로 쉽게 찾을 수 있다.

    여기서는 go언어의 기본 라이브러리를 이용해서 단순히 RGB 값의 차이 값을 이용해서 다른 점을 찾을 것이다.

     

    [0,255] 컬러 range로 변경하고, RGB 전체 차이가 threshold^2 인지 정도로 이용했다. (10~15도 괜찮게 작동)

    func colorsAreSimilar(c1, c2 color.Color) bool {
    	const threshold = 10
    
    	r1, g1, b1, _ := c1.RGBA()
    	r2, g2, b2, _ := c2.RGBA()
    
    	diffR := uint8(r1>>8) - uint8(r2>>8)
    	diffG := uint8(g1>>8) - uint8(g2>>8)
    	diffB := uint8(b1>>8) - uint8(b2>>8)
    
    	dist := int(diffR)*int(diffR) + int(diffG)*int(diffG) + int(diffB)*int(diffB)
    
    	return dist < threshold*threshold
    }

     

    오버레이하는 함수에서는 다음과 같고, 이미지 path를 리턴 시켰다.

    func OverlayImages(baseImageFile, markerImagePath string) (string, error) {
    	originalBaseImg, _, err := loadImage(baseImageFile) // Load the original base image
    	if err != nil {
    		return "", err
    	}
    	originalBaseBounds := originalBaseImg.Bounds()
    
    	resultImg := image.NewRGBA(originalBaseBounds)
    	draw.Draw(resultImg, originalBaseBounds, originalBaseImg, image.Point{}, draw.Src)
    
    	files, err := os.ReadDir(markerImagePath)
    	if err != nil {
    		return "", fmt.Errorf("failed to read marker image path: %w", err)
    	}
    
    	for _, file := range files {
    		if filepath.Ext(file.Name()) == ".png" {
    			markerImg, _, err := loadImage(filepath.Join(markerImagePath, file.Name()))
    			if err != nil {
    				fmt.Println("Warning: skipping file due to error:", err)
    				continue // Skip files that can't be loaded
    			}
    			overlayDifferences(resultImg, markerImg, originalBaseImg)
    		}
    	}
    
    	resultPath := filepath.Join(markerImagePath, "result.png")
    	if err := saveImage(resultImg, resultPath); err != nil {
    		return "", fmt.Errorf("failed to save result image: %w", err)
    	}
    
    	return resultPath, nil
    }
    
    
    func overlayDifferences(base *image.RGBA, overlay image.Image, originalBase image.Image) {
    	bounds := base.Bounds()
    	for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
    		for x := bounds.Min.X; x < bounds.Max.X; x++ {
    			baseColor := originalBase.At(x, y)
    			overlayColor := overlay.At(x, y)
    			if !colorsAreSimilar(baseColor, overlayColor) {
    				base.Set(x, y, overlayColor)
    			}
    		}
    	}
    }

     

    PDF 사용

    fpdf 라이브러리를 사용했는데, makefont 할 필요 없이 폰트 하나 로컬에 저장해서 다음과 같이

    한글 폰트를 사용할 수 있다.

    func GenerateMapPDF(imagePath, tempDir, title string) (string, error) {
    	pdf := fpdf.New("P", "mm", "A4", "")
    	pdf.AddPage()
    
    	pdf.AddUTF8Font("NanumGothic", "", "fonts/nanum.ttf")
    
    	pdf.SetFont("NanumGothic", "", 10)
    	pdf.CellFormat(190, 10, "More at k-pullup.com", "0", 1, "C", false, pdf.AddLink(), "https://ㅇㅇ.com")
    	pdf.SetFont("NanumGothic", "", 16)
    	pdf.CellFormat(190, 10, title, "0", 1, "C", false, 0, "")
    
    	pdf.ImageOptions(imagePath, 10, 30, 190, 0, false, fpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}, 0, "")
    
    	pdfName := fmt.Sprintf("제목-%s.pdf", uuid.New().String())
    	pdfPath := path.Join(tempDir, pdfName)
    	err := pdf.OutputFileAndClose(pdfPath)
    	if err != nil {
    		return "", err
    	}
    
    	return pdfPath, nil
    }

     

    fiber 백엔드 다운로드

    c.Download(로컬파일path) 를 이용하면 알아서 attachtment; filename= 을 해준다.

    func SaveOfflineMapHandler(c *fiber.Ctx) error {
    	latParam := c.Query("latitude")
    	longParam := c.Query("longitude")
    
    ...
    
    	pdf, err := services.SaveOfflineMap(lat, long)
    	if err != nil {
    		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create a PDF: " + err.Error()})
    	}
    
    	return c.Download(pdf)
    }

     

    오버레이 overhead

    오버레이 하기 위해 여러 이미지를 전부 다운로드하여야 한다.

    아무리 goroutine 으로 해도 조금 느리다.

    그래서 WCONGNAMUL 좌표계를 (x, y) 이미지 픽셀 위치로 변환하는 방법을 찾아내서 아래처럼 빠르게

    base 지도 한 장으로 커스텀 마커 이미지를 이용해 직접 그릴 수도 있다.

    (WGS84 좌표 -> WCONGNAMUL 변환 글)

    // PlaceMarkersOnImage places markers on the given base image according to their WCONGNAMUL coordinates.
    func PlaceMarkersOnImage(baseImageFile string, markers []WCONGNAMULCoord, centerCX, centerCY float64) (string, error) {
    	baseImg, _, err := loadImage(baseImageFile)
    	if err != nil {
    		return "", err
    	}
    	bounds := baseImg.Bounds()
    	resultImg := image.NewRGBA(bounds)
    	draw.Draw(resultImg, bounds, baseImg, image.Point{}, draw.Src)
    
    	// SCALE by 2.5 in 1280x1080 image only, center (centerCX, centerCY).
    	// Load the marker icon
    	markerIconPath := "fonts/marker_40x40.webp"
    	markerIcon, _ := LoadWebP(markerIconPath)
    	markerBounds := markerIcon.Bounds()
    	markerWidth := markerBounds.Dx()
    	markerHeight := markerBounds.Dy()
    
    	for _, marker := range markers {
    		x, y := PlaceMarkerOnImage(marker.X, marker.Y, centerCX, centerCY, bounds.Dx(), bounds.Dy())
    
    		// Calculate the top-left position to start drawing the marker icon
    		// Ensure the entire marker icon is within bounds before drawing
    		startX := x - int(markerWidth)/2 - 5
    		startY := y - int(markerHeight)
    		draw.Draw(resultImg, image.Rect(startX, startY, startX+int(markerWidth), startY+int(markerHeight)), markerIcon, image.Point{0, 0}, draw.Over)
    
    		// Ensure (x,y) is within bounds
            // red dot을 그립니다
    		//if x >= 0 && x < bounds.Dx() && y >= 0 && y < bounds.Dy() {
    		//	resultImg.Set(x, y, color.RGBA{255, 0, 0, 255}) // Red color
    		//}
    	}
    
    	resultPath := filepath.Join(filepath.Dir(baseImageFile), "result_with_markers.png")
    	if err := saveImage(resultImg, resultPath); err != nil {
    		return "", fmt.Errorf("failed to save image with markers: %w", err)
    	}
    
    	return resultPath, nil
    }

     

    kpullup-ebd762de-f5d4-469a-a515-a1192cca95b8.pdf
    0.44MB

    728x90

    댓글